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¶
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¶
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¶
{
"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:
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.