Skip to content

Health checks

The core exposes three signals. Use the right one for the question you are asking.

Endpoint Auth Touches the DB Answers the question
GET /v1/health none no Is the process alive and listening?
GET /v1/install/status none yes Is the schema installed and can the database be reached?
GET /v1/diagnostics bearer yes (tolerates failure) What version is running, what schema does it see, is observability on?

/v1/health

{"status":"ok"}

Always 200 while the process runs, even if the schema failed to install or the database is down. Use it for liveness only.

/v1/install/status

{"status":true,"data":{"state":"installed","version":3}}
state Meaning What to do
installed Schema present; version is its version. The healthy answer. Nothing.
not_installed Empty database. The core installs on its own at startup; seeing this after startup means the install failed. docker logs axium-core \| grep '\[install\]'. Usually a permissions problem (permission denied for database).
partial Axium schemas exist but no version table. A previous install died halfway. Inspect by hand. On a fresh install, drop the core, llm, rm and stats schemas and restart.
error / no answer Database unreachable. Check DATABASE_URL, network, credentials.

Use it for readiness: a replica that cannot reach the database should not receive traffic. It is cheap, but it is a real query; probe every 10 to 30 s, not every second.

/v1/diagnostics

curl -s http://localhost:3000/v1/diagnostics -H "Authorization: Bearer <token>"
{
  "version": "3.1.0",
  "db_schema": { "state": "installed", "version": 3, "expected": 3 },
  "observability": { "enabled": true, "type": "postgres" }
}

With the database down db_schema becomes {"state":"unreachable","expected":3} and the route still answers 200. Good for a support ticket.

Built into the image

The core image declares its own Docker HEALTHCHECK: every 30 s, 5 s timeout, 3 retries, 40 s start period, a GET /v1/health performed with the embedded runtime (no curl or wget in the image). docker ps shows healthy once it passes.

Probes by platform

core:
  healthcheck:
    test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/v1/install/status').then(r => r.json()).then(j => process.exit(j.data.state === 'installed' ? 0 : 1)).catch(() => process.exit(1))"]
    interval: 15s
    timeout: 5s
    start_period: 60s
    retries: 5

The image has no curl or wget; bun -e is the available HTTP client.

livenessProbe:
  httpGet: { path: /v1/health, port: 3000 }
  initialDelaySeconds: 10
  periodSeconds: 15
readinessProbe:
  httpGet: { path: /v1/install/status, port: 3000 }
  initialDelaySeconds: 10
  periodSeconds: 20
  failureThreshold: 3
startupProbe:
  httpGet: { path: /v1/health, port: 3000 }
  periodSeconds: 5
  failureThreshold: 12        # 60 s for the DB wait + install
terminationGracePeriodSeconds: 20

The readiness probe treats any 200 as ready, so a not_installed answer also passes. If you want the state checked, use an exec probe with the bun -e one-liner from the Compose tab.

Cloud Run uses the container's listening port as the startup signal. Add a startup probe on /v1/health if cold starts are slow:

gcloud run services update axium-core --region europe-west1 \
  --startup-probe=httpGet.path=/v1/health,httpGet.port=3000,periodSeconds=5,failureThreshold=12

Alert on GET https://axium-api.your-domain.com/v1/install/status not containing "state":"installed". That single check covers process, network, database and schema.

The ui

No endpoint. A 200 on / means the static server is up. Whether the console can reach the API depends on API_URL and is decided in the user's browser; test it by logging in.