Skip to content

Account & operations

Self-hosting with Docker

Run the whole platform — Next.js, Rust API and MongoDB — with Docker Compose.

What runs

ServiceImagePurpose
caddycaddyHTTPS (automatic certificates), routes /api to the API and everything else to the web app
webbuilt from frontend/Next.js marketing site, dashboards, admin console, hosted forms
apibuilt from backend/Rust REST API
mongomongo:8Database (and GridFS for files)
clamavclamav/clamav (optional)Malware scanning for uploads

MongoDB and the API talk over an internal network with no internet access; only Caddy publishes ports.

1. Configure Clerk

  1. Create an application at clerk.com.
  2. Copy the publishable key and secret key.
  3. In Configure → Sessions → Customize session token, add:
    { "email": "{{user.primary_email_address}}", "name": "{{user.full_name}}" }
    
  4. Enable Billing, create a free plan and a plan with the slug pro (or set CLERK_PRO_PLAN_SLUGS).
  5. Add a webhook endpoint https://your-domain/api/v1/hooks/clerk for user.*, subscription.* and subscriptionItem.* events and copy its signing secret.

2. Create the environment file

cp .env.example .env
openssl rand -base64 48   # → APP_SECRET
openssl rand -base64 32   # → DATA_ENCRYPTION_KEY
openssl rand -hex 24      # → MONGO_PASSWORD

Fill in your domain, Clerk keys (CLERK_ISSUER is the Frontend API URL from the Clerk dashboard) and the generated secrets. Back up DATA_ENCRYPTION_KEY — without it encrypted responses can’t be read.

3. Start

docker compose up -d --build
# with malware scanning (also set CLAMAV_ADDR=clamav:3310 in .env):
docker compose --profile clamav up -d --build

Caddy obtains TLS certificates automatically for SITE_DOMAIN. For local testing use SITE_DOMAIN=localhost.

Operations

  • Health: GET /healthz (process) and GET /readyz (database).
  • Logs: JSON logs with LOG_FORMAT=json.
  • Backups: back up the mongo-data volume (mongodump) and your .env.
  • Key rotation: set the new key as DATA_ENCRYPTION_KEY and the old one as DATA_ENCRYPTION_KEY_PREVIOUS, restart. Old data stays readable; new data uses the new key.
  • Scaling: the API is stateless except for in-memory rate limits; run several replicas behind Caddy and add an edge rate limiter for strict global limits.

Security checklist

  • APP_ENV=production (refuses to start with missing secrets)
  • Strong, unique APP_SECRET and DATA_ENCRYPTION_KEY
  • CLERK_WEBHOOK_SECRET set
  • ADMIN_EMAILS set to your team only, ADMIN_REQUIRE_MFA=true
  • MongoDB not exposed to the internet (the compose file keeps it on the internal network)
  • Turnstile keys set if your forms get spam
  • ClamAV profile enabled if you accept files from the public