Skip to content

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 compose plugin. Run docker compose version to 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 via SERVER_PORT / WEBRTC_PORT in .env):
    • 8080/tcp — NovaVMS API + embedded frontend (SERVER_PORT default)
    • 8555/tcp + 8555/udp — WebRTC media (WEBRTC_PORT default)
    • 3478/udp — TURN
    • 49152–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).

Terminal window
git clone https://github.com/novavms/novavms.git
cd novavms
./deploy.sh setup # generates .env.prod interactively

Then open .env.prod in an editor and add at minimum:

VariableWhat to setWhy
POSTGRES_PASSWORDGenerated by deploy.sh setupProtects your DB
JWT_SECRETGenerated by deploy.sh setup (openssl rand -hex 32)Signs user session tokens
PLATFORM_JWT_SECRETYou 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_ORIGINhttps://your-hostname (no trailing slash)Browser access from your domain
GOOGLE_APPLICATION_CREDENTIALSPath 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

Terminal window
./deploy.sh up # or: docker compose -f docker-compose.prod.yml --env-file .env.prod up -d

The 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:

Terminal window
docker compose -f docker-compose.prod.yml ps

All four services should show healthy. Tail the cloud logs to watch startup:

Terminal window
docker compose -f docker-compose.prod.yml logs -f cloud

Step 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:

Terminal window
migrate -path cloud/migrations -database "$DATABASE_URL" up

You 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

Terminal window
# 1. Back up the database
docker compose exec postgres pg_dump -U novavms novavms | gzip > backup-$(date +%F).sql.gz
# 2. Pull latest code and images
git pull
docker compose pull
# 3. Recreate containers with the new images
docker compose up -d

Read the release notes before every update. Breaking migration changes are flagged there.

If this didn’t work

  • If the cloud container restarts in a crash-loop, check docker compose -f docker-compose.prod.yml logs cloud for the first ERROR line. The four common causes are (a) DATABASE_URL pointing at the host’s localhost instead of the compose service name postgres, (b) JWT_SECRET shorter than 32 chars, (c) PLATFORM_JWT_SECRET missing or equal to JWT_SECRET (the loader returns config: PLATFORM_JWT_SECRET is required (D80) or config: 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.yml to 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.