# 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`](https://docs.kogut.dev/reference/environment-variables/)
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](https://docs.kogut.dev/reference/mcp/#proposals) `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`, and `expect`; for a
  heartbeat, the same service, schedule, and `grace`; for a channel, the same `type` and
  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](https://docs.kogut.dev/agent/command-checks/) 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`](https://docs.kogut.dev/reference/cli/#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:

```sh
kogut export >> /var/lib/kogut/kogut.toml
```

Secrets 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`](https://docs.kogut.dev/reference/cli/#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:

```yaml
- name: Check kogut.toml
  run: docker run --rm -v "$PWD:/repo:ro" ghcr.io/piotrkowski/kogut:0.1 config check --allow-unset /repo/kogut.toml
```

Errors read `<file>:<line>:<column>: <key>: <message>`. Lines and columns start at 1,
columns count characters, and array indexes start at 0:

```text
/var/lib/kogut/kogut.toml:14:3: check[2].every: 5s is too often; checks run at most every 10s
```

A 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:

```toml
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

```toml title="kogut.toml"
[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](https://docs.kogut.dev/reference/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](https://docs.kogut.dev/alerts/how-alerts-work/#when-kogut-loses-its-internet-connection). |
| `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](https://docs.kogut.dev/high-availability/heartbeat-url/). |

## `[brand]`

Branding for the [status page](https://docs.kogut.dev/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](https://docs.kogut.dev/status-page/#enable-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](https://docs.kogut.dev/web-apps/tracking-script/) 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](https://docs.kogut.dev/alerts/how-alerts-work/) and [likely causes](https://docs.kogut.dev/incidents/correlation/). |
| `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.

```toml title="kogut.toml"
[[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](https://docs.kogut.dev/monitoring/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](https://docs.kogut.dev/monitoring/checks/#request-headers) 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](https://docs.kogut.dev/monitoring/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`, and `Upgrade` are set by kogut and are errors here. `Host` and
  `User-Agent` replace 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 `status` assertion, the response must be `2xx`.
- A `429` or `503` that fails the status assertion makes the check **degraded**, not down,
  and the body assertions are skipped.
- `GET` and `HEAD` checks 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:

```text
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](https://docs.kogut.dev/reference/http-api/#ping-urls). To keep
a token your jobs already send, set `ping_token`; see
[Keep an existing push URL](https://docs.kogut.dev/monitoring/heartbeats/#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](https://docs.kogut.dev/alerts/channels/) and
[How alerts work](https://docs.kogut.dev/alerts/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](https://docs.kogut.dev/alerts/channels/#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](https://docs.kogut.dev/alerts/channels/#apprise) 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](#file-handling)).

## `[auth]`

Sign-in to the panel: [passwords](https://docs.kogut.dev/sign-in/passwords/),
[OpenID Connect](https://docs.kogut.dev/sign-in/openid-connect/), and [forward auth](https://docs.kogut.dev/sign-in/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](https://docs.kogut.dev/sign-in/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](https://docs.kogut.dev/sign-in/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](https://docs.kogut.dev/deploys/github/). 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](https://docs.kogut.dev/deploys/github/#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](https://docs.kogut.dev/high-availability/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. |
