Deploy NovaVMS on-prem with Docker Compose
Use this procedure to run your own NovaVMS cloud on a Linux server that you control. The production compose stack at docker-compose.prod.yml ships postgres, cloud, frontend, and the ai-runtime sidecar. MinIO is included only in the self-host dev compose (docker-compose.yml); production self-hosters typically point storage at GCS or external S3 via env vars, or run MinIO separately.
Who this is for
You are standing up NovaVMS on hardware you own — a customer site, a lab, or an air-gapped network. You will be the admin of the resulting org. If you only want to use the Novalien-hosted SaaS version at novavms.novalien.com, you do not need this page — sign in and skip ahead to Add your first site.
Prerequisites
- OS: Ubuntu 22.04 LTS or newer, or any Linux with kernel 5.10+.
- Docker: Docker Engine 24+ with the
composeplugin. Rundocker compose versionto confirm. - Hardware (single-site, up to 20 cameras): 4 CPU cores, 8 GB RAM, 200 GB SSD.
- Inbound network ports open on the server (defaults from
docker-compose.yml— override viaSERVER_PORT/WEBRTC_PORTin.env):8080/tcp— NovaVMS API + embedded frontend (SERVER_PORTdefault)8555/tcp + 8555/udp— WebRTC media (WEBRTC_PORTdefault)3478/udp— TURN49152–49252/udp— TURN relay ports
- Outbound network access from the server to the internet (for AI provider API calls, optional SendGrid).
Step 1 — Clone the repo and configure .env.prod
The simplest path is ./deploy.sh setup, which generates .env.prod with secure random POSTGRES_PASSWORD and JWT_SECRET interactively. The script does not yet generate PLATFORM_JWT_SECRET — you must add that manually (see GAP-9-01).
git clone https://github.com/novavms/novavms.gitcd novavms./deploy.sh setup # generates .env.prod interactivelyThen open .env.prod in an editor and add at minimum:
| Variable | What to set | Why |
|---|---|---|
POSTGRES_PASSWORD | Generated by deploy.sh setup | Protects your DB |
JWT_SECRET | Generated by deploy.sh setup (openssl rand -hex 32) | Signs user session tokens |
PLATFORM_JWT_SECRET | You must add manually — a random string that differs from JWT_SECRET (e.g. openssl rand -hex 32). The cloud container refuses to start without it (cloud/internal/config/config.go:488-494). | Signs platform-admin tokens, per D80 must not equal JWT_SECRET |
CORS_ORIGIN | https://your-hostname (no trailing slash) | Browser access from your domain |
GOOGLE_APPLICATION_CREDENTIALS | Path to a Google service-account JSON inside the container (e.g. /secrets/service-account.json) | Required for the production Vertex AI path; leave unset only if you accept AI-disabled operation |
Step 2 — Start the stack
./deploy.sh up # or: docker compose -f docker-compose.prod.yml --env-file .env.prod up -dThe production compose starts four long-running services: postgres, cloud (the Go server, container novavms-cloud), frontend (the Vite-built SPA behind nginx), and ai-runtime (the AI sidecar). The optional gateway service is commented out by default; uncomment it for co-located deployments. Wait 30–60 seconds for health checks to pass, then verify:
docker compose -f docker-compose.prod.yml psAll four services should show healthy. Tail the cloud logs to watch startup:
docker compose -f docker-compose.prod.yml logs -f cloudStep 3 — Run database migrations
Migrations run automatically on the cloud server’s first startup via the embedded golang-migrate runner (cloud/cmd/server/main.go:194-197). Confirm by looking for a log line like migration applied in the cloud service log.
If you ever need to run migrations manually (for example after editing migration files during development), the command is:
migrate -path cloud/migrations -database "$DATABASE_URL" upYou need the migrate CLI installed locally and the same DATABASE_URL as the server.
Step 4 — Complete the first-run setup wizard
Open the URL you configured for CORS_ORIGIN (or http://<your-server-hostname> if you exposed the frontend container directly) in a browser. While setup_state.completed_at is null, the setup guard allows setup, health, and SPA routes but returns 503 SETUP_REQUIRED for other API routes. AuthGuard reads setup status and uses React Router to navigate you to /setup; the server does not issue HTTP 302.
The current wizard is Admin Account → Storage → First Site → Health Check, then Finish Setup. The historical setup-time AI-provider choice was retired; there is no /api/v1/setup/ai route. Event-intelligence provider credentials are deployment-time Gemini/Vertex configuration, while the Settings AI page manages supported features. See First site setup for the walkthrough.
Step 5 — Put Caddy or nginx in front for TLS
Do not expose the NovaVMS container directly to the internet without TLS. Terminate HTTPS in a reverse proxy. Minimal Caddy example (/etc/caddy/Caddyfile):
novavms.example.com { reverse_proxy localhost:8080}Then sudo systemctl reload caddy. Caddy handles Let’s Encrypt certificate issuance automatically on first request.
For production hardening (headers, rate limiting, separate TURN vhost, log rotation), see arch-deployment §9 Security considerations in the engineering docs.
Updating NovaVMS
# 1. Back up the databasedocker compose exec postgres pg_dump -U novavms novavms | gzip > backup-$(date +%F).sql.gz
# 2. Pull latest code and imagesgit pulldocker compose pull
# 3. Recreate containers with the new imagesdocker compose up -dRead the release notes before every update. Breaking migration changes are flagged there.
If this didn’t work
- If the
cloudcontainer restarts in a crash-loop, checkdocker compose -f docker-compose.prod.yml logs cloudfor the first ERROR line. The four common causes are (a)DATABASE_URLpointing at the host’slocalhostinstead of the compose service namepostgres, (b)JWT_SECRETshorter than 32 chars, (c)PLATFORM_JWT_SECRETmissing or equal toJWT_SECRET(the loader returnsconfig: PLATFORM_JWT_SECRET is required (D80)orconfig: PLATFORM_JWT_SECRET must differ from JWT_SECRET), (d) storage validation failing (storage validation failed). - Gateway-side install on a separate machine (deploying
docker-compose.gateway.ymlto a Raspberry Pi or remote Linux host) is covered by the First gateway install page; that workflow is Phase 10 hardware territory and is not in scope here. - If the container is healthy but you cannot log in, reset the admin password via a direct DB update — see your DBA, or open the self-host support channel.
Related
- First site setup — finish the setup wizard after the stack is up
- First gateway install — pair the first gateway into your new org
- Network requirements — port and bandwidth reference