Health checks
Every container can tell your host whether it is working, so a broken release never takes over and a stuck one is restarted.
What each container answers
Section titled “What each container answers”| Container | Path | Healthy when |
|---|---|---|
| api | /api/health |
200; 503 if Postgres or Redis is down |
| web, site | /healthz |
200, answered by Caddy without the API |
| worker | none | It has no address. Restart it on exit |
On Railway these are already set, and production:up on your machine
waits for them too. On another host, point its health check at the
paths above.
What the API says
Section titled “What the API says”/api/health needs no session. It runs select 1 against the database and pings Redis on every
call:
{ "status": "ok", "version": "1.0.0", "uptimeSeconds": 4211.3, "checks": { "database": "up", "redis": "up" }, "timestamp": "2026-10-01T09:12:44.120Z"}When the database or Redis cannot be reached, status is degraded, its check is down, the
answer is 503, and the reason is logged as an error. The shape is healthSchema in
packages/contracts/src/health.ts, which the web app’s status card, the mobile app and
deploy:check all read.
To check something else, such as a service your product cannot work without, add it to
checks in the schema and to HealthService in apps/api/src/health. Keep it fast: hosts ask
every few seconds, which is also why these requests are left out of the logs.
Watch it from outside
Section titled “Watch it from outside”Point an uptime monitor at https://<your web address>/api/health and alert on anything but
200. It goes through Caddy to the API to the database, so one address covers all three.
After a release, or when something seems off, check everything a visitor depends on:
bun run deploy:check https://app.example.com --site https://example.comSee Check a deployment for what it looks at.
Stopping and restarting
Section titled “Stopping and restarting”When your host stops a container, the API finishes what it is doing first: the worker takes no new jobs and waits for the ones in progress, then the database connections are closed. A job that was cut off anyway is picked up again after 10 minutes. See Background jobs.
On Railway a service that exits is restarted, up to 5 times in a row.