Docker Production Deployment
Self-hosting VeriWorkly with the provided Docker Compose configuration.
Docker Production Deployment
The repository ships a compose.yaml that orchestrates four services.
| Service | Image / build | Port | Role |
|---|---|---|---|
web | Built from the root Dockerfile | 3000 | The marketing site (apps/site), built with Next.js standalone output. |
api | Built from apps/server/Dockerfile | 8080 | The Express API. |
redis | redis:7-alpine | internal | Cache, sessions, rate limiting, quotas, and job locks. Persisted to a named volume with AOF enabled. |
redisinsight | redis/redisinsight:latest | 5540 | A browser GUI for inspecting Redis. A debugging convenience, not a runtime dependency. |
Do not expose `redisinsight` publicly
It publishes port 5540 with no authentication in front of it and hands whoever reaches it full
read/write access to Redis — which is where sessions live. Bind it to localhost, put it behind
your reverse proxy's auth, or delete the service from compose.yaml for a production deployment.
What Compose does and does not deploy
The web service builds only the marketing site. Studio, the portfolio builder, the
documentation site, and the blog are not part of this Compose file — deploy those separately
(for example on Vercel, or with your own container images) if you need them.
PostgreSQL is not containerised
Because production-grade persistence, backups, and volume management are deployment-specific,
compose.yaml expects an external PostgreSQL instance (Neon, AWS RDS, a managed VPS, and so
on). Provide its connection string via DATABASE_URL.
Prerequisites
- Docker Engine 24 or higher.
- Docker Compose v2 or higher.
- An external PostgreSQL database and its connection string.
Configuration
Create the environment files
# Values consumed by compose.yaml itself
cp .env.docker.example .env.dockercompose.yaml reads everything from the file passed via --env-file, so .env.docker is the
single source of truth for a containerised deployment. DATABASE_URL and AUTH_SECRET are
mandatory — Compose fails fast if either is missing.
Configure network resolution
Three URL variables control how traffic is routed:
NEXT_PUBLIC_BACKEND_URL— the publicly reachable API endpoint used by the browser, for examplehttps://api.yourdomain.com/api/v1. This is baked in at build time.BACKEND_INTERNAL_URL— the address Next.js uses for server-side rendering inside the Docker network. Set it tohttp://api:8080/api/v1.ALLOWED_ORIGINS— a comma-separated list of frontend origins the API will accept CORS requests from. It must include your public site URL.
Set production-required variables
The API runs a fail-fast configuration validator at boot when NODE_ENV=production. It refuses
to start unless these hold:
AUTH_SECRETis set and is not the development default.AUTH_BASE_URLuses HTTPS.TRUST_PROXYis an explicit hop count or CIDR — a baretrueis rejected, because it lets a client spoofX-Forwarded-Forand defeat IP-based rate limiting. Behind a single reverse proxy, setTRUST_PROXY=1.API_KEY_HASH_SECRETis set and is different fromAUTH_SECRET.AUTH_EMAIL_PROVIDERissmtp(theconsoleprovider does not actually send mail), with valid SMTP credentials.AI_API_KEYis set and the AI policy defines both modes for every action.- Dodo Payments and all five Cloudflare R2 credentials are present.
This is deliberate: a misconfigured production deployment fails at boot rather than silently breaking on the first real checkout or upload.
Deployment
Build and start
From the repository root:
docker compose --env-file .env.docker up -d --buildCompose starts redis first, waits for its health check, then api, then web once the API
reports healthy.
Verify
# Service status
docker compose ps
# Unified logs
docker compose logs -f
# API logs only
docker compose logs -f apiThen confirm the API's dependency connections:
curl http://localhost:8080/api/v1/health/ready/health/ready probes PostgreSQL and Redis. /health is a lightweight liveness check that does
not touch either — the container health check uses /health deliberately, so an uptime probe
does not keep serverless database compute awake.
Production hardening
Exposing container ports directly to the internet is not recommended. Put a reverse proxy / TLS terminator (Nginx, Traefik, or Caddy) in front to handle:
- TLS termination — the API's own configuration validator requires an HTTPS
AUTH_BASE_URL. - Domain routing — mapping
yourdomain.comtoweb:3000andapi.yourdomain.comtoapi:8080. - Request filtering — an additional layer against common web attacks.
If you also deploy the portfolio builder, it serves published sites on wildcard subdomains
(*.yourdomain.com), which needs a wildcard DNS record and a wildcard TLS certificate.
Updating
# Pull the latest source
git pull origin master
# Apply any new Prisma schema changes to your external database
npm run db:push
# Rebuild and restart
docker compose --env-file .env.docker up -d --buildRun the schema push before restarting the API, so the running code never sees an older schema than it expects.