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
- **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`.
270 lines
12 KiB
YAML
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:
|