Production hardening
./install.sh produces a working dev install. Before exposing it to
real users, work through this checklist.
1. Move behind a reverse proxy with TLS
The bundled stack listens HTTP on :3000 (web) and :8000 (Kong /
Supabase API). For production you want one front-door, TLS-terminated.
Caddy is the lowest-friction choice — it provisions Let's Encrypt automatically.
# /etc/caddy/Caddyfile
your-domain.com {
reverse_proxy localhost:3000
}
api.your-domain.com {
reverse_proxy localhost:8000
}
After Caddy is up:
# In ./ccd/.env
SITE_URL=https://your-domain.com
NEXT_PUBLIC_SUPABASE_URL=https://api.your-domain.com
SUPABASE_PUBLIC_URL=https://api.your-domain.com
docker compose up -d --force-recreate web auth
GoTrue uses SUPABASE_PUBLIC_URL to build email-confirmation links,
so it MUST match the browser-reachable URL — Caddy's external
hostname, not localhost:8000.
2. Lock down sign-up
After creating your admin account:
DISABLE_SIGNUP=true
MAILER_AUTOCONFIRM=false
docker compose up -d --force-recreate auth
New accounts now require an explicit invite (via the admin portal in cloud edition, or via SQL in self-host).
3. Wire SMTP
GoTrue handles password reset + email confirm + magic-link delivery. Without SMTP, none of those flows complete. Any standard provider (SendGrid, Mailgun, Resend, AWS SES) works.
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASS=re_xxxxxxxxxxxxxxxx
SMTP_SENDER_NAME=Your Org
SMTP_ADMIN_EMAIL=admin@your-domain.com
docker compose up -d --force-recreate auth
Test: trigger a password reset from the login page. The mail should arrive within a minute.
4. Lock down the Postgres host binding
By default Postgres binds to 127.0.0.1:5432 for operator psql
convenience. Production should not expose Postgres to the host
network — remove this line in docker-compose.yml:
db:
ports:
- '127.0.0.1:${POSTGRES_PORT:-5432}:5432' # ← delete or comment out
Postgres stays reachable from inside the compose network (auth/rest/
realtime/storage/worker still work). For ad-hoc psql, use:
docker compose exec db psql -U postgres
5. Rotate secrets
./install.sh generates initial secrets on first run. Rotate
periodically — at minimum after any team-member offboarding.
# Generate a fresh secret set without disrupting the existing keys
./ccd keys regenerate
# Pipe a single line into the right .env update — `sed -i` differs
# across platforms; tmp+mv is portable:
sed 's|^JWT_SECRET=.*|JWT_SECRET=<new-value>|' .env > .env.tmp && mv .env.tmp .env
Rotating JWT_SECRET invalidates every issued user session. All
users get logged out and have to sign back in.
SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY are JWTs signed
with JWT_SECRET — they must be regenerated together. The
./ccd keys regenerate command handles this.
After rotating, recreate every service that reads the affected vars:
docker compose up -d --force-recreate
6. Bound resource usage
Default compose has no memory limits. On a shared host, add per- service limits to prevent Ollama OOM from killing the data plane:
ollama:
deploy:
resources:
limits:
memory: 12G
(Use deploy.resources even outside swarm — modern compose honors
it.)
7. Backup cadence
See Backup + restore. Minimum acceptable:
- Daily
./ccd backup createvia cron. - Offsite ship via
rclone/rsyncto a different provider. - Quarterly restore drill into a staging copy.
8. Wire observability
Observability — the Grafana profile gives you visibility into agent latency, queue depth, and container resource use. Required for any install you depend on, optional for an evaluation.
9. Subscribe to release notifications
./ccd upgrade is the safe upgrade path, but you have to know a new
release exists. Follow the
changelog or subscribe to the
release mailing list from your account for email notifications.