-
Notifications
You must be signed in to change notification settings - Fork 0
Installation
This guide walks through every step needed to get OPNGMS running in production on a Linux host, from prerequisites through first login. For day-two operations (upgrades, key rotation, backups) see Upgrading; for the optional syslog log lake see Log-Lake; for every environment variable in detail see Configuration.
- Prerequisites
- Image model
- Step 1 — Configure the environment (.env)
- Step 2 — Choose a TLS model
- Step 3 — Pull and start
- Step 4 — First run
- Log lake (optional)
- Development setup
| Requirement | Notes |
|---|---|
| Linux host | Any modern amd64 or arm64 distribution. |
| Docker Engine | Current stable release recommended. |
| Docker Compose v2.24.4 or newer | The TLS overlay files use the !override YAML merge tag, which requires this version. Check with docker compose version. |
| Outbound HTTPS from the host | The worker polls each managed OPNsense appliance over HTTPS. All target boxes must be reachable from the server. |
| Outbound SMTP | An external SMTP relay you provide and configure inside the app after the first login. No relay container is included. |
| Public DNS name + ports 80/443 | Required only for automatic TLS (Models 3a/3b). The domain must resolve to the host and both ports must accept inbound connections from the internet for Let's Encrypt validation. |
Verify Compose before proceeding:
docker compose version
# Docker Compose version v2.24.4 or higher requiredOPNGMS ships pre-built multi-arch container images from the GitHub Container Registry — no local source build is needed for a standard deployment:
| Image | Architectures |
|---|---|
ghcr.io/l0rdg3x/opngms-backend |
linux/amd64, linux/arm64
|
ghcr.io/l0rdg3x/opngms-frontend |
linux/amd64, linux/arm64
|
Images are published only from semver release tags — latest always resolves to the newest tagged release, never to a development commit. Pin a specific release for reproducible deploys:
OPNGMS_VERSION=0.1.0 # in .env; default is `latest`Building from source instead. If you need to run unreleased code or build your own images, build locally before running up:
docker build -t ghcr.io/l0rdg3x/opngms-backend:latest ./backend
docker build -t ghcr.io/l0rdg3x/opngms-frontend:latest ./frontend
# then run `up -d` without `pull` — Compose will use the local imagecp .env.example .env
# edit .env — it is gitignored; never commit itThe migrate service creates the non-superuser opngms_app database role from APP_ROLE_PASSWORD, and the api service connects as that role using the password embedded in DATABASE_URL. Similarly, the worker and migrate services connect as the owner using ADMIN_DATABASE_URL, which must carry the same password as POSTGRES_PASSWORD (the credential the TimescaleDB container creates on first boot). If either pair does not match, the app will fail to connect to the database.
| Variable to set… | …must equal | Role |
|---|---|---|
Password in DATABASE_URL
|
APP_ROLE_PASSWORD |
Non-superuser opngms_app — API, RLS-enforced |
Password in ADMIN_DATABASE_URL
|
POSTGRES_PASSWORD |
DB owner — migrations, worker, RLS-exempt |
Example (both pairs aligned):
POSTGRES_USER=opngms
POSTGRES_PASSWORD=s3cr3t-owner-pw
POSTGRES_DB=opngms
DATABASE_URL=postgresql+asyncpg://opngms_app:s3cr3t-app-pw@db:5432/opngms
APP_ROLE_PASSWORD=s3cr3t-app-pw
ADMIN_DATABASE_URL=postgresql+asyncpg://opngms:s3cr3t-owner-pw@db:5432/opngms# SESSION_SECRET — server-side session signing key
python -c "import secrets; print('SESSION_SECRET=' + secrets.token_urlsafe(48))"
# MASTER_KEY — Fernet key encrypting device credentials and SMTP credentials at rest
python -c "from cryptography.fernet import Fernet; print('MASTER_KEY=' + Fernet.generate_key().decode())"Paste the output into .env. The MASTER_KEY_OLD_KEYS variable is only used during key rotation (see Upgrading) — leave it empty on a fresh install.
The API refuses to start if any of the following variables still contains the change-me placeholder shipped in .env.example:
POSTGRES_PASSWORD-
DATABASE_URL(its embedded password) APP_ROLE_PASSWORD-
ADMIN_DATABASE_URL(its embedded password) SESSION_SECRETMASTER_KEY
This is intentional: you cannot accidentally run OPNGMS on default secrets.
Defaults to latest. Pin to a semver tag for reproducible deployments:
OPNGMS_VERSION=0.1.0Set an IANA timezone name (e.g. Europe/Rome, America/New_York) so container logs read in your local time. All data is stored in UTC. Report-schedule hours are interpreted in UTC by design — changing TZ does not shift when a report fires, only how timestamps appear in logs.
TZ=Europe/RomeDepending on the TLS model you choose below, you may need to set some of these:
| Variable | Default | Used by |
|---|---|---|
FRONTEND_BIND |
127.0.0.1 |
Base model (Model 1) — bind address of the plain-HTTP frontend port |
FRONTEND_HTTP_PORT |
8080 |
Base model — host port to forward from your upstream proxy |
SERVER_NAME |
opngms.example.com |
Models 1 and 2 — your hostname (nginx server_name) |
CERT_DIR |
./certs |
Model 2 — directory containing fullchain.pem + privkey.pem
|
HTTP_PORT |
80 |
Models 2, 3a, 3b — published HTTP port |
HTTPS_PORT |
443 |
Models 2, 3a, 3b — published HTTPS port |
DOMAIN |
opngms.example.com |
Models 3a, 3b — public hostname for Let's Encrypt |
ACME_EMAIL |
you@example.com |
Models 3a, 3b — ACME account email for Let's Encrypt |
The SPA carries login sessions and Secure cookies. HTTPS is mandatory in production — browsers silently drop Secure cookies over plain HTTP on a real domain, which breaks login entirely. Pick exactly one model; the overlay files are mutually exclusive.
| # | Compose files | TLS terminated by | Certificate source | Host ports published |
|---|---|---|---|---|
| 1 | base only (docker-compose.prod.yml) |
Your edge proxy / LB / ingress | Yours (upstream) |
127.0.0.1:8080 (HTTP, localhost-only) |
| 2 | + docker-compose.tls.yml |
Built-in nginx | Yours — fullchain.pem + privkey.pem in ./certs
|
80 (redirect) → 443
|
| 3a | + docker-compose.caddy.yml |
Built-in Caddy | Let's Encrypt (automatic) |
80 + 443
|
| 3b | + docker-compose.traefik.yml |
Built-in Traefik | Let's Encrypt (automatic) |
80 + 443
|
The base stack serves the frontend as plain HTTP bound to 127.0.0.1:8080 — it is never internet-facing. Put your existing TLS terminator (Cloudflare, a cloud ALB, an existing nginx/Caddy/Traefik, a Kubernetes ingress) in front and forward traffic to 127.0.0.1:8080. The upstream proxy must add the header:
X-Forwarded-Proto: https
This tells uvicorn and nginx to issue Secure cookies. Without this header, logins will fail in browsers on a real domain.
No additional .env changes are required for the base bind address and port. If you run the proxy on a different machine and need to bind to 0.0.0.0, set FRONTEND_BIND=0.0.0.0 in .env — but only do so if something terminates TLS in front of that port.
nginx terminates TLS itself. HTTP on port 80 redirects to HTTPS on port 443.
- Place your certificate files in
${CERT_DIR}(default./certs):./certs/fullchain.pem./certs/privkey.pem
- Set
SERVER_NAME=your.domainin.env.
If no certificate files are present, a self-signed certificate is generated at startup so the container still boots (browsers will show a TLS warning).
A bundled Caddy container obtains and auto-renews a real certificate from Let's Encrypt. Zero cert files needed.
- Set
DOMAIN=your.domainandACME_EMAIL=you@example.comin.env. - Ensure the DNS A/AAAA record for
DOMAINpoints at this host and ports 80/443 are reachable from the internet.
Functionally identical to 3a but uses Traefik — useful if you already run Traefik on the same Docker host or on Kubernetes. Same DNS and port requirements apply.
- Set
DOMAIN=your.domainandACME_EMAIL=you@example.comin.env.
Note: On Kubernetes, use the base compose model (Model 1) and let a Traefik or any other ingress controller terminate TLS and forward
X-Forwarded-Proto: httpsto the frontend service.
Run the command that matches the TLS model you chose. migrate applies alembic upgrade head (creating the schema, the opngms_app role, RLS policies, and grants) before api and worker start — this happens automatically on every deploy.
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -ddocker compose -f docker-compose.prod.yml -f docker-compose.tls.yml pull
docker compose -f docker-compose.prod.yml -f docker-compose.tls.yml up -ddocker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml pull
docker compose -f docker-compose.prod.yml -f docker-compose.caddy.yml up -ddocker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml pull
docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml up -ddocker-compose.full.yml bundles the core six services plus OpenSearch, syslog-bootstrap, and syslog-ng in one file. It serves plain HTTP bound to localhost (Model 1), so you still need a TLS-terminating proxy in front.
docker compose -f docker-compose.full.yml pull
docker compose -f docker-compose.full.yml up -dBuild from source instead of pulling. Build the images locally first (see Image model above), then run
up -dwithoutpull— Compose uses the locally tagged images.
# Check all services are up and api is healthy
docker compose -f docker-compose.prod.yml ps
# Tail live logs from the API
docker compose -f docker-compose.prod.yml logs -f api
# Tail worker logs (report delivery, polling)
docker compose -f docker-compose.prod.yml logs -f workerThe api service has a health check at GET /healthz (polled every 10 s, 12 retries). Wait for it to show healthy before proceeding to Step 4. The migrate service exits with code 0 on success; if it exits non-zero, check its logs:
docker compose -f docker-compose.prod.yml logs migrateIn the examples below, replace https://<your-domain> with http://127.0.0.1:8080 if you are on Model 1 and your upstream proxy is not yet in place.
This endpoint is one-time only — it refuses if any user already exists:
curl -X POST https://<your-domain>/api/setup \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","name":"Admin","password":"<strong-password>"}'Use a real email domain. Addresses on reserved TLDs (
.local,.internal,.test, and similar) are rejected by RFC-compliant validation.
Navigate to https://<your-domain> and sign in with the credentials you just created. Go to Two-factor auth to enrol TOTP (scan the QR code with an authenticator app) and save the one-time recovery codes in a safe place.
Optionally set the enforcement policy (off / all / privileged) to require MFA for all users or only privileged roles. When MFA is enrolled, subsequent logins are two-step: password first (POST /api/login), then TOTP code (POST /api/login/mfa).
Break-glass recovery. If the last superadmin is locked out, reset MFA with the host-level CLI:
docker compose -f docker-compose.prod.yml exec api \ python -m app.cli mfa-reset --email admin@example.com
Go to Admin → SMTP delivery and enter your relay settings:
- Host, port, security (STARTTLS / implicit TLS / none)
-
Username and password (encrypted at rest with
MASTER_KEY— never returned by the API) - Default From address and display name
- Enable delivery
Use Send a test email to verify the relay is reachable before configuring real recipients. SMTP is configured entirely in-app; there are no SMTP-related variables in .env.
From the admin console:
- Create tenants (customers).
- Add devices under each tenant, providing the OPNsense API key/secret and the box's address. Use the reachability test to confirm the connector can reach the box.
- Per tenant, set Report settings — title, logo, language, and an optional white-label sender address that overrides the global SMTP From.
- Per tenant, configure Report schedule — a fleet-level schedule (whole tenant) and/or per-device schedules. Each schedule runs weekly, monthly, or on-demand at a UTC hour, with its own list of recipient email addresses.
- Use Send now to deliver a report immediately without waiting for the schedule.
For the version-aware configuration editor and config template library, see Configuration-Editor and Configuration.
The log lake is an opt-in overlay that adds mTLS syslog-ng → OpenSearch ingest for full per-device, per-tenant log storage and investigation. It is separate from the API-pull event ingest (Suricata alerts, DNS queries), which continues to work independently.
See Log-Lake for the full bring-up guide, network requirements (port 6514 must be reachable by managed devices), the multi-node OpenSearch option, and the in-app Logs investigation page.
Quick start — add the log lake on top of the core stack:
# First: add the log-lake variables to .env
# (SYSLOG_RECEIVER_HOST, SYSLOG_TLS_PORT, OPENSEARCH_URL, LOG_RETENTION_DAYS)
docker compose -f docker-compose.prod.yml -f docker-compose.logs.yml up -dOr use docker-compose.full.yml (see Step 3) to bring up core + log lake in a single command.
For a local development environment (Python venv + uvicorn --reload + Vite dev server), see Development. Development requires Docker + Docker Compose (for the infrastructure services only), Python 3.14, and Node.js 24+.
| Symptom | Cause / fix |
|---|---|
| API container exits immediately with "refusing to start" | A secret in .env still contains change-me. Set real values for all six guarded variables (Step 1). |
| Login works via curl but the browser keeps returning to the sign-in page | The SPA is being served over plain HTTP on a real domain — browsers drop Secure cookies. Terminate TLS (Step 2) and ensure X-Forwarded-Proto: https is forwarded to the app. |
docker compose errors on the !override tag |
Docker Compose is older than v2.24.4. Upgrade it. |
| Scheduled reports never arrive | SMTP not enabled or misconfigured — use Send a test email in Admin → SMTP delivery. Also check that the schedule is enabled and has at least one recipient. Inspect worker logs and the in-app audit log. |
| Let's Encrypt won't issue a certificate (Models 3a/3b) | The DNS A/AAAA record for DOMAIN must resolve to this host and ports 80 and 443 must be reachable from the internet for the HTTP-01 challenge. |
migrate exits non-zero |
Check docker compose logs migrate. Most often a wrong password pair (Step 1) or the database container not yet healthy. |
For further diagnostics see Troubleshooting. For security hardening notes (TLS pinning, MASTER_KEY rotation, session configuration) see Security.
Deploy & operate
Understand & extend