Reference
kogut.toml
Configuration keys, types, defaults, and validation rules.
kogut.toml defines services, checks, heartbeats, host alert thresholds, and
notification channels. File-managed objects are read-only in the panel.
File handling
The default path is $KOGUT_DATA/kogut.toml; KOGUT_CONFIG
overrides it. A missing or empty file means the defaults. All top-level tables are
optional. Unknown keys are errors.
kogut checks the file every 2 seconds and applies a valid file atomically. An invalid
file stops startup. An invalid reload keeps the previous configuration and reports the
error in the panel and log. listen needs a restart. There is no reload signal.
Removing an object from the file removes it from kogut, and deleting the file counts as
an empty file. kogut never writes the file. The MCP propose_ tools
return tables to add to it, judged as a reload would judge them.
Checks and heartbeats share one namespace with panel-created checks and heartbeats, and
channels share another. Panel channel names cannot use the file’s naming scheme, such as
ntfy or slack-2. When the file names a check, heartbeat, or channel created in the
panel:
- With the same definition, the file takes it over. It becomes file-managed and keeps
its id, results, incidents, ping URL, silences, and deliveries. A channel’s secrets
are removed from the database. The same definition means, for a check, the same
service, type, target,
from,every,timeout,method, andexpect; for a heartbeat, the same service, schedule, andgrace; for a channel, the sametypeand keys, secrets included. - With another definition, the file is rejected. The error names the first key that
differs, such as
check[2].every: a check named "api" was created in the panel with another definition. Make the two equal, or rename one. - A name used by another kind of object, such as a panel check for a file heartbeat, rejects the file.
A takeover is one-way: from then on only the file changes the object, and removing it from the file deletes it. A command check never rejects the file: the file’s check takes the name, and the host page names the command check that was left out.
Move panel objects into the file
kogut export prints the checks, heartbeats, and channels
created in the panel, and the kogut edge and heartbeat URL set there, as kogut.toml
that the next reload takes over. Settings → Move to kogut.toml in the panel copies
the same text:
kogut export >> /var/lib/kogut/kogut.tomlSecrets are written as ${NAME} placeholders, listed at the top of the output. Set
those variables before kogut reloads the file; until then the reload is rejected and the
panel objects stay as they are.
A service or host already in the database becomes file-managed when the file names it,
in [[service]] hosts or in [[host]]. A [[host]] never clashes with a panel host:
the file takes it over. If the file later drops it, it stays panel-managed while panel
checks use it. Sites, users, tokens, ping URLs, silences, maintenance windows, and the
status page exist only in the database.
Check the file
kogut config check checks a file without starting the
server. In GitHub Actions, run it from the image; --allow-unset checks the file
without its secrets:
- name: Check kogut.toml
run: docker run --rm -v "$PWD:/repo:ro" ghcr.io/piotrkowski/kogut:0.1 config check --allow-unset /repo/kogut.tomlErrors read <file>:<line>:<column>: <key>: <message>. Lines and columns start at 1,
columns count characters, and array indexes start at 0:
/var/lib/kogut/kogut.toml:14:3: check[2].every: 5s is too often; checks run at most every 10sA failed validation points at its key, a missing key at its table, such as the
[[check]] line, and a syntax or type error at the exact character. A name clash with
the panel gives the key path without a position. Errors about addresses omit the value,
which may contain a secret. The startup error, the reload log, and the panel show the
same text.
Secrets
String values support ${NAME} substitution from the environment:
secret = "${GITHUB_WEBHOOK_SECRET}"Names contain letters, digits, and _, and cannot start with a digit. $NAME stays
literal. There is no default syntax or recursive expansion. Keys, numbers, and booleans
are not expanded. Durations are strings, so every = "${EVERY}" works.
If the variable is unset, kogut reads $CREDENTIALS_DIRECTORY/NAME, without one
trailing newline. If neither exists, the file is invalid. Substitutions are read again
on reload. File secrets, including channel tokens and check headers, never enter the
database.
Durations
A whole number and a unit: ms, s, m, h, or d, such as "500ms", "30s",
"5m", "1h", or "7d". Units cannot be combined: write "90m", not "1h30m".
Example
[server]
url = "https://kogut.acme.com"
[[service]]
name = "api"
hosts = ["vps-1"]
depends_on = ["postgres"]
[[service]]
name = "postgres"
hosts = ["vps-1"]
[[check]]
name = "API"
service = "api"
url = "https://api.acme.com/health"
expect = [{ json = "$.db", equals = "ok" }]
[[host]]
name = "vps-1"
disk = 95
[[alert]]
type = "ntfy"
url = "https://ntfy.sh/acme-ops"[server]
Environment variables override listen and url.
| Key | Type | Default | Description |
|---|---|---|---|
listen | string | "0.0.0.0:8484" | Listen address and port for all HTTP endpoints. Bare ports are invalid. Read once at startup. |
url | string | none | Public panel URL, starting with http:// or https://. Used for generated links, snippets, agent commands, and panel Origin checks. HTTPS marks session cookies Secure. Without a URL, kogut uses the request’s Host. |
timezone | string | the machine’s zone | IANA zone for notification times, the daily summary at 07:00, and visitor salt rotation at 04:00. Defaults to TZ, then /etc/localtime. An unreadable configured zone logs an error and uses UTC. Heartbeat cron has its own zone. |
trusted_proxies | list of strings | [] | Proxy IPs or networks trusted for visitor addresses in X-Forwarded-For and X-Real-IP. Loopback and [auth.proxy] trusted_proxies are also trusted. Used for rate limits and visitor counts, never sign-in. |
connectivity_hosts | list of strings | ["1.1.1.1:443", "9.9.9.9:443"] | host:port connectivity probes, every 30 seconds while a server-run check targets a public address. Two consecutive rounds without a response mark kogut offline. [] disables probes. See connectivity handling. |
heartbeat_url | string | none | HTTP or HTTPS watchdog URL. Requested with GET at startup and every minute, with a 10-second timeout. Overrides the panel setting while present. Often contains a secret. See Heartbeat URL. |
[brand]
Branding for the status page, not the panel. Without [brand], the page
and browser tab read “Status”.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Page heading. The browser title adds “status” unless the name already ends with it. |
logo | string | none | A URL starting with http:// or https://, or a file path relative to the data directory (no ..). A file is served as /status-logo on the status domain. Supports SVG, PNG, WebP, and JPEG. |
accent | string | none | A colour as #rgb or #rrggbb. Colours the name in the status page header when there is no logo. |
status_domain | string | none | Status hostname, without a scheme or path. Serves the status page and subscription routes, not the panel or API. |
subscriptions | boolean | false | Email subscriptions, sent through the first email notification channel. Links use https://<status_domain>, or the panel’s public URL. See Email updates. |
[[service]]
A service named by a check or heartbeat but not declared is created with defaults.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique name, such as api. Up to 100 characters, no spaces at the start or end. |
hosts | array of strings | [] | Agent hosts the service runs on. Each is created in kogut; connect its agent from the panel. |
sites | array of strings | [] | Domains of existing sites linked to this service. Does not create sites. Each domain belongs to at most one service. |
depends_on | array of strings | [] | Dependencies. When a dependency is down, this service’s alerts join its incident instead of opening their own. A failing shared host takes precedence over a dependency as the root. Cycles are invalid. See alert grouping and likely causes. |
tier | string | "standard" | "critical", "standard", or "low". Orders services in the panel and in the daily summary. |
[[host]]
Alert thresholds of one agent host. A key you leave out keeps its default. A host named here is created in kogut and becomes file-managed, and its thresholds are read-only in the panel. Removing the table restores the defaults.
[[host]]
name = "nas"
disk = 95
swap = false
temperature = 80
ignore_mounts = ["/mnt/backup"]| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | The host, as in [[service]] hosts. Each host has at most one [[host]]. |
cpu | integer or false | 90 | Percent. CPU above it for 5 min degrades CPU on <host>. |
memory | integer or false | 90 | Percent. Memory in use above it for 5 min degrades Memory on <host>. |
swap | integer or false | 50 | Percent. Swap in use above it for 5 min degrades Swap on <host>. |
disk | integer or false | 90 | Percent. The fullest mount above it degrades Disk on <host> at once, with the mount named, such as /srv 93%, above 90%. |
temperature | integer or false | false | Degrees Celsius, 1 to 150. The hottest sensor above it for 5 min degrades Temperature on <host>. A host without sensors never alerts. |
ignore_mounts | array of strings | [] | Mount points the disk alert leaves out, as the host page lists them, such as "/mnt/nas". Each is an absolute path. The disk chart still shows every mount. |
Percentages are whole numbers from 1 to 100. false turns that alert off; true is an
error. A changed limit applies from the host’s next sample; after a raised limit, the
5 min start again from the first sample above it. Turning off an open alert closes it.
The 5 min are fixed.
For hosts added in the panel, set the same values under Alerts → Edit alerts on the host page.
[[check]]
Each check has exactly one target, url, tcp, dns, ping, or domain, which sets
its kind. See Checks.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Shown in alerts and the panel. Unique among checks and heartbeats. Up to 100 characters. |
service | string | required | The service the check belongs to. |
url | string | none | An HTTP or HTTPS URL. Makes an HTTP check. HTTPS checks also watch the certificate: degraded 14 days before it expires, down 7 days before. A user name or password in the URL is an error; send Basic authentication in headers. |
tcp | string | none | host:port, such as "db.internal:5432" or "[2001:db8::1]:22". Makes a TCP check. |
dns | string | none | A name to resolve, such as "acme.com". Makes a DNS check. |
ping | string | none | A host name or IP address, such as "router.lan" or "10.0.0.1", without a port. Makes a ping check: three ICMP echo requests per run. |
domain | string | none | A registered domain, such as "acme.com". Makes a domain check, which reads the registration expiry from the registry once a day. One check per domain. |
method | string | "GET" | HTTP method, in capitals: GET, HEAD, POST, PUT, PATCH, or DELETE. Only with url. OPTIONS is accepted but the check fails when it runs. Checks send no body. |
headers | inline table | none | Request headers, such as { Authorization = "Bearer ${API_TOKEN}" }. Only with url. See below. |
every | duration | "1m" for HTTP, TCP, and ping, "5m" for DNS | Interval from "10s" to "1d". Domain checks run daily and reject this key. |
timeout | duration | "10s" | How long to wait. Between "100ms" and "1m", and not longer than every. At least "1s" for ping checks. |
from | string | the server | Agent host, listed in a service’s hosts. Domain checks always run on the server. See Internal services. |
expect | array of inline tables | 2xx status | Assertions about the response. Only with url, and with ping, where only response_time_below is allowed. |
headers
Request headers for an HTTP check, as an inline table of names and string values. Values
can use ${NAME}.
- Names are HTTP header names. The same name twice in different case is an error.
- Values allow printable ASCII and tabs, up to 8 KiB, without line breaks. At most 50 headers.
Connection,Content-Length,Keep-Alive,Proxy-Connection,TE,Trailer,Transfer-Encoding, andUpgradeare set by kogut and are errors here.HostandUser-Agentreplace kogut’s own.- The headers go only to the origin of
url. A redirect to another scheme, host, or port is followed without them. - Header values are never logged, shown in the panel, or written to the database. Error messages about a value name the header, not the value.
expect
Each item is one assertion. All must pass for the check to be up.
| Assertion | Example | Passes when |
|---|---|---|
status | { status = 200 }, { status = "2xx" } | The status is that code (100 to 599), or in that class (1xx to 5xx). |
body_contains | { body_contains = "Welcome" } | The body contains the text. |
body_not_contains | { body_not_contains = "Maintenance" } | The body does not contain the text. |
json | { json = "$.version" } | The body is JSON and the path exists. A path starts with $, followed by .key and [index] steps, such as $.a.b[0].c. |
json with equals | { json = "$.db", equals = "ok" } | The value at the path equals the given value. Any TOML value works: equals = true, equals = 3. |
response_time_below | { response_time_below = "800ms" } | The response arrived faster than the limit. |
response_time_below with else | { response_time_below = "800ms", else = "degraded" } | As above. A slower response makes the check degraded instead of down. else is "degraded" or "down" (the default). |
- Without a
statusassertion, the response must be2xx. - A
429or503that fails the status assertion makes the check degraded, not down, and the body assertions are skipped. GETandHEADchecks follow up to 5 redirects. Other methods follow none. kogut reads at most 1 MiB of the body.
When an assertion fails, the alert shows what kogut saw:
status 503, expected 2xx
$.db is "degraded", expected "ok"[[heartbeat]]
A heartbeat alerts when a scheduled task misses a ping, starting from its first ping.
Ping URLs are created in the panel or through the API. To keep
a token your jobs already send, set ping_token; see
Keep an existing push URL.
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Unique among checks and heartbeats. |
service | string | required | The service the heartbeat belongs to. |
every | duration | none | The longest time between pings. Between "1m" and "31d". |
cron | string | none | A five-field cron expression (minute, hour, day of month, month, and day of week), such as "0 3 * * *". Lists, ranges, steps, and names such as mon and jan work. An expression that can never fire is an error. Give either every or cron. |
timezone | string | "UTC" | An IANA time zone for cron, such as "Europe/Warsaw". Only with cron. [server] timezone does not apply. |
grace | duration | a fifth of every (between 1 min and 1 h), or "1h" with cron | How late a ping may be before it is missed. Between "1m" and "7d". |
ping_token | string | none | A token that pings this heartbeat at /ping/<token> and /api/push/<token>. 10 to 128 letters, digits, -, or _, unique among heartbeats, and not the token of another heartbeat’s panel ping URL. Give it as a secret, such as "${BACKUP_PUSH_TOKEN}". kogut keeps it in memory only, not in the database. A ping URL made in the panel works next to it. |
[[alert]]
A notification channel. type selects its keys. See
Notification channels and
How alerts work.
type | Keys |
|---|---|
"ntfy" | url (required, the topic URL), token (an access token for protected topics), allow_private (default false, true to reach a server on a private or loopback address) |
"slack" | url (required, an incoming webhook URL) |
"discord" | url (required, a channel webhook URL) |
"telegram" | bot_token (required), chat_id (required) |
"webhook" | url (required), secret (signs the timestamp and body of each request with HMAC-SHA256, see Verify the signature), allow_private (default false, true to reach a private or loopback address) |
"email" | smtp (required, host:port), tls ("starttls", "implicit", or "none", default "implicit" on port 465 and "starttls" on any other), username and password (together or not at all, never with tls = "none"), from (required), to (required, at least one address), allow_private (default false, true to reach a relay on a private or loopback address) |
Without allow_private, kogut delivers only to public addresses. Slack, Discord, and
Telegram always need one. For other services, such as Microsoft Teams or Pushover, use a
webhook to an Apprise API server.
name (optional) names the channel: lowercase letters, digits, and hyphens, such as
ops-phone, unique among [[alert]] tables. Without it, the channel is named after its
type in file order: ntfy, then ntfy-2, and so on, counting named channels too. A
name, or a panel channel’s name, cannot have that form. A named channel with the same
name and settings as a panel channel takes it over (see File handling).
[auth]
Sign-in to the panel: passwords, OpenID Connect, and forward auth. Several can be on at once.
| Key | Type | Default | Description |
|---|---|---|---|
password | boolean | true | Local accounts with passwords. false is accepted only with [[auth.oidc]] or [auth.proxy]. |
[[auth.oidc]]
One table per provider, each a button on the sign-in page. Needs the public URL
(KOGUT_URL or [server] url). See OpenID Connect.
| Key | Type | Default | Description |
|---|---|---|---|
preset | string | none | "authentik", "authelia", "keycloak", "pocket_id", "zitadel", "kanidm", "google", or "entra". |
issuer | string | "https://accounts.google.com" for Google, otherwise required | The issuer URL, https:// (plain http:// only on loopback). |
client_id | string | required | |
client_secret | string | required | Write it as ${ENV_NAME}. |
scopes | array of strings | ["openid", "email", "profile"] | Authelia, Pocket ID, and Kanidm add "groups". Zitadel adds "urn:zitadel:iam:org:projects:roles". Must include "openid". |
groups_claim | string | "groups" | None for Google. Zitadel uses "urn:zitadel:iam:org:project:roles". |
admin_groups | array of strings | [] | Members are admins. When set, groups decide the role at every sign-in. |
allowed_domains | array of strings | any domain, required with Google | Email domains that may sign in. |
[auth.proxy]
A reverse proxy passes the person’s email in a header. See Forward auth.
| Key | Type | Default | Description |
|---|---|---|---|
trusted_proxies | array of strings | required | IP addresses or networks of the proxy. Identity headers from any other TCP peer are ignored. These addresses are also trusted to name the visitor’s address in X-Forwarded-For, like [server] trusted_proxies. |
preset | string | none | "authelia", "authentik", "oauth2_proxy", "pomerium", "cloudflare_access", or "tailscale". |
headers | inline table | none | { email = "...", groups = "..." }, email required. Replaces preset. |
admin_groups | array of strings | [] | Members are admins. Requires a groups header. |
[github]
Deploys from a GitHub webhook. Without this table, /hooks/github
answers 404.
| Key | Type | Default | Description |
|---|---|---|---|
secret | string | required | The webhook secret, used to check X-Hub-Signature-256. Write it as ${ENV_NAME}. |
repo | array of tables | none | One [[github.repo]] per repository and service: name as owner/repo, and the service it deploys, which must be a service known to the file. Names match without regard to case. A repository listed for several services marks a deploy of each. |
Each [[github.repo]] takes these keys:
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | The repository, as owner/repo. |
service | string | required | The service it deploys. |
environment | string | production deployments only | The only deployment environment recorded for this service, compared without regard to case. Applies to deployment_status only. See Deployments. |
Pushes to the default branch, published releases, and successful production deployment
statuses become deploy markers. branch is not a supported key.
[edge]
kogut sends the kogut edge watchdog a heartbeat every 30 seconds. This table overrides the panel setting and locks it. Failures retry after 5, 10, 20, and 40 seconds, then every minute.
| Key | Type | Default | Description |
|---|---|---|---|
url | string | required | The Worker’s URL, with http:// or https://. kogut posts to <url>/heartbeat. |
token | string | required | The token the Worker expects with each heartbeat, the same as its KOGUT_EDGE_TOKEN secret. |
[retention]
How long data is kept. Each value is between "1h" and "3650d". Older data is deleted
in small batches in the background.
| Key | Type | Default | Description |
|---|---|---|---|
check_results | duration | "7d" | Raw results of every check run, and heartbeat pings. |
host_metrics | duration | "1d" | Raw host metrics, every 15 s: CPU, memory, disks, and containers. systemd unit states, CPU, and memory are stored when a state changes and once a minute. |
minute_buckets | duration | "30d" | One- and five-minute rollups of checks and host metrics, and five-minute rollups of container and systemd unit CPU and memory. |
hourly_buckets | duration | "365d" | Hourly and daily check and host rollups, hourly container and unit CPU and memory, plus hourly Web Vitals for long-range charts. |
analytics_events | duration | "90d" | Raw page views, visits, custom events, and Web Vitals. Minimum "3d". Daily rollups require complete days of raw page views. Older analytics uses those rollups. |
analytics_rollups | duration | "400d" | Daily analytics totals and top dimension values. Minimum "366d" for complete 1y ranges. |
error_events | duration | "30d" | Single JavaScript errors. Error groups are kept. |