The server exits when the database does not answer within about ten seconds, so while the database is down the pod crash-loops with a restart backoff that grows to five minutes, and /healthz only answers after it has connected. The hook gave up after a fixed two minutes per attempt, and Flux's default five-minute timeout came first anyway. A failed hook fails the release and nothing runs it again until the next upgrade, so a database that was merely slow left the install without an administrator. Each attempt now waits bootstrap.waitSeconds (default 600), logging progress, and the Job gets an activeDeadlineSeconds derived from it so a database that never comes up still ends the hook. No change to the binary, API or schema. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
4.6 KiB
Deployment
Running the server in a container and on Kubernetes with the Helm chart. Back to the README and the documentation index.
Docker
docker build -t terdut-server .
docker run -p 8080:8080 \
-e TERDUT_DB_DSN='postgres://terdut:secret@host.docker.internal:5432/terdut?sslmode=disable' \
terdut-server
The server creates its own schema on startup and needs a reachable Postgres; it stores nothing on disk, so there is no volume to mount.
Kubernetes
A Helm chart is published from this repository as an OCI artifact, versioned in lockstep
with the app — chart x.y.z is always app vx.y.z:
helm upgrade --install terdut-server oci://git.ryuvia.com/niklas/terdut-server \
--version 0.9.2 \
--namespace terdut-server --create-namespace \
--set networking.hostname=terdut.example.com
The chart expects a Gateway API Gateway named envoy-main in
the envoy-gateway-system namespace to already exist — it renders an HTTPRoute against it rather
than an Ingress. TLS is terminated at the gateway, so the server itself never sees a certificate.
| Value | Default | Description |
|---|---|---|
networking.hostname |
terdut.example.com |
Hostname the HTTPRoute serves |
networking.listener |
"" |
Gateway listener (sectionName) to bind to. Empty attaches to every matching listener, including plaintext HTTP — set it to the HTTPS listener's name to serve TLS only |
networking.servicePort |
8080 |
Port the route forwards to; keep in sync with service.port |
bootstrap.enabled |
true |
Runs a post-install/upgrade hook that creates the first administrator with a generated password and stores username, password and api-key in the <release>-admin-key Secret. The password is generated once and reused, so recreating the database brings the same account back. Already-bootstrapped servers are left alone. Set false to create the first user yourself with POST /api/bootstrap |
bootstrap.waitSeconds |
600 |
How long each attempt of the hook waits for the server to answer /healthz, which it does only once the database is reachable. Keep Helm's --timeout (Flux spec.timeout) above it; if the timeout comes first the release is marked failed, but the Job keeps running and can still create the administrator |
bootstrap.username / bootstrap.email |
admin / admin@example.com |
Account the hook creates |
bootstrap.secretName |
"" |
Secret to keep the credentials in; empty means <release>-admin-key |
database.dsn |
"" |
Required. Postgres DSN, with no password in it. The chart provisions no database |
database.passwordSecret.name |
"" |
Secret supplying PGPASSWORD. With the Zalando postgres operator, the Secret it generates for the role |
database.passwordSecret.key |
password |
Key within that Secret |
The API key travels in an Authorization: Bearer header, so set networking.listener whenever the
hostname is reachable outside a trusted network.
The database
The chart provisions no database: it takes a DSN and expects a Postgres that already exists. In this
cluster the wrapper chart declares an acid.zalan.do/v1 postgresql CR; anywhere else, any reachable
Postgres 14+ will do.
The DSN carries no password. pgx falls back to libpq's environment variables for whatever the DSN
leaves out, so the password arrives as PGPASSWORD from a Secret and never appears in values, in
the rendered manifest or in kubectl describe pod. With the postgres operator that Secret is the
one it generates for the role, so a rebuild mints a new password with nothing to keep in sync —
the same wiring miniflux uses.
The server migrates its own schema on startup, so a new database only has to exist and be writable.
Backups
Postgres is backed up where it runs, not from here. The database pod carries a
k8up k8up.io/backupcommand annotation that streams a pg_dump, the same way
gitea and immich do in this cluster.
On Kubernetes with the operator
terdut-operator runs a server for you from a
TerdutServer object and manages its teams, escalation ladders, dead man's switches and alert
sources as Kubernetes objects. It hands the server a generated key through TERDUT_OPERATOR_KEY
(see Configuration and SERVICE-ACCOUNTS.md), and
the server then treats configuration as operator-managed (TERDUT_OPERATOR_MODE), refusing edits
made by hand in the web UI. Use the Helm chart above for a plain install, the operator when you
want that configuration in gitops.