Self-Hosting
Production Checklist
A complete checklist to verify before going live with a self-hosted HelloJohn instance.
Production Checklist
Review every item before sending real users to your HelloJohn instance.
Infrastructure
- PostgreSQL is version 14 or later
- Postgres runs on a separate host from HelloJohn (not in the same container without persistent volumes)
- Database has automated backups — daily snapshots or continuous WAL archiving
- Redis is configured for rate limiting and session caching (recommended for production)
- Persistent volumes are mounted for any local storage (avatars, etc.)
- Reverse proxy (nginx/Caddy/Traefik) handles TLS termination
- TLS certificate is valid and auto-renewing (Let's Encrypt / ACM)
- Port 3000 is not publicly exposed — only the reverse proxy port (443) is open
- Firewall rules restrict access to PostgreSQL and Redis from HelloJohn only
Security
-
JWT_SECRETis at least 32 random bytes — generated withopenssl rand -hex 32 - Ed25519 key pair used instead of HMAC secret (preferred)
- Secrets are not in version control — use
.envfiles, Docker secrets, or a secrets manager -
SESSION_COOKIE_SECURE=trueis set (requires HTTPS) -
CORS_ALLOWED_ORIGINSis set to your actual domains, not* -
TRUST_PROXY=trueis set with the correctTRUST_PROXY_HOPSvalue - Security headers are set by the reverse proxy (HSTS, X-Frame-Options, etc.)
- HTTPS is enforced — HTTP redirects to HTTPS
- Rate limiting is enabled (
RATE_LIMIT_ENABLED=true) - Admin dashboard path is non-default or dashboard is disabled if not needed
- SMTP credentials are configured and tested
- SPF record is set for your sending domain
- DKIM is configured for your sending domain
- DMARC policy is set to
quarantineorreject - Magic link TTL is appropriate (default 15 minutes)
- Test email delivery — trigger a password reset and verify it arrives
Application
-
APP_URLmatches the exact public URL (no trailing slash, correct protocol) -
ENV=productionis set -
LOG_LEVEL=infoorwarn(notdebugin production — too verbose) - Health endpoint returns 200:
curl https://auth.yourdomain.com/health - First admin account has been created and login has been tested
- Admin password has been changed from the seeded default
- OAuth redirect URIs are configured in each OAuth provider's dashboard
Observability
- Logs are being collected — forwarded to a log aggregator (Datadog, Loki, CloudWatch, etc.)
- Alerts are configured for error rate spikes and latency increases
- Database metrics are monitored (connection count, query latency, cache hit rate)
- Disk space is monitored — PostgreSQL WAL can fill disks quickly
- Uptime monitoring is configured — e.g., Better Uptime, Checkly, or AWS Route 53 health checks
Multi-tenancy
- Default tenant has been created (if applicable)
- Per-tenant OAuth apps are configured (if different OAuth apps per tenant)
- Email domain restrictions are set for tenants that require it
MFA
- TOTP issuer name (
MFA_TOTP_ISSUER) matches your product name - WebAuthn RP ID and origin are set to production values (not localhost)
- Backup codes are enabled for users who enroll MFA
Disaster recovery
- Backup restore has been tested — not just created, but actually restored
- Recovery time objective (RTO) is documented and achievable with current backup strategy
- Runbook exists for common failure scenarios (DB failover, key rotation, etc.)
Final checks
# Health check
curl https://auth.yourdomain.com/health
# TLS certificate validity
echo | openssl s_client -connect auth.yourdomain.com:443 2>/dev/null | \
openssl x509 -noout -dates
# Security headers
curl -sI https://auth.yourdomain.com | grep -E \
'Strict-Transport-Security|X-Frame-Options|X-Content-Type-Options|Referrer-Policy'
# Verify JWT signing works (sign in with a test user)
curl -X POST https://auth.yourdomain.com/v1/auth/sign-in \
-H "Content-Type: application/json" \
-d '{"email":"test@yourdomain.com","password":"testpassword"}'Common mistakes
| Mistake | Impact | Fix |
|---|---|---|
Weak JWT_SECRET | Token forgery risk | Use openssl rand -hex 32 |
APP_URL set to localhost in production | Broken magic links and OAuth redirects | Set to public HTTPS URL |
| No database backups | Data loss on failure | Enable automated backups |
| Port 3000 exposed publicly | Bypasses TLS and proxy security | Bind to 127.0.0.1:3000 |
LOG_LEVEL=debug in production | Sensitive data in logs | Use info or warn |
CORS_ALLOWED_ORIGINS=* | CSRF risk | Set to your actual origins |
| Shared JWT secret across environments | Prod tokens valid in dev | Use separate secrets per environment |