Managed Postgres / Redis / S3
The in-compose Postgres / Redis / MinIO are fine for a one-box deploy or a small operator. When you outgrow them (or want managed backups, HA failover, regional replication), Scani makes no assumption about where its dependencies live. Comment out the compose service and update the corresponding env vars.
Postgres
Section titled “Postgres”Any Postgres 16+ instance. The schema works with any vanilla distribution.
| Provider | Notes |
|---|---|
| Neon | Serverless. Set ?sslmode=require and POSTGRES_POOL_MAX=5 if the URL includes ?pgbouncer=true (Neon’s default endpoint pools through PgBouncer). |
| Render | Standard. ?sslmode=require. |
| AWS RDS / Aurora | Standard. ?sslmode=require. |
| Supabase | Use the direct connection string for migrations, the pooled one for runtime (or set POSTGRES_POOL_MAX=5). |
| Self-hosted | Anything Postgres 16+. |
DATABASE_URL=postgres://user:pass@host:5432/scani?sslmode=requirePOSTGRES_POOL_MAX=5 # only if behind a connection poolerThen comment out the postgres service in docker-compose.prod.yml:
# services:# postgres:# image: postgres:16-alpine# ...Migrations on a managed Postgres
Section titled “Migrations on a managed Postgres”Apply migrations explicitly before each deploy. The
scani/migrate image is a
pre-built one-shot that wraps the Drizzle runner — no workspace
clone, no Bun install on your side.
docker run --rm \ -e DATABASE_URL="$DATABASE_URL" \ scani/migrate:${SCANI_IMAGE_TAG:-latest}For a Kubernetes deploy, wrap it in a Job that runs before the api
Deployment rolls out:
apiVersion: batch/v1kind: Jobmetadata: name: scani-migrate-{{ .Values.image.tag }}spec: template: spec: restartPolicy: Never containers: - name: migrate image: scani/migrate:{{ .Values.image.tag }} env: - name: DATABASE_URL valueFrom: { secretKeyRef: { name: scani, key: DATABASE_URL } }For a CI-based deploy, run it as a step before the api/worker rollout. The runner is idempotent — already-applied migrations are skipped — so re-running on every deploy is safe and cheap.
Always pin the same tag you’re using for your other scani/*
images. Mixing scani/migrate:1.2.0 with scani/api:1.3.0 is
unsupported.
See Production with docker-compose → Apply migrations
for the compose-based variant and the “what if you forget” failure
modes (api’s /readyz returns 503, worker restart-loops).
Any Redis 7+ instance. Cluster mode is supported via standard node-redis behaviour.
| Provider | Notes |
|---|---|
| Upstash | TLS endpoint. rediss:// URL. |
| Redis Cloud | TLS endpoint. |
| AWS ElastiCache | In-VPC. Standard redis:// URL. |
| Self-hosted | Anything Redis 7+ with AOF persistence. |
REDIS_URL=rediss://default:pass@host:6379Then comment out redis in docker-compose.prod.yml.
S3-compatible storage
Section titled “S3-compatible storage”Any S3-compatible store works.
| Provider | Notes |
|---|---|
| Cloudflare R2 | No egress fees. S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com. |
| AWS S3 | Region-specific endpoint. |
| Backblaze B2 | S3_ENDPOINT=https://s3.<region>.backblazeb2.com. |
| MinIO (self-hosted, scaled out) | Same as compose-managed, just point at a remote instance. |
S3_ENDPOINT=https://<endpoint>S3_PUBLIC_ENDPOINT=https://<public-endpoint> # often same as aboveS3_ACCESS_KEY_ID=<key>S3_SECRET_ACCESS_KEY=<secret>S3_BUCKET=scani-uploadsThen comment out minio and minio-init. Create the bucket
yourself before first boot — there’s no init container for managed
providers.
| Provider | Variables |
|---|---|
| Any SMTP server | SMTP_URL, SMTP_FROM. |
| Fastmail | FASTMAIL_API_TOKEN (JMAP). Takes precedence over SMTP. |
| Postmark / SendGrid / Mailgun | Use their SMTP relay or set up a transactional API. SMTP is the simplest path. |
The data-provider is the only service that sends email. In Tier 1
that’s your container; in Tier 2/3 the hosted data-provider handles
it (the user-side .env doesn’t need email config).
Object storage public endpoint quirk
Section titled “Object storage public endpoint quirk”S3_PUBLIC_ENDPOINT is what gets baked into presigned URLs the
browser uses. For compose-managed MinIO, S3_ENDPOINT is
http://minio:9000 (server-to-server) and S3_PUBLIC_ENDPOINT is
http://localhost:9000 (the browser can’t resolve minio). For
most cloud providers both URLs are the same.
Code does not change
Section titled “Code does not change”No code change is required to use any of these. The schema doesn’t care. The application doesn’t care. The compose file is just one opinionated way to wire things together.