Operate a self-hosted runtime
This page continues Self-host Kindgi: the same containers (kindgi-server, kindgi-db, kindgi-pack), the same kindgi.env and pack.env, and the same pack. Commands that take your API token read it from $KINDGI_API_TOKEN.
Restart the runtime
Section titled “Restart the runtime”The runtime reads its settings when it starts, so most changes on this page take a restart:
docker stop --time 30 kindgi-serverdocker rm kindgi-serverdocker run -d --name kindgi-server --network kindgi \ --add-host registry.localhost:host-gateway \ -p 127.0.0.1:4000:4000 --env-file kindgi.env \ quay.io/kindgi/runtime:0.1.2On a stop, the runtime stops taking requests and gives the runs it's executing up to 7 seconds to finish, then exits with code 0. --time 30 gives it that time before Docker kills it.
Check health and logs
Section titled “Check health and logs”curl -s http://localhost:4000/health{"ok":true}/health says the process is up. /ready says its database answers too, within two seconds. Neither needs a token:
curl -s http://localhost:4000/ready{"ok":true,"database":"ok"}While Postgres is unreachable, /ready answers 503 with {"ok":false,"database":"unreachable"}, and /health still answers {"ok":true}. Use /ready for a load balancer's or platform's readiness check. The image's own health check uses it, so docker ps shows the runtime's state:
docker ps --filter name=kindgi-server --format 'table {{.Names}}\t{{.Status}}'NAMES STATUSkindgi-server Up 34 seconds (healthy)docker ps shows (unhealthy) while the database is down. A route that reads the database answers 500 and names the cause. With Postgres unreachable, GET /v1/deployments answers:
curl -s http://localhost:4000/v1/deployments -H "authorization: Bearer $KINDGI_API_TOKEN"{"error":{"code":"internal-server-error","message":"Deployment list failed: deployments: Tenant-scoped query failed: getaddrinfo ENOTFOUND kindgi-db","requestId":"req-…"}}The startup log
Section titled “The startup log”The runtime prints what it's running with when it starts (docker logs kindgi-server). The lines to check after a change:
Token: kgi_bt_…65bb (provided) … Public run tokens: off (no signing key) License: Docs example · non-production · until 2026-11-02 ⚠ The license key expires in 29 days (2026-11-02). Renew it: contact@kindgi.com. Env: production (tool secrets resolve in it) Tenant host access: deployed (stdio MCP endpoints refused; KINDGI_TENANT_HOST_ACCESS) Pack service: http://kindgi-pack:8080 — acme-pack (artifact 20261003.1), protocol 2, 3 tools, 1 checkToken: the last four characters of the API token it accepts.License: who the key is for, its kind and its end date. A warning follows within 30 days of the end.Pack service: the pack it reached, with its artifact version. When it can't reach it, a warning takes this line's place, with the reason.
After a crash
Section titled “After a crash”When the runtime comes back after stopping without a shutdown, its sweep repairs the runs the stop left mid-way (If the runtime stops). Each repair is a line in its log:
[run-leases] resuming run <run id> (tenant <tenant id>): its wait resolved and nothing resumed it[run-leases] woke the flow run waiting on child run <run id> …: the child had moved on[run-leases] ended run <run id> …: its parent run had endedWhere errors show
Section titled “Where errors show”-
A setting the runtime refuses (a missing license key, for example): it exits with code 2, and its log says what to fix.
-
A database it can't reach while starting: it exits with code 1, and its log says which database and why, never the password. Here Postgres wasn't running:
Terminal window docker inspect --format '{{.State.ExitCode}}' kindgi-serverdocker logs kindgi-server1Can't connect to the database at kindgi-db:5432/kindgi: getaddrinfo ENOTFOUND kindgi-db. Check KINDGI_DATABASE_URL, and that Postgres is up and reachable from here. -
Another failure while starting: it exits with code 1, and the log's first line starts with
kindgi-runtime: fatal:and ends with the cause. -
A run that fails: its
failureMessage, inpnpm exec kindgi runs get <run id>.
Back up Postgres
Section titled “Back up Postgres”The runtime keeps its state in Postgres: deployments, trusted keys, providers, runs and their journals. Back it up with pg_dump, while the runtime runs:
docker exec kindgi-db pg_dump -U kindgi -Fc kindgi > kindgi-backup.dump-Fc is pg_dump's custom format, which pg_restore reads. The dump holds your runs' inputs and outputs: store it like the database.
Restore into a fresh database
Section titled “Restore into a fresh database”Start a new Postgres, and restore the backup into it once it accepts connections:
docker run -d --name kindgi-db-restored --network kindgi \ -e POSTGRES_USER=kindgi -e POSTGRES_PASSWORD="$DB_PASSWORD" -e POSTGRES_DB=kindgi \ -v kindgi-db-restored:/var/lib/postgresql/data \ pgvector/pgvector:pg16
docker exec kindgi-db-restored pg_isready -U kindgi -d kindgidocker exec -i kindgi-db-restored pg_restore -U kindgi -d kindgi --no-privileges < kindgi-backup.dump--no-privileges leaves out the backup's grants. The runtime grants its database role what it needs every time it starts, and a fresh server doesn't have that role yet.
Point the runtime at the new database in kindgi.env:
KINDGI_DATABASE_URL=postgres://kindgi:<DB_PASSWORD>@kindgi-db-restored:5432/kindgiThen restart the runtime. A run from before the backup is there:
pnpm exec kindgi runs get <run id> --url http://localhost:4000 --token "$KINDGI_API_TOKEN" "status": "completed", … "output": { "reply": "Hello, Ada! It's great to meet you. How can I assist you today?", "greeting": "Hello, Ada!" }Upgrade the runtime
Section titled “Upgrade the runtime”-
Pull the new version, and restart with it, with the same
kindgi.env:Terminal window docker pull quay.io/kindgi/runtime:<version>docker stop --time 30 kindgi-serverdocker rm kindgi-serverdocker run -d --name kindgi-server --network kindgi \--add-host registry.localhost:host-gateway \-p 127.0.0.1:4000:4000 --env-file kindgi.env \quay.io/kindgi/runtime:<version>
When it starts, the runtime brings the database up to date: it applies the migrations the database doesn't have yet, then serves. On a database that has them all, it applies nothing, so a restart on the same version changes nothing. The log doesn't list them: once the Kindgi API server listening lines appear, they're done. If one fails, the runtime exits with code 1, and its log says kindgi-runtime: fatal: Error: migration failed for … and why.
Migrations only go forward, and an older runtime isn't guaranteed to work on a database a newer one migrated. To go back, restore the backup you took before the upgrade, and run the older version on it.
Rotate the API token
Section titled “Rotate the API token”Make a new token, replace KINDGI_API_TOKEN in kindgi.env with it, and restart:
printf 'kgi_bt_%s\n' "$(openssl rand -hex 32)"The Token line in the log now shows the new token's last four characters, and the old token is refused:
{"error":{"code":"auth-missing","message":"Bearer token is not recognized","requestId":"req-…"}}KINDGI_API_TOKEN is one token: switch your CLI and apps to the new one when you restart.
Rotate the pack service token
Section titled “Rotate the pack service token”The runtime and your pack's service share this token. Make a new one:
openssl rand -base64 32Put it in both files, KINDGI_PACK_SERVICE_TOKEN in pack.env and in kindgi.env. Then restart the pack's service with the same image:
docker stop --time 30 kindgi-packdocker rm kindgi-packdocker run -d --name kindgi-pack --network kindgi --env-file pack.env \ registry.localhost:5050/acme-pack@sha256:<your pack's digest>And restart the runtime. While the two tokens differ, the pack's service refuses the runtime's calls, and your tools fail. The runtime's log says so:
⚠ Pack service at http://kindgi-pack:8080 isn't answering (pack-service-unauthorized: The pack service rejected the pack token). The server is up; pack tools and checks fail until it answers.Once both sides have the same token, the runtime's calls go through again, with no further restart.
Rotate the license key
Section titled “Rotate the license key”Replace KINDGI_LICENSE_KEY in kindgi.env with the new key, restart, and check the License line in the log:
License: Docs example · non-production · until 2026-11-02Rotate a signing key
Section titled “Rotate a signing key”Rotate under a new key id. A key id stays bound to its public key, and a revoked id can't be trusted again.
-
Create a key, and trust it:
Terminal window pnpm exec kindgi key create acme-selfhost-2 --env selfhostpnpm exec kindgi key trust acme-selfhost-2 --url http://localhost:4000 --token "$KINDGI_API_TOKEN" -
Sign your next release with it. In the
selfhostblock ofkindgi.config.ts:signingKey: '~/.kindgi/keys/acme-selfhost-2.pem',signerKeyId: 'acme-selfhost-2',Then build and deploy as usual, and run the new image as your pack's service (step 4). The first line keeps the old key's envelope, to check the revocation below:
Terminal window cp .kindgi/build/deploy-envelope.json old-envelope.jsonpnpm exec kindgi build --local --push --env selfhost --artifact-version 20261003.2pnpm exec kindgi deploy --env selfhost --token "$KINDGI_API_TOKEN"Registering deployment✓ POST /v1/deployments → 201 CreateddeploymentId: 2a4677f2-5845-4e05-8630-5f0d01972331artifactVersion: 20261003.2The artifact version defaults to today's date with
.1; this example's second release of the day is.2. -
Revoke the old key:
Terminal window pnpm exec kindgi key revoke acme-selfhost --reason "rotated to acme-selfhost-2" \--url http://localhost:4000 --token "$KINDGI_API_TOKEN"✓ Revoked acme-selfhostA revoked id can't be trusted again:
kindgi key trustsays so, and how to trust another key.
From then on, the runtime refuses a deploy signed by the old key. Here, an envelope it signed earlier:
pnpm exec kindgi deploy --env selfhost --from-envelope old-envelope.json \ --idempotency-key redeploy-old --token "$KINDGI_API_TOKEN" Registering deployment ✗ POST /v1/deployments → HTTP 403 code: signer-not-trusted message: Signer key "acme-selfhost" is not on this tenant's trust listWithout --idempotency-key, the CLI's key is a hash of the envelope. Sending an envelope you already deployed then returns the first answer again (for 24 hours), and deploys nothing.
The runtime checks signatures when you deploy. A deployment the revoked key signed keeps running, through restarts too. The revoked key stays listed for audit:
curl -s "http://localhost:4000/v1/signing-keys?includeRevoked=true" -H "authorization: Bearer $KINDGI_API_TOKEN"Each key in the list has its revokedAt and revokedReason, if it was revoked.
Rotate the public run token key
Section titled “Rotate the public run token key”If browsers follow runs, the runtime signs their public run tokens with a key of its own. Make one:
openssl genpkey -algorithm ed25519 -out public-token-signing.pemchmod 600 public-token-signing.pemGive it to the runtime as a file: add this line to kindgi.env, and restart with the file mounted:
KINDGI_PUBLIC_TOKEN_SIGNING_KEY_PATH=/etc/kindgi/public-token-signing.pemdocker run -d --name kindgi-server --network kindgi \ --add-host registry.localhost:host-gateway \ -v "$PWD/public-token-signing.pem:/etc/kindgi/public-token-signing.pem:ro" \ -p 127.0.0.1:4000:4000 --env-file kindgi.env \ quay.io/kindgi/runtime:0.1.2The file must have mode 0600, and the runtime's user in the container (uid 10001) must be able to read it. The log says:
Public run tokens: on (browsers follow runs; no CORS origins)To rotate it, replace the file with a new key (the same two commands), and restart with the same mount. Tokens signed with the old key are refused from then on:
{"error":{"code":"auth-missing","message":"Bearer token is not recognized","requestId":"req-…"}}Tokens minted after the restart work as before.