services: prometheus: image: prom/prometheus:v3.3.1 container_name: prometheus restart: unless-stopped volumes: # Verzeichnis- statt Einzeldatei-Mount: Docker haengt Einzeldatei-Mounts am # Inode auf, und `git pull` ersetzt Dateien per Rename (neuer Inode) - der # Container sah die Aenderung dann nie, waehrend SIGHUP brav Erfolg meldete. # Verzeichnis-Mounts loesen bei jedem Zugriff ueber den Pfad auf. # Config-Pfade bleiben unveraendert (--config.file zeigt weiter dorthin). - ./prometheus:/etc/prometheus:ro - prometheus_data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--storage.tsdb.retention.time=45d' - '--storage.tsdb.retention.size=15GB' # Remote-Write-Receiver: k3s-Cluster und Matrix-Server pushen hierher. # ACHTUNG: Port 9090 ist oeffentlich und ohne Auth erreichbar -- # per Hetzner Cloud Firewall auf die Absender-IPs einschraenken! - '--web.enable-remote-write-receiver' ports: - "9090:9090" networks: - traefik alertmanager: image: prom/alertmanager:v0.28.1 container_name: alertmanager restart: unless-stopped volumes: # Verzeichnis-Mount, gleicher Grund wie bei Prometheus. Die beiden .py aus # diesem Ordner liegen dadurch mit unter /etc/alertmanager - ungenutzt und # harmlos, alertmanager laedt ausschliesslich --config.file. - ./alertmanager:/etc/alertmanager:ro - alertmanager_data:/alertmanager command: - '--config.file=/etc/alertmanager/alertmanager.yml' - '--storage.path=/alertmanager' # bewusst kein oeffentlicher Port - nur Prometheus/Receiver im traefik-Netz networks: - traefik # Alertmanager-Webhook -> Matrix (wartung-Raum), gleiche Machart wie # maintenance-notify (gitops Issue #24). Secrets kommen aus .env. matrix-alerts: image: python:3.13-slim container_name: matrix-alerts restart: unless-stopped environment: - MATRIX_HOMESERVER=${MATRIX_ALERT_HOMESERVER} - MATRIX_ROOM_ID=${MATRIX_ALERT_ROOM_ID} - MATRIX_TOKEN=${MATRIX_ALERT_TOKEN} # Raum-Routing (gitops#47): Alerts mit Label room=security -> Security-Raum - MATRIX_ROOM_SECURITY=${MATRIX_RELEASE_ROOM_ID:-} # Ohne das liegt der State unter /tmp im Writable Layer: der ueberlebt # zwar ein "compose restart", aber kein "up -d", das den Container neu # baut -- also genau jeden Deploy. Dann verlieren offene Alarme ihre # Event-Zuordnung und loesen sich ueber den Fallback als separate # Nachricht auf, statt die Firing-Nachricht abzuhaken. - MATRIX_STATE_FILE=/state/matrix-alerts-state.json volumes: - ./alertmanager:/app:ro - matrix_alerts_data:/state command: python3 /app/matrix-alerts.py networks: - traefik # Release-/Advisory-Watch (gitops#22): meldet neue Releases der Element-Stack- # Upstreams in den Alerts-Raum (🚨 bei Security-Verdacht). Gleicher Bot/Raum # wie matrix-alerts, eigener State (Erstlauf merkt nur, flutet nicht). release-watch: image: python:3.13-slim container_name: release-watch restart: unless-stopped environment: - MATRIX_HOMESERVER=${MATRIX_ALERT_HOMESERVER} - MATRIX_ROOM_ID=${MATRIX_ALERT_ROOM_ID} # CVE-/Release-Raum (gitops#47): leer lassen = Fallback Alerts-Raum - MATRIX_RELEASE_ROOM_ID=${MATRIX_RELEASE_ROOM_ID:-} - MATRIX_TOKEN=${MATRIX_ALERT_TOKEN} volumes: - ./alertmanager:/app:ro - release_watch_data:/state command: python3 /app/release-watch.py networks: - traefik # CVE-Pipeline (gitops#47), Teil 1: Trivy scannt die real deployten Images # (Ziele aus /targets/targets.txt - abgeleitet vom cve-exporter, NICHT von Hand # pflegen: #0106 / ADR-0026) cve-scan: image: aquasec/trivy:0.58.2 container_name: cve-scan restart: unless-stopped entrypoint: ["/bin/sh", "/config/scan-loop.sh"] volumes: - ./cve:/config:ro - cve_results:/results - cve_trivy_cache:/root/.cache # Ab Slice 3 die Quelle der Ziele; in Slice 1 nur eingehaengt, damit # der Pfad existiert und beim Umschalten kein Rennen entsteht. - cve_targets:/targets:ro networks: - traefik # Teil 2: Reports -> Prometheus-Metriken (Schema + Pflichtfelder siehe gitops#47) cve-exporter: image: python:3.13-slim container_name: cve-exporter restart: unless-stopped environment: - RESULTS_DIR=/results - STATE_FILE=/state/first-seen.json # Soll-Menge der Scan-Ziele (#0106, ADR-0026): abgeleitet, nicht gepflegt. # Der Exporter bildet sie, weil er fuer die Deckungsmetrik ohnehin BEIDE # Mengen kennen muss - ein zweiter Ort waere eine zweite Wahrheit. - TARGETS_FILE=/targets/targets.txt - PROMETHEUS_URL=http://prometheus:9090 - REGISTRY_HOST=rohana.axion1337.de # Reichweite (Entscheidung sorb 2026-08-21, Option C): Bestand plus die # letzten drei Fassungen je Repo - so weit, wie ein Rollback zielt. - REGISTRY_TAGS_JE_REPO=3 # Nicht je Scrape herleiten: bei 15 s waeren das ~21 000 # Registry-Anfragen am Tag; eine Runde kostet ~90. - SOLL_INTERVALL_SEKUNDEN=3600 volumes: - ./cve:/app:ro - cve_results:/results:ro - cve_exporter_state:/state - cve_targets:/targets command: python3 /app/cve-exporter.py networks: - traefik loki: image: grafana/loki:3.7.1 container_name: loki restart: unless-stopped volumes: - ./loki:/etc/loki:ro - loki_data:/loki command: -config.file=/etc/loki/loki-config.yaml # Port 3100 oeffentlich: externe Alloys pushen Logs hierher. # Ebenfalls per Firewall auf bekannte Absender-IPs einschraenken. ports: - "3100:3100" networks: - traefik alloy: image: grafana/alloy:v1.16.0 container_name: alloy restart: unless-stopped volumes: - ./alloy:/etc/alloy:ro - /var/log:/var/log:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro - /var/run/docker.sock:/var/run/docker.sock:ro # Muss persistent sein, sonst ist die Positions-Datei nach jedem # Recreate weg und Alloy liest alle Container-Logs von vorn. Loki # weist die alten Eintraege dann ab (reject_old_samples, Default 7d). - alloy_data:/var/lib/alloy command: run --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy cap_add: - DAC_READ_SEARCH - NET_RAW networks: - traefik depends_on: - prometheus - loki grafana: image: grafana/grafana:12.0.0 container_name: grafana restart: unless-stopped environment: # Greift nur beim allerersten Start mit leerem Volume. # Der Live-Admin ist davon unabhaengig (Passwortaenderungen in der UI). - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} volumes: - grafana_data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/var/lib/grafana/dashboards:ro labels: - "traefik.enable=true" - "traefik.docker.network=traefik" - "traefik.http.routers.grafana.rule=Host(`selendis.axion1337.de`)" - "traefik.http.routers.grafana.entrypoints=websecure" - "traefik.http.routers.grafana.tls.certresolver=letsencrypt" - "traefik.http.services.grafana.loadbalancer.server.port=3000" depends_on: - prometheus - loki networks: - traefik node-exporter: image: prom/node-exporter:v1.9.1 container_name: node-exporter restart: unless-stopped volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro command: - '--path.procfs=/host/proc' - '--path.sysfs=/host/sys' - '--path.rootfs=/rootfs' - '--web.listen-address=:9100' - '--web.disable-exporter-metrics' ports: - "9100:9100" networks: - traefik volumes: prometheus_data: alertmanager_data: matrix_alerts_data: release_watch_data: cve_results: cve_trivy_cache: cve_exporter_state: cve_targets: grafana_data: loki_data: alloy_data: networks: traefik: external: true