Files
OPNexus/docker-compose.yml
T
Julian Boldt f5dbe8a41a
Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release / publish (updater) (push) Canceled after 0s
Release v1.21.0
- **Management-Tunnel** (System › Management-Tunnel): Firewalls hinter NAT/CGNAT verwalten. Ein optionaler WireGuard-Hub auf dem
  OPNexus-Server (Compose-Profil `overlay`, Container `wg-hub` aus dem Backend-Image mit `NET_ADMIN`) nimmt die Firewalls entgegen, die sich
  nach außen einwählen; OPNexus erreicht deren API über einen kleinen CONNECT-Proxy des Hubs, der nur ins Tunnel-Netz verbindet. Je Firewall
  eine Tunnel-Adresse und ein eigener Pre-Shared Key, Anleitung mit kopierbaren Werten, Live-Status (Handshake, Datenmenge). Der Hub nimmt
  keine neuen Verbindungen aus dem Tunnel an. Ende-zu-Ende gegen eine echte OPNsense 26.7 geprüft (Handshake, API, Zertifikat festsetzen).
  Neue Tabellen `overlay_settings`, `overlay_peers` (Migration `0015`); das Backend-Image enthält jetzt `wireguard-tools`, `iproute2`, `iptables`.
2026-10-20 01:21:45 +02:00

270 lines
12 KiB
YAML

name: opnexus
# Dieses File enthält nur fertige Release-Images (kein `build:`) und ist
# damit zugleich das Compose-File des Release-Bundles.
# - Fertige Release-Images: in .env OPNEXUS_IMAGE_REPO und OPNEXUS_VERSION
# setzen (siehe .env.example), dann `opnexus update` oder
# docker compose pull && docker compose up -d --no-build
# - Entwicklung/Selbstbau aus dem Repo: docker-compose.override.yml wird
# automatisch mitgeladen und ergänzt die Build-Kontexte:
# docker compose up -d --build
# (taggt lokal als local/opnexus-<dienst>:latest)
# Shared bound-size log driver — without this, Docker's default json-file
# driver keeps every log line forever, which on a long-running self-hosted
# install can quietly fill the disk (this has already happened once here
# via accumulated build cache, see the 2026-09-12 health audit — logs are
# the same failure mode).
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
logging: *default-logging
# Docker's default 64MB /dev/shm is too small for Postgres's parallel
# vacuum/query workers on anything but a toy dataset — found the hard
# way running VACUUM on a multi-GB table (see docs/HANDBUCH.md's "Logs" section for how
# large that table gets). Official Postgres image docs recommend raising this.
shm_size: "256mb"
environment:
# Datenbank- und Benutzername. Wirken nur beim allerersten Start eines
# leeren Volumes; eine bestehende Installation behält ihre Namen und
# trägt sie in .env ein (siehe docs/HANDBUCH.md, "Datenbank-Namen").
POSTGRES_DB: ${POSTGRES_DB:-opnexus}
POSTGRES_USER: ${POSTGRES_USER:-opnexus}
# No insecure fallback on purpose — a guessable baked-in default
# would ship as public, well-known source. Set it in .env.
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env before starting (see .env.example)}
volumes:
- postgres_data:/var/lib/postgresql/data
# No host port published — only reachable from other containers on
# this stack's own network. Keeps us out of "which host ports are
# free" territory entirely for this service.
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
interval: 5s
timeout: 5s
retries: 10
# Nightly logical dump of OPNexus's OWN database (users, rules, NAT,
# VPN/cert entities, audit trail, deployment history — everything
# except the secret material, which never touches Postgres in the
# first place, see the *_secret_store.py modules). This is distinct
# from the app's own backup feature under /hosts/{id}/backups, which
# backs up the MANAGED FIREWALLS' configs, not OPNexus's own data —
# before this, a lost/corrupted postgres_data volume had no recovery
# path at all. Reuses the postgres image directly (pg_dump is already
# in it) rather than adding a new image to build/maintain.
# Excludes firewall_log_entries/host_resource_samples: high-volume,
# disposable operational history (see docs/HANDBUCH.md's "Logs" section for
# how large these get) rather than
# config a disaster recovery actually needs — including them would
# make the backup itself multi-GB and a disk-usage risk in its own
# right, working against the point of this service.
postgres-backup:
image: postgres:16-alpine
restart: unless-stopped
logging: *default-logging
depends_on:
postgres:
condition: service_healthy
environment:
PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env before starting (see .env.example)}
volumes:
- postgres_backups:/backups
entrypoint: ["sh", "-c"]
command:
- |
set -e
while true; do
ts=$$(date +%Y%m%d-%H%M%S)
echo "postgres-backup: dumping to /backups/opnexus-$$ts.dump"
pg_dump -h postgres -U ${POSTGRES_USER:-opnexus} -d ${POSTGRES_DB:-opnexus} -F c \
--exclude-table=firewall_log_entries --exclude-table=host_resource_samples \
-f "/backups/opnexus-$$ts.dump"
find /backups -name 'opnexus-*.dump' -mtime +14 -delete
sleep 86400
done
backend:
image: ${OPNEXUS_IMAGE_REPO:-local}/opnexus-backend:${OPNEXUS_VERSION:-latest}
restart: unless-stopped
logging: *default-logging
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-opnexus}:${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env before starting (see .env.example)}@postgres:5432/${POSTGRES_DB:-opnexus}
OPNSENSE_SECRET_DIR: /data/opnsense-secrets
OPNSENSE_BACKUP_DIR: /data/opnsense-backups
IPSEC_PSK_SECRET_DIR: /data/ipsec-psk-secrets
OPENVPN_KEY_SECRET_DIR: /data/openvpn-key-secrets
WIREGUARD_KEY_SECRET_DIR: /data/wireguard-key-secrets
CERT_KEY_SECRET_DIR: /data/cert-key-secrets
ACME_SECRET_DIR: /data/acme-secrets
TOTP_SECRET_DIR: /data/totp-secrets
NOTIFICATION_SECRET_DIR: /data/notification-secrets
OVERLAY_DIR: /data/overlay
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-}
COOKIE_SECURE: ${COOKIE_SECURE:-false}
TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-false}
UPDATE_CHECK_URL: ${UPDATE_CHECK_URL:-}
ALLOW_INSECURE_FIREWALL_URLS: ${ALLOW_INSECURE_FIREWALL_URLS:-false}
ALLOW_INSECURE_SMTP_AUTH: ${ALLOW_INSECURE_SMTP_AUTH:-false}
SMTP_CA_FILE: ${SMTP_CA_FILE:-}
UPDATE_CHECK_INTERVAL_HOURS: ${UPDATE_CHECK_INTERVAL_HOURS:-12}
# Optionaler Updater-Dienst (Profil "updater", siehe unten). Ohne ihn
# zeigt das Webinterface nur an, wie man ihn aktiviert.
UPDATER_URL: ${UPDATER_URL:-http://updater:8080}
volumes:
- secrets:/data/opnsense-secrets
- backups:/data/opnsense-backups
- ipsec_psk_secrets:/data/ipsec-psk-secrets
- openvpn_key_secrets:/data/openvpn-key-secrets
- wireguard_key_secrets:/data/wireguard-key-secrets
- cert_key_secrets:/data/cert-key-secrets
- acme_secrets:/data/acme-secrets
- totp_secrets:/data/totp-secrets
- notification_secrets:/data/notification-secrets
# Management-Overlay: Konfiguration für den wg-hub und dessen Status.
- overlay_state:/data/overlay
# Gleiches Volume wie der postgres-backup-Service: hier landet das
# automatische Sicherheits-Backup vor einer Schema-Migration.
- postgres_backups:/backups
# Zugriffstoken des Updaters (nur lesen). Leer, solange der Updater
# nicht aktiviert ist.
- updater_state:/updater:ro
# Bound to loopback only by default — reachable from this host, not
# the network. Set BIND_ADDRESS in .env (e.g. to 0.0.0.0, or a
# specific interface IP) for network-wide access instead of editing
# this file — see docs/HANDBUCH.md's "Zugriff aus dem Netz und TLS" for the TLS-proxy
# alternative, which is safer than widening this directly.
ports:
- "${BIND_ADDRESS:-127.0.0.1}:${BACKEND_PORT:-8100}:8000"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=3)"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
frontend:
image: ${OPNEXUS_IMAGE_REPO:-local}/opnexus-frontend:${OPNEXUS_VERSION:-latest}
restart: unless-stopped
logging: *default-logging
environment:
# Docker always sets HOSTNAME=<container-id> in the environment,
# and Next.js's standalone server.js binds to `process.env.HOSTNAME
# || '0.0.0.0'` — so without this override it silently listens only
# on the container's own IP, never on 127.0.0.1. Harmless for the
# published port (Docker's NAT reaches the container IP directly,
# not via loopback) but breaks any in-container check (e.g. the
# healthcheck below) that queries localhost.
HOSTNAME: "0.0.0.0"
depends_on:
backend:
condition: service_healthy
# Same rationale and same BIND_ADDRESS override as the backend
# service above.
ports:
- "${BIND_ADDRESS:-127.0.0.1}:${FRONTEND_PORT:-3100}:3000"
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/login', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# TLS termination in front of everything. The only service meant to be
# reachable from the network; backend/frontend stay on 127.0.0.1.
proxy:
image: ${OPNEXUS_IMAGE_REPO:-local}/opnexus-proxy:${OPNEXUS_VERSION:-latest}
restart: unless-stopped
logging: *default-logging
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_healthy
environment:
OPNEXUS_SITES: ${OPNEXUS_SITES:-localhost}
OPNEXUS_DEFAULT_SNI: ${OPNEXUS_DEFAULT_SNI:-localhost}
ports:
- "${PROXY_BIND_ADDRESS:-0.0.0.0}:${PROXY_HTTPS_PORT:-443}:443"
- "${PROXY_BIND_ADDRESS:-0.0.0.0}:${PROXY_HTTP_PORT:-80}:80"
volumes:
- caddy_data:/data
- caddy_config:/config
# ── Optional: Management-Overlay (WireGuard-Hub) ─────────────────────
# Standardmäßig NICHT gestartet. Aktivieren mit COMPOSE_PROFILES=overlay in
# .env und `docker compose up -d`. Damit erreicht OPNexus auch Firewalls hinter
# NAT/CGNAT: sie wählen sich per WireGuard hier ein (UDP-Port ${OVERLAY_PORT:-51820}
# muss vom Internet erreichbar sein). Gleiches Image wie das Backend, mit
# NET_ADMIN nur für die WireGuard-Schnittstelle. Einrichtung: Handbuch,
# "Firewalls hinter NAT".
wg-hub:
image: ${OPNEXUS_IMAGE_REPO:-local}/opnexus-backend:${OPNEXUS_VERSION:-latest}
profiles: ["overlay"]
restart: unless-stopped
logging: *default-logging
command: ["python", "-m", "app.overlay_hub"]
cap_add:
- NET_ADMIN
environment:
OVERLAY_DIR: /data/overlay
volumes:
- overlay_state:/data/overlay
ports:
- "${OVERLAY_PORT:-51820}:51820/udp"
# ── Optional: Updates aus dem Webinterface ──────────────────────────
# Standardmäßig NICHT gestartet. Aktivieren mit COMPOSE_PROFILES=updater in
# .env und `docker compose up -d`. Dies ist der einzige Container mit
# Zugriff auf den Docker-Socket (= root-gleiche Rechte auf diesem Host): er
# hat keine veröffentlichten Ports, ist nur mit seinem Token ansprechbar
# und kann nur "aktualisieren"/"zurückrollen" (updater/server.py). Das
# Backend selbst bekommt den Socket nie. Nur für Release-Images
# (OPNEXUS_IMAGE_REPO gesetzt).
updater:
profiles: ["updater"]
image: ${OPNEXUS_IMAGE_REPO:-local}/opnexus-updater:${OPNEXUS_VERSION:-latest}
restart: unless-stopped
logging: *default-logging
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Das Verzeichnis mit docker-compose.yml und .env (der Updater schreibt
# nur OPNEXUS_VERSION und .opnexus-update-state hinein).
- .:/project
- updater_state:/state
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
volumes:
updater_state:
postgres_data:
postgres_backups:
secrets:
backups:
ipsec_psk_secrets:
openvpn_key_secrets:
wireguard_key_secrets:
cert_key_secrets:
acme_secrets:
totp_secrets:
notification_secrets:
overlay_state:
caddy_data:
caddy_config: