Skip to content

Installation

l0rdg3x edited this page Jun 13, 2026 · 1 revision

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.


Contents


Prerequisites

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 required

Image model

OPNGMS 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 tagslatest 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 image

Step 1 — Configure the environment (.env)

cp .env.example .env
# edit .env — it is gitignored; never commit it

Two password pairs that MUST match

The 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

Generate fresh secrets

# 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.

Fail-closed change-me guard

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_SECRET
  • MASTER_KEY

This is intentional: you cannot accidentally run OPNGMS on default secrets.

OPNGMS_VERSION

Defaults to latest. Pin to a semver tag for reproducible deployments:

OPNGMS_VERSION=0.1.0

Timezone (TZ)

Set 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/Rome

TLS-related variables (fill in for Step 2)

Depending 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

Step 2 — Choose a TLS model

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

Model 1 — Behind your own reverse proxy or load balancer (recommended)

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.

Model 2 — Built-in nginx TLS with your own certificate

nginx terminates TLS itself. HTTP on port 80 redirects to HTTPS on port 443.

  1. Place your certificate files in ${CERT_DIR} (default ./certs):
    • ./certs/fullchain.pem
    • ./certs/privkey.pem
  2. Set SERVER_NAME=your.domain in .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).

Model 3a — Automatic TLS via Caddy (Let's Encrypt)

A bundled Caddy container obtains and auto-renews a real certificate from Let's Encrypt. Zero cert files needed.

  1. Set DOMAIN=your.domain and ACME_EMAIL=you@example.com in .env.
  2. Ensure the DNS A/AAAA record for DOMAIN points at this host and ports 80/443 are reachable from the internet.

Model 3b — Automatic TLS via Traefik (Let's Encrypt)

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.

  1. Set DOMAIN=your.domain and ACME_EMAIL=you@example.com in .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: https to the frontend service.


Step 3 — Pull and start

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.

Model 1 — Behind your proxy

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

Model 2 — Built-in TLS, your certificate

docker 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 -d

Model 3a — Automatic TLS via Caddy

docker 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 -d

Model 3b — Automatic TLS via Traefik

docker 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 -d

All-in-one: core + log lake in a single file

docker-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 -d

Build from source instead of pulling. Build the images locally first (see Image model above), then run up -d without pull — Compose uses the locally tagged images.

Health checks

# 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 worker

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

Step 4 — First run

In 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.

1. Create the first superadmin

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.

2. Sign in and enrol TOTP

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

3. Configure SMTP delivery

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.

4. Onboard tenants and devices

From the admin console:

  1. Create tenants (customers).
  2. 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.
  3. Per tenant, set Report settings — title, logo, language, and an optional white-label sender address that overrides the global SMTP From.
  4. 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.
  5. 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.


Log lake (optional)

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 -d

Or use docker-compose.full.yml (see Step 3) to bring up core + log lake in a single command.


Development setup

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+.


Troubleshooting

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.

Clone this wiki locally