This guide covers production deployment with Cloudflare Full SSL: the public dashboard and API are served over HTTPS via Cloudflare, while nginx on the origin listens on port 80 only (no origin TLS certificate required).
Mail (MX/SMTP) bypasses Cloudflare and uses a grey-cloud hostname or direct IP.
| Traffic | Hostname | Cloudflare | Origin port |
|---|---|---|---|
| Dashboard + API | app.example.com | Proxied (orange) | 80 → nginx |
| MX / SMTP | mail.example.com or VPS IP | DNS only (grey) | 25 / 2525 / 587 |
| Grafana / Prometheus | — | Not public | localhost only |
sudo bash install/install.sh \
--public-hostname app.example.com \
--mail-host mail.example.com
app.example.com pointing to your VPS IP. Enable Proxied (orange cloud).mail.example.com pointing to the same IP. Set DNS only (grey cloud). Customer MX records should point to mail.example.com.Bypass cache for dynamic paths:
/api/*/admin/*/dashboard/*Use a Cache Rule: If URI Path starts with /api → Bypass cache.
Add a rate limit rule on the backend login endpoint /api/v1/auth/login (e.g. 10 requests per minute per IP).
Note: nginx proxies only
/api/v1/*(plus/api/docs,/health,/metrics,/r/) to the backend API. NextAuth routes under/api/auth/*(sign-in callback, session, CSRF) are served by the web app and must not be routed to the backend.
Sign-in with Google, Entra ID or a generic OIDC provider needs DEPLOYMENT_MODE=cloudflare
and OAUTH_LOGIN_ENABLED=true. These providers cover login only — mail always arrives
over SMTP on the MX.
https://app.example.com/api/auth/callback/googlehttps://app.example.com/api/auth/callback/microsoft-entra-idhttps://app.example.com/api/auth/callback/oidc| Port | Purpose |
|---|---|
| 80 | HTTP from Cloudflare to nginx |
| 25 | MX (optional, --mx-port-25) |
| 2525 | SMTP ingest |
| 587 | Outbound SMTP STARTTLS |
Do not expose 3000, 3001, 9090, or 3002 publicly. Grafana and Prometheus bind to 127.0.0.1 in docker-compose.prod.yml.
From your workstation:
ssh -L 3002:127.0.0.1:3002 -L 9090:127.0.0.1:9090 user@your-vps
Then open http://localhost:3002 (Grafana) or http://localhost:9090 (Prometheus).
Alternatively use Cloudflare Access or a VPN for ops traffic.
Key production settings (set by installer with --public-hostname):
DEPLOYMENT_MODE=cloudflare
PUBLIC_URL=https://app.example.com
MAIL_HOST=mail.example.com
CLOUDFLARE_PROXY=true
WEB_URL=https://app.example.com
NEXTAUTH_URL=https://app.example.com
API_BASE_URL=https://app.example.com/api/v1
NEXT_PUBLIC_API_URL=https://app.example.com/api/v1
SCANNER_FAILURE_MODE=quarantine
After DNS propagates:
curl -sf https://app.example.com/api/health
| Error | Cause | Fix |
|---|---|---|
| 525 SSL handshake failed | Origin not listening on 80 | Check nginx and docker compose ps |
| 526 Invalid SSL cert | Cloudflare set to Full (strict) without origin cert | Use Full not Full (strict), or add origin cert |
| OAuth redirect mismatch | Wrong callback URL | Match provider console to https://PUBLIC_HOSTNAME/api/auth/callback/... |
| Mail not arriving | MX proxied through Cloudflare | Grey-cloud mail. record; MX must not be orange-cloud |
The installer schedules a nightly encrypted backup in /etc/cron.d/coldfeet-backup.
Check the last run with cat backup_data/latest.json, and see
BACKUP-RESTORE.md for restore steps and the drill.
After install you can change platform hostnames without editing SSH files manually:
/admin/settings)..env.production, regenerates nginx snippet, and restarts API/web (when PLATFORM_APPLY_EXEC=true on the API host, or run install/apply-platform.sh).Customer domains (per tenant) stay in the database; only platform MX copy in the domain wizard updates to the new MAIL_HOST.
For origin TLS with Cloudflare Full (strict), see DEPLOY-TLS-ORIGIN.md.