Account & operations
Self-hosting with Docker
Run the whole platform — Next.js, Rust API and MongoDB — with Docker Compose.
What runs
| Service | Image | Purpose |
|---|---|---|
caddy | caddy | HTTPS (automatic certificates), routes /api to the API and everything else to the web app |
web | built from frontend/ | Next.js marketing site, dashboards, admin console, hosted forms |
api | built from backend/ | Rust REST API |
mongo | mongo:8 | Database (and GridFS for files) |
clamav | clamav/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
- Create an application at clerk.com.
- Copy the publishable key and secret key.
- In Configure → Sessions → Customize session token, add:
{ "email": "{{user.primary_email_address}}", "name": "{{user.full_name}}" } - Enable Billing, create a free plan and a plan with the slug
pro(or setCLERK_PRO_PLAN_SLUGS). - Add a webhook endpoint
https://your-domain/api/v1/hooks/clerkforuser.*,subscription.*andsubscriptionItem.*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) andGET /readyz(database). - Logs: JSON logs with
LOG_FORMAT=json. - Backups: back up the
mongo-datavolume (mongodump) and your.env. - Key rotation: set the new key as
DATA_ENCRYPTION_KEYand the old one asDATA_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_SECRETandDATA_ENCRYPTION_KEY -
CLERK_WEBHOOK_SECRETset -
ADMIN_EMAILSset 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