Skip to content

Deployment

Reference stack

The reference production deployment runs on a single Linux host (bosun — HPE ML350 Gen10) using Docker Compose with Caddy as the reverse proxy. Services are separated by compose project:

/home/sauer/bosun/compose/
├── saebooks-sauer/    # saebooks-api + saebooks-web + postgres
│   ├── docker-compose.yml
│   └── .env           # secrets, not committed
├── discourse/         # discourse.saebooks.com.au
├── edge-proxy/        # Caddy reverse proxy + ACME
└── ...

The Postgres data volume is a named volume (db_data) within the compose project. Mail outbox files land at /opt/data/saebooks-sauer/mail-outbox on the host.

Caddy routing

The web container is reachable from Caddy over the shared revproxy Docker network (compose service name web, internal port 8080). The API container (api, internal port 8000) is never exposed publicly — the web process reaches it via the internal compose network at http://api:8000. A gRPC-Web proxy (grpcwebproxy) is also present for the desktop client.

Environment variables

saebooks-api

Variable Required Notes
DATABASE_URL yes Owner-role (BYPASSRLS) URL — used by Alembic migrations and pre-auth lookups (LoginSessionLocal). postgresql+asyncpg://saebooks:…@db:5432/<dbname>
SAEBOOKS_APP_DATABASE_URL yes (prod) Runtime URL for the non-superuser saebooks_app role — required when SAEBOOKS_ENV is not dev/test/ci; see migration 0056_split_db_role.
SAEBOOKS_APP_DB_PASSWORD yes (prod) Password for saebooks_app; also read by migration 0128_app_role.
SAEBOOKS_SECRET_KEY yes HMAC secret for issued JWTs; 32+ random bytes. If unset, a per-process random key is generated (safe for dev/single-process only).
SAEBOOKS_EDITION no community (default), offline, business, pro, enterprise. When set to any value other than community, the licence resolver is bypassed entirely (USB and portal JWT drivers are skipped). Intended for development; a warning is logged on startup.
STRIPE_SECRET_KEY for billing sk_test_… or sk_live_…
STRIPE_WEBHOOK_SECRET for billing whsec_…

saebooks-web

Variable Required Notes
SAEBOOKS_WEB_API_URL yes Base URL of saebooks-api (e.g. http://api:8000)
SAEBOOKS_WEB_SECRET_KEY yes Session cookie signing secret
DISCOURSE_SSO_SECRET for DiscourseConnect Shared secret for DiscourseConnect /auth/discourse/*
SAEBOOKS_WEB_SITE_ORIGIN yes Comma-separated allowed origins for CSRF enforcement
SAE_STAFF_USERNAMES no Comma-separated staff usernames for admin bypass

Build and deploy

The compose files use local build contexts (no registry). To deploy a code change:

# On bosun
cd /home/sauer/bosun/compose/saebooks-sauer
sudo docker compose build web
sudo docker compose up -d web

To update the API (includes Alembic upgrade):

sudo docker compose build api
sudo docker compose up -d api
# Alembic upgrade head runs on container start via entrypoint.sh

Health probes

Path Service Purpose
/healthz api, web Liveness — always 200 while the process is up (no DB round-trip)
/api/v1/healthz api Canonical liveness path for external probes
/api/v1/version api Edition + package version (unauthenticated)
/api/v1/docs api Swagger UI (restrict in prod)

Note

A /readyz readiness probe has not yet been implemented. The /healthz liveness probe intentionally skips the DB round-trip.

Log access

sudo docker logs saebooks-sauer-web-1 --since 30m
sudo docker logs saebooks-sauer-api-1 --since 30m -f

The web process tags logs saebooks_web; the API tags saebooks.

Backup

Nightly pg_dump of the database via a cron job or make backup (which runs scripts/backup.sh). The Postgres data volume is the named volume db_data within the compose project.