Deployment and Operations
Vuoro runs as a Docker Compose stack (API, web, docs, Postgres, Redis, backup sidecar) behind a Caddy reverse proxy on a single host in Helsinki, Finland. Primary data and off-site backups both stay in Finland.
CI Pipeline
Every push to main runs the GitHub Actions pipeline on a self-hosted runner:
- Install with Node 22 and pnpm 11 (frozen lockfile).
- Lint (including the i18n key-parity check), typecheck, and security gates. The hard gate fails only on vulnerabilities in the production dependency closure. Advisory scanning, secret scanning and SAST run alongside it.
- Tests: API unit tests, API E2E against a real Postgres, and web unit tests, followed by Playwright web E2E.
- Docker build: the API, web and docs images are built once in CI, using the production
NEXT_PUBLIC_*build arguments, and taggedvuoron-<service>:<short-sha>. The docs site build is also verified. - Deploy (below), followed by a notification.
Hotfix fast path. If the commit subject contains [hotfix], the unit tests and E2E stages are skipped. Lint, typecheck, security gates and the image build still run. Use it only for small fixes that have already been validated locally.
Work lands on main directly.
Deploys
The production host never builds images. The deploy job:
- Verifies that the CI-built images for the commit exist.
- Ships them to the host with
docker save | gzip | ssh docker load, before touching the running stack. - Tags the current images for rollback and takes a short-lived pre-deploy database snapshot. The deploy aborts if the snapshot fails.
- Runs database migrations and RLS policies on the host, before any new containers start.
- Retags the shipped images as
:latestand runsdocker compose up -d --no-build. - Waits for
GET /api/health/ready, then checks thatversion.shafromGET /api/healthmatches the shipped commit. If either check fails, the previous images are restored automatically. Rollback swaps images and does not runcompose down.
A deploy takes about a minute. Building on the host was retired because the Next.js build could exhaust the production host's memory. There is no container registry: images travel directly from the runner to the host.
Backups and Disaster Recovery
| What | How |
|---|---|
| PostgreSQL | Daily dump from the backup sidecar, kept locally and pushed off-site with restic (encrypted) to a Hetzner Storage Box. 30 daily snapshots are retained. |
.env secrets | Captured daily into the same restic repository under a separate config tag, as a safety net alongside the password-manager vault entry. |
| Code and images | Code lives in git. Images can be rebuilt from git during a recovery. |
| Redis | Not backed up. Queue state is rebuilt from Postgres by the schedulers. |
Monitoring:
GET /api/health/backupreturns503if no backup has succeeded in the last 26 hours. Point external uptime monitoring at it.- A weekly backup recovery test workflow restores the latest local dump, the off-site dump and the off-site
configsnapshot into a throwaway Postgres instance, and verifies them. It never touches the live database.
The complete restore procedure is in the repository's docs/disaster-recovery.md runbook. It covers provisioning a fresh host in the same region, restoring secrets and the database, applying RLS and migrations, smoke testing and cutting DNS over, along with a quarterly drill checklist. The targets are RTO 4 hours and RPO 24 hours.
Configuration
The API validates its environment at boot, and a missing required variable stops startup. Required variables are DATABASE_URL (non-superuser application role), DATABASE_ADMIN_URL (admin role for migrations and the few cross-tenant paths), JWT_SECRET and JWT_REFRESH_SECRET. Optional integrations are only switched on when their variables are set:
| Feature | Variables |
|---|---|
| BI outbound feed | BI_API_TOKEN, BI_API_TENANT_ID |
| BI demand ingest | BI_INGEST_GCS_BUCKET, GOOGLE_APPLICATION_CREDENTIALS, BI_INGEST_TENANT_ID |
| AI roster drafting | ROSTER_DRAFT_ENABLED, ROSTER_DRAFT_LLM_API_KEY, ROSTER_DRAFT_LLM_MODEL |
| Public usage stats | PUBLIC_STATS_TOKEN |
BREVO_API_KEY (plus EMAIL_OVERRIDE_RECIPIENT for non-production) | |
| Error tracking | SENTRY_DSN |
| Refresh-token grace window | REFRESH_ROTATION_GRACE_SECONDS (default 60; 0 means strict rotation) |
| Backup freshness | BACKUP_FRESHNESS_PATH |
Optional variables that Compose passes through as empty strings must accept '' in the validation schema. Otherwise an unset optional variable prevents the API from booting.
Background Jobs
Scheduled work runs as BullMQ repeatable jobs in the API process. Jobs that touch tenant data run once per tenant within that tenant's RLS context. Examples:
- Daily BI demand ingest (06:00 Helsinki)
- Shift-offer expiry reaper
- Monthly TES experience accrual with a rolling re-evaluation window
- Scheduled payroll reconciliation sweeps that alert on differences between source data and stored payroll results
- Certification expiry reminders
- Hourly open clock-in warnings (07–20 Helsinki). Supervisors are notified once about any clock-in left open for more than 14 hours.