Skip to content

Docker (Single-VM)

A self-hosted OpenTremor instance runs as a set of Docker containers on a single host — there is no separate installer package. The workspace root ships two Compose files; which one you want depends on what you’re running:

FileUse for
docker-compose.yamlLocal dev / demo — localhost, one HAProxy front door, path-routed. See Quick Start.
docker-compose.prod.yamlA real, internet-facing single-VM install — per-service subdomains, TLS via Let’s Encrypt, zero-downtime cert reload. Covered on this page.

For a Kubernetes production deployment instead of a single host, see the Helm chart in OpenTremor-deployments.


All at the workspace root, alongside OpenTremor-core, both Terraform analyzer repos, OpenTremor-dashboard, and OpenTremor-docs checked out as siblings:

  • mongodb — persistent storage (./data/mongodb)
  • opentremor-core — API, built from docker/core-with-analyzers.Dockerfile
  • dashboard — built domain-rooted (not path-prefixed), pointed at the API’s own subdomain
  • docs — static Starlight build served via nginx
  • haproxy — TLS termination and routing, the only container publishing 80/443
  • certbot / certbot-bootstrap / certbot-webroot / haproxy-reloader — request and renew Let’s Encrypt certificates and hot-reload HAProxy with zero dropped connections, with a temporary self-signed cert generated on first boot so HAProxy can bind :443 before any real certificate exists

Each app service gets its own subdomain rather than being path-prefixed under one origin (e.g. api.example.com, docs.example.com, bare example.com for the dashboard) — see haproxy/prod.cfg.


  • A Linux host with Docker + Compose
  • Three DNS records (API, docs, dashboard/bare domain) already pointing at the host’s public IP — Let’s Encrypt’s HTTP-01 challenge needs to reach the host over the real internet on port 80 before any cert can be issued
  • Ports 80 and 443 free on the host (HAProxy owns them)

Copy and edit OpenTremor-core/configs/opentremor-core-prod.yaml — see that file’s own header comment for which fields have an environment-variable override. At minimum, set:

  • auth.api_key (bootstrap key — file-only, no env override)
  • cors.allow_origins to your dashboard’s real origin (a credentialed request can’t use *)

Prefer environment variables (or an untracked env file) over editing secrets directly into the YAML for anything that has an override — MONGODB_URI, JWT_SECRET, LLM_CREDENTIAL_KEY, DEFAULT_ADMIN_EMAIL, DEFAULT_ADMIN_PASSWORD.

Since the dashboard and API don’t share an origin here, also set SERVER_COOKIE_DOMAIN (e.g. .example.com) on the opentremor-core service in docker-compose.prod.yaml — without it the session cookie set at login defaults host-only to the API’s subdomain, and the dashboard’s server-side requests on the other host never see it. See Dashboard Deployment for the full explanation.

Terminal window
docker compose -f docker-compose.prod.yaml up --build -d

HAProxy binds :443 immediately using a temporary self-signed certificate — this is expected before the next step.

Terminal window
docker compose -f docker-compose.prod.yaml --profile init run --rm certbot-init

Edit the --email in docker-compose.prod.yaml’s certbot-init service before running. This is deliberately not part of the normal up — running it repeatedly during testing risks Let’s Encrypt’s rate limits. Once it succeeds, haproxy-reloader picks up the real certs on its next check (hourly), or force it immediately:

Terminal window
docker compose -f docker-compose.prod.yaml restart haproxy-reloader

Same one-time step as the quickstart, pointed at your real domain:

Terminal window
curl -X PATCH https://api.example.com/admin/settings \
-H "X-API-Key: <bootstrap-api-key>" -H "Content-Type: application/json" \
-d '{"server_dashboard_base_url": "https://example.com"}'

Unattended. The certbot service calls certbot renew roughly twice a day (a no-op unless a certificate is within 30 days of expiry); haproxy-reloader notices the resulting file change and reloads HAProxy with zero downtime. Nothing to schedule yourself.


Terminal window
docker compose -f docker-compose.prod.yaml up --build -d

Rebuilds and replaces the app images; MongoDB data and certificates persist in their volumes/bind mounts untouched.


Persistent state lives entirely in bind mounts under ./data/./data/mongodb and ./data/certbot. Back those up; the containers themselves are stateless and disposable.