Production deployment¶
The service is intentionally single-container: FastAPI serves the API and public pages, SQLite stores relational data, and LMDB stores short-lived OAuth/session state. Run one application worker against one persistent data volume.
Checklist¶
- Set
APP_ENV=production. - Inject
JWT_SECRET_KEYandTOKEN_ENCRYPTION_KEYfrom a secrets manager; never bake them into an image or commit an environment file. - Configure exact public URLs and
CORS_ALLOWED_ORIGINS. - Terminate TLS at the platform or reverse proxy.
- Mount
/app/dataon encrypted, backed-up persistent storage. LMDB contains refresh-session records and other short-lived authentication artifacts. - Keep exactly one application replica and one Uvicorn worker while using embedded storage, and prevent old/new revision overlap during deployment. The durable webhook queue is resumed by that single worker after restarts.
- Run
alembic upgrade headonce before application startup. The image does this automatically, but platforms that can start multiple containers must serialize that migration step or disable overlapping revisions. - Configure only the OAuth providers you operate and register each exact callback.
- Scan and smoke-test the built image before deployment.
Container¶
docker build --target runtime -t subconscious-api:latest .
docker run --rm -p 8000:8000 \
--env-file /etc/subconscious/prod.env \
-v subconscious-data:/app/data \
subconscious-api:latest
The image runs as an unprivileged user. Check /health for liveness and
/health/ready for embedded-store readiness.
Azure Container Apps¶
The checked-in workflow deploys an immutable image to the existing subconscious-prod
container app and polls /health/ready before reporting success; it does not provision
or verify the app's persistent runtime configuration. GitHub Actions are pinned to
reviewed commit SHAs. Before enabling automatic production deployment, verify in Azure
that:
/app/datais an encrypted persistent volume mounted read/write by UID 1000;- minimum and maximum replicas are both one and revision overlap is disabled;
- ingress targets port 8000, probes use
/healthand/health/ready, andTRUSTED_HOSTSincludes every public/probe Host header; - JWT, Fernet, OAuth, and SMTP values come from Container Apps secrets;
- the app can pull the private GHCR image; and
DATABASE_URLis SQLite as packaged. PostgreSQL requires adding and testing an async PostgreSQL driver before deployment.
The application does not trust arbitrary forwarded client-IP headers. Configure rate limiting at the trusted Azure ingress as well as in the application, rather than enabling broad proxy trust. A failed readiness check intentionally does not perform an automatic image rollback: an older image may be incompatible after a forward database migration. Retain backups and use an operator-reviewed rollback or forward fix after checking schema compatibility.
Webhook destinations must use HTTPS and resolve only to public IP addresses. Restrict outbound container egress as an additional SSRF defense.
Legacy data checks¶
The baseline migration is intentionally non-destructive. Before upgrading a pre-Alembic database, take a backup and inspect it for case-insensitive duplicate email addresses and OAuth/API-key rows for providers that are no longer enabled. Resolve collisions and revoke obsolete provider credentials deliberately; the migration does not delete user data automatically.
Changing JWT_ISSUER, JWT_AUDIENCE, or the signing key invalidates existing
access tokens. Coordinate that change with native clients, which must implement
the authorization-code plus S256 PKCE callback contract documented here.
Key rotation¶
Set a newly generated Fernet key as TOKEN_ENCRYPTION_KEY and the previous key
as TOKEN_ENCRYPTION_KEY_OLD. Existing connected-provider tokens remain
readable during a staged rotation. Keep the old key until accounts have been
reauthorized; the service does not currently provide bulk re-encryption.