Operations
Logs and health
docker compose logs -f app # application
docker compose logs -f worker # background dispatcher
docker compose ps # status + healthcheck
The image defines a healthcheck on GET /login; wire it into your monitor.
Inject request ids/correlation as your log pipeline requires.
Migrations
Migrations live in drizzle/ and run automatically via the one-shot migrate
service on every docker compose up. To apply them explicitly:
docker compose run --rm migrate pnpm db:migrate
Apply migrations before rolling out new app code. Migrations must be backward compatible for the duration of a rolling deploy.
Upgrades
git pull
docker compose build --pull
docker compose run --rm migrate pnpm db:migrate # or rely on `up`
docker compose up -d
ORIGIN is optional and no longer baked, so changing the public URL needs no
rebuild — set ORIGIN/PUBLIC_ORIGIN to pin it, or rely on the request-derived
origin (see Production hardening).
Releases
Tags matching v*.*.* run .github/workflows/publish-image.yml, which builds the
single image and pushes it to GHCR (ghcr.io/<owner>/<repo>) with tags X.Y.Z,
latest, and a sha-… handle. ORIGIN is optional and not baked, so one image
serves any domain; set PUBLIC_ORIGIN only to pin a canonical origin.
To deploy a release, pull the pinned tag, run migrations, then roll the app and worker to the same tag:
docker pull ghcr.io/<owner>/<repo>:X.Y.Z
docker run --rm -e DATABASE_URL='…' ghcr.io/<owner>/<repo>:X.Y.Z pnpm db:migrate
# then run the app/worker from ghcr.io/<owner>/<repo>:X.Y.Z
Backups
The bundled database stores data in the pgdata volume.
# logical dump of the bundled database
docker compose exec -T db pg_dump -U intranet intranet > intranet-$(date +%F).sql
Restore:
cat intranet-2026-10-08.sql | docker compose exec -T db psql -U intranet -d intranet
For managed Postgres, use the provider's automated backups and PITR.
Also export the .env (secrets) alongside backups — the database alone is not a
full restore.
The worker and the outbox
app writes events to a transactional outbox table; the worker dispatches them
(at-least-once). Run exactly one worker per environment unless you have confirmed
idempotent handlers. If the outbox backs up, check docker compose logs worker
and confirm the worker can reach the database and any downstream services.
Seeding
docker compose run --rm migrate pnpm db:seed # idempotent
docker compose run --rm migrate pnpm db:seed -- --reset # clear domain data first
--reset clears domain and credential tables but keeps user, session, and
account rows.
Inspecting the database
docker compose exec db psql -U intranet -d intranet
Clock and timezone
Durations are stored in integer seconds and timestamps in UTC. Run containers on a host with a correct, synchronised clock (NTP); the worker's scheduling depends on it.
Rotation
| Secret | Effect of rotation |
|---|---|
BETTER_AUTH_SECRET | Invalidates sessions — users sign in again. |
OAUTH_CLIENT_SECRET | Recreate the client in Authentik and update .env. |
| API tokens / service clients | Recreate in the admin UI; old secrets stop working. |