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 create via cron.
  • Offsite ship via rclone/rsync to 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.