Skip to content

Infrastructure Roadmap

Prioritized backlog for philipp.info infrastructure. Ordered by criticality/effort/value ratio.

Priority Matrix

# Item Criticality Effort Value Priority
1 Mailstack evaluieren & umbauen High 1S + 3-5S High P1
5 /etc nach Ansible Medium Low High P1
6 scripts/ nach Ansible Medium Low High P1
4 Docker-Labels evaluieren Low Low Medium P2
7 OpenLDAP weg von Bitnami High Medium High P2
3 Synapse modernisieren (MAS) Medium Medium High P3
8 Nextcloud & Paperless Verdrahtung Low Low Low P3
9 cAdvisor containerd socket fix Low Low Medium P3
2 Nextcloud Object Storage Medium High High P4

P1 — Quick Wins (niedriger Aufwand, hoher Nutzen)

5. /etc Sonder-Installationen nach Ansible ✅

Status: Erledigt (2026-02-15).

Integriert: - /etc/mysql_snapshot.envmariadb_config Rolle (Vault-Variablen) - /etc/docker/daemon.json → war bereits in docker_host Rolle - /etc/borg-backup.env → war bereits in borg_backup Rolle - cert-renewer@{keycloak,ldap,traefik} Overrides → docker_host Rolle (datengetrieben via docker_cert_renewers Variable) - fail2ban.service.d/override.conf (Hardening) → fail2ban Rolle (konfigurierbar via fail2ban_systemd_override)

Nicht integriert (Distro-Defaults): - Logrotate-Rules (mariadb, fail2ban, prometheus-*) — kommen mit den jeweiligen Paketen, kein Custom-Bedarf


6. scripts/ nach Ansible auflösen ✅

Status: Erledigt (2026-02-15).

Script Ansible-Rolle Ergebnis
phil-db/borg-backup/backup.sh borg_backup War bereits integriert — Referenzkopie obsolet
phil-db/mysql_snapshot/mysql_snapshot.sh mariadb_config Template + systemd Timer/Service + Vault env
phil-app/xfs_quota_exporter/xfs_quota_docker.sh monitoring_agents Template + systemd Service
phil-db/node-reboot-required/ monitoring_agents Script + Timer (kein .deb-Package mehr nötig)

Zusätzlich: - 55_monitoring.yml Playbook erweitert auf beide Server (war vorher nur phil-db) - scripts/ Verzeichnis kann archiviert/gelöscht werden

Offen: - Vault-Variablen für mysql_snapshot (Nextcloud WebDAV Credentials) müssen ins Vault-File eingetragen werden - scripts/ Verzeichnis löschen nach Validierung auf dem Server


P2 — Mittlerer Aufwand, strategischer Nutzen

4. Loki Label-Cleanup ✅

Status: Erledigt (2026-02-15).

1. Entfernte Loki-Labels (via Alloy-Relabeling und Compose-Files): - env — immer "prod" (1 Wert, kein Filterwert) - team — immer "platform" (1 Wert, kein Filterwert) - log_format — nur pipeline-intern für JSON/Text-Routing (via stage.label_drop vor Loki gestrippt) - service — redundant mit project + component - service_name — auto-generiert von Alloy OTEL-Layer, wird beibehalten (Grafana Logs-View nutzt es als Default-Gruppierung) - container — 60 Werte, nie gefiltert - hostname — Container-ID, redundant mit server (external label) - filename — auto-generiert von loki.source.file - config — auto-generiert (Traefik-Config-Referenzen) - systemd_unit — ~6000 Werte, größter Kardinalitäts-Killer - unit, slice, systemd_transport — Journal-Metadaten, nie gefiltert - source — redundant mit job + app - format, site — nie genutzt, aus Ansible-Template entfernt

2. Label-Homogenisierung (phil-app ↔ phil-db): - Journal-Labels angeglichen: beide Hosts nutzen app=host, component=journald, tier=infra - Phil-app: relabel_rules vom Journal-Source entfernt → nutzt jetzt __journal__systemd_unit direkt in Pipeline-Selektoren (wie phil-db), keine unit-Label-Promotion mehr - Nextcloud-Logs unterscheidbar: component=app vs component=audit - EIDSP: sourceapp umbenannt (konsistent mit Schema), Typo rolesrole gefixt - Ansible-Template: log.* Parallel-Labels und site external label entfernt - Auto-generierte Labels (hostname, filename) via stage.label_drop in allen Pipelines gestrippt; service_name bewusst beibehalten (Grafana Logs-View) - Alloy Self-Telemetrie: logging { write_to = [...] } + loki.process "alloy_internal" auf beiden Hosts — interne Logs bekommen app=alloy, component=agent statt service_name=unknown_service

3. phil-db Loki-Konnektivität repariert: - Problem: phil-app Shorewall blockiert Port 443 aus WireGuard-Zone → Alloy auf phil-db konnte Loki nicht erreichen - Fix: monitoring_loki_scheme: http, monitoring_loki_port: 3100 (direkter Loki-Zugriff über WireGuard, Port 3100 ist erlaubt) - Ansible-Template um monitoring_loki_port-Variable erweitert

Verbleibende Labels: app, component, tier, job, project, level, server, instance

Geänderte Dateien: - stack/itop/config/alloy/00-sources.alloy — Relabeling bereinigt, Journal auf phil-db-Schema, Nextcloud component-Split - stack/itop/config/alloy/10-pipeline.web.alloystage.label_drop für routing-only + auto-Labels - stack/itop/config/alloy/20-pipeline.app.alloy — WireGuard-Selector auf __journal__systemd_unit, stage.label_drop - ansible/roles/monitoring_agents/templates/config.alloy.j2log.* weg, format weg, site weg, Port-Variable, label_drop - stack/itop/config/alloy/90-output.alloyloki.process "cleanup" entfernt (service_name bleibt) - ansible/roles/monitoring_agents/defaults/main.ymlmonitoring_loki_port, monitoring_site_label entfernt - ansible/inventory/prod/host_vars/phil-db.yml — HTTP + Port 3100, log_queries_not_using_indexes: 0 - 19 docker-compose.yml in stack/*/log.env und log.team Labels entfernt

Erwartete Reduktion: ~206 → ~80-100 aktive Streams (nach Retention-Zeitraum der alten Streams)


7. OpenLDAP weg von Bitnami ✅

Status: Erledigt (2026-02-27). Siehe ADR-006.

bitnamilegacy/openldap:2 wurde durch nfrastack/openldap:2.6 ersetzt (das Projekt wurde von tiredofit/docker-openldap zu nfrastack/container-openldap umbenannt — tiredofit/openldap:latest ist eingefroren und crasht auf modernen Kerneln).

Umgesetzt: - Image: bitnamilegacy/openldap:2nfrastack/openldap:2.6 (Alpine-basiert, aktiv gewartet) - Port: 1389 (Bitnami non-root) → 389 (Standard) - Volume-Pfad: /bitnami/openldap/data - Custom Dockerfile entfernt — memberOf ist in nfrastack kompiliert (kein Env-Var nötig) - Kopano-Schema bereinigt (slapcatscripts/clean-kopano-ldif.pyldapadd -c) - ldap_config_password Secret hinzugefügt - Alle Konsumenten auf Port 389 aktualisiert (Keycloak, phpLDAPadmin, mailcow) - Keycloak LDAP-Federation erfolgreich reconnected


P3 — Niedrige Dringlichkeit, moderater Aufwand

3. Synapse modernisieren (MAS) ✅

Status: Erledigt (2026-02-16).

Umgesetzt: - Matrix Authentication Service (MAS) als Docker-Service in stack/matrix/ deployt - MAS delegiert Auth upstream zu Keycloak (sso.philipp.info/realms/Family) via neuen mas OIDC-Client - Synapse nutzt experimental_features.msc3861 zur Auth-Delegation an MAS - Native OIDC-Provider (oidc_providers:) und LDAP-Fallback (password_providers:) aus homeserver.yaml entfernt - ldap-network aus dem Matrix-Stack entfernt (nicht mehr benötigt) - MAS läuft unter matrix.philipp.info/auth/ (pfad-basiertes Routing via Traefik, kein eigener DNS-Eintrag nötig) - MAS-DB (mas) in PostgreSQL 17 lokal im Stack - shared_secret_authenticator Modul für Bridge-Double-Puppeting beibehalten - Eigene MAS-Config: stack/matrix/config/mas/config.yaml

Migration (Schritte auf dem Server): 1. Keycloak: neuen Client mas im Realm Family anlegen (confidential, redirect: https://matrix.philipp.info/auth/upstream/callback/*) 2. PostgreSQL: mas-DB manuell anlegen (init-Script läuft nur bei erster DB-Init) 3. Secrets in config/mas/config.yaml eintragen (DB-Passwort, Keycloak-Client-Secret, Matrix-Secret, Admin-Token) 4. Secrets in homeserver.yaml msc3861-Block abgleichen (client_secret, admin_token) 5. docker compose up -d mas — MAS starten, syn2mas check + dry-run ausführen 6. Synapse + Bridges stoppen, syn2mas migrate ausführen, Stack neu starten 7. Login + Bridge-Double-Puppeting + Federation testen


9. cAdvisor: Switch to containerd socket (userns-remap fix) ✅

Status: Erledigt (2026-02-27).

Problem: cAdvisor's Docker factory is broken under userns-remap — the Docker socket returns empty DockerVersion/DockerAPIVersion, so cAdvisor discovers 0 containers. All container_oom_events_total metrics fire without container labels, making the ContainerOOMKilled alert useless. See pitfalls.md.

Fix: In stack/itop/docker-compose.yml, switch cAdvisor from Docker socket to containerd socket: 1. Mount /run/containerd/containerd.sock:/run/containerd/containerd.sock:ro 2. Pass --containerd=/run/containerd/containerd.sock to cAdvisor 3. Remove Docker socket volume mount (/var/run/docker.sock) 4. Keep the /var/lib/docker/165536.165536:/var/lib/docker:ro volume mount

Effort: Low (single compose file change). Impact: ContainerOOMKilled alerts become useful; per-container metrics correctly labeled.


8. Nextcloud & Paperless Verdrahtung (Nice-to-have)

Status: Integration existiert bereits und funktioniert: - Shared Volume paperless_consume — Nextcloud kann Dateien zum Paperless-Consume ablegen - Paperless-Archiv als Read-Only Bind-Mount in Nextcloud (/opt/paperless-ngx/media/documents/archive) - Beide nutzen Keycloak OIDC

Verbesserungsoptionen: - Nextcloud External Storage App: Paperless-Archiv als External Storage in Nextcloud einbinden (statt Bind-Mount), damit Dateien im Nextcloud-UI sichtbar sind mit Metadaten - Nextcloud Workflow → Paperless: Nextcloud Flow-Rules die PDFs automatisch in den Consume-Ordner kopieren (z.B. alle PDFs in einem bestimmten Ordner)

Aufwand: Gering. Niedrige Priorität — bestehende Integration ist funktional.


P4 — Große Projekte (hoher Aufwand, hoher Nutzen)

2. Nextcloud von Storagebox auf Hetzner Object Storage

Status: - Primärdaten: Bind-Mount auf Host (/mnt/nextcloud_data/) - Archiv/Daten laufen über ein SSHFS-gemountetes Storagebox-Volume — das ist kein Good Practice (fragil, FUSE-Overhead, Single Point of Failure bei Netzwerkproblemen) - Storagebox wird auch für Borg-Backups genutzt (520+ GB für friendicame allein)

Problem: SSHFS-Mount auf Hetzner Storagebox ist instabil und nicht für dauerhaften Betrieb als Nextcloud-Storage geeignet. Netzwerk-Timeouts, FUSE-Layer-Overhead und fehlende native S3-Integration machen dies zum Engpass.

Warum Object Storage: - S3-kompatible API — Nextcloud unterstützt S3 als Primary Storage nativ (kein FUSE-Layer) - Skaliert besser als Storagebox (keine feste Quota) - Kein SSHFS/rclone-Mount nötig (direkter S3-Zugriff aus PHP) - Günstiger bei großen Datenmengen (Object Storage: ~€5/TB vs Storagebox: ~€3.50/TB aber mit Borg-Overhead)

Risiken: - Migration: Alle bestehenden Dateien müssen migriert werden (occ files:scan + S3-Upload). Bei großen Instanzen dauert das Stunden/Tage. - Performance: S3-Latenz höher als lokales Filesystem. Für Preview-Generierung und Thumbnail-Caching relevant. - Backups: Borg kann nicht direkt auf S3 zugreifen. Backup-Strategie muss angepasst werden (Object Storage hat eigene Versionierung). - Vendor Lock-in: Hetzner Object Storage ist S3-kompatibel, aber Wechsel erfordert Datenmigration.

Aufwand: 2-3 Sessions. Testinstanz aufsetzen, migrieren, Backup-Strategie anpassen.

Vorgehen: 1. Hetzner Object Storage Bucket anlegen 2. Nextcloud config.php auf S3 Primary Storage umstellen (Testinstanz) 3. Daten migrieren via occ files:scan + occ maintenance:repair 4. Performance validieren (Preview-Generierung, Upload/Download) 5. Backup-Strategie anpassen (S3 Lifecycle Rules statt Borg) 6. SSHFS-Mount und Storagebox-Nutzung für Nextcloud eliminieren


1. Mailstack: Kopano → mailcow ✅

Status: Abgeschlossen (2026-02-27). Alle 8 Phasen erledigt. Entscheidung: mailcow. Siehe ADR-005.

Phase 0 abgeschlossen: Speichermessung ergab ~105 GB Migrationsdaten (DB 16.89 GB + Attachments 88 GB), nicht 500 GB wie geschätzt. md4 hat 614 GB frei → Szenario A (md4 mit zstd, kein sda nötig).

Phase 1 abgeschlossen (2026-02-17): mailcow-dockerized als Fork auf git.opensocial.at, Submodule in stack/mailcow/. Stack laeuft sauber, Web-UI unter mailcow.philipp.info erreichbar. userns-remap geloest via Init-Container fix-permissions (chownt Bind-Mounts + Volumes auf UID 165536 bei jedem docker compose up). Dovecot zstd-Kompression per-protocol konfiguriert. Alle Services healthy, keine Permission-Errors in Logs.

Phase 2 abgeschlossen (2026-02-18): Keycloak OIDC End-to-End. SSO-Login via Keycloak funktioniert, SOGo-Redirect transparent. force_sso aktiviert — Web-UI zeigt nur SSO-Button, Admin-UI (/admin) behaelt lokalen Login. Auth fuer Protocol-Clients (IMAP/SMTP/ActiveSync) via App-Passwoerter pro Geraet (erstellt nach Keycloak-SSO-Login mit 2FA in mailcow Web-UI).

Zielarchitektur: mailcow (Postfix + Dovecot + SOGo + Rspamd + ClamAV) als Docker Compose Stack.

Warum mailcow (Kurzfassung): - Einzige Option mit OIDC/Keycloak End-to-End (inkl. SOGo-Proxy-Auth — kein zweiter Login) - ActiveSync für iPhone/Android (Mail + Kalender + Kontakte in einem Profil) - SOGo-Webmail mit integriertem Kalender/Kontakten - Docker-nativ, bewährte Migration via imapsync - Details und verworfene Alternativen: ADR-005

Bekannte Risiken: - SOGo ActiveSync: Push-Bugs auf iPhone (bekannt, Workaround: SOGo-Restart-Cron) - SOGo 6: ActiveSync evtl. nicht im ersten Release (H2 2026) — mailcow-Community beobachtet - Eigene MySQL-Instanz in mailcow (läuft nicht auf phil-db)

Migration (~105 GB, 6 Domains):

Phase Schritt Status
0 Speicherplatz messen ✅ 105 GB, md4 reicht
1 mailcow deployen (Fork, Submodule, Volumes, Traefik) ✅ Stack laeuft, userns geloest
2 Keycloak OIDC + Auth ✅ SSO + LDAP passdb + force_sso
3 Testmigration (imapsync) ✅ philipp: 15.7 GiB, 248k Msgs, 0 Err
4 CalDAV/CardDAV-Migration ✅ philipp: 4365 Events, 925 Kontakte
5 Mail-Vollmigration via imapsync ✅ alle 6 User, 0 Fehler
6 DNS-Cutover (15-30 Min Downtime) ✅ Routing + SMTP + LE-Certs aktiv
7 Verifizierung + Fixes + Monitoring + Limits ✅ TLS-Fix, Auth-Fix, Monitoring, Limits
8 Aufräumen (Kopano, mail2, sogo entfernen) ✅ Abgeschlossen (2026-02-27)

Phase 3 abgeschlossen (2026-02-22): IMAP-Testmigration mit groesster Mailbox (philipp@dieholzers.at). mailcow's eingebauter Sync Job (imapsync im dovecot-Container) lief 11.5h: 147/147 Ordner, 248.623 Nachrichten, 15.731 GiB, 0 Fehler. Dovecot zstd-Kompression aktiv — 20 GB auf Disk. md4 noch 588 GB frei.

Phase 4 abgeschlossen (2026-02-22): CalDAV/CardDAV-Testmigration fuer philipp. Kopano kdav (SabreDAV) unzuverlaessig bei Recurring Events (duplizierte HREFs, Timeouts). Loesung: Kopano Python API (item.ics() / item.vcf()) exportiert direkt aus der DB → CalDAV PUT auf SOGo Port 20000. Ergebnis: 4365 Events + 925 Kontakte migriert, 0 Fehler. PK-Events (497) und Tasks (3) via vdirsyncer.

Phase 5 abgeschlossen (2026-02-22): IMAP-Sync + CalDAV/CardDAV fuer alle 6 User. Kopano Postfix gestoppt vor Migration. IMAP: christian 22k/4.8G, clemens 17k/10.5G, julia 10k/10.9G, uschi 9k/6.7G, karin 1.5k/176M, philipp 248k/15.7G. CalDAV/CardDAV: alle 6 User importiert, 0 Fehler gesamt.

Phase 6 abgeschlossen (2026-02-22): Cutover durchgefuehrt. Prod-Routing aktiv (HTTP + IMAP/IMAPS + SMTP). Let's Encrypt Certs aus Traefik-Dump in mailcow, Cron-Job fuer Erneuerung. Alle Services verifiziert (SMTPS 465, Submission 587, IMAPS 993, HTTP, SOGo).

Phase 7 abgeschlossen (2026-02-23): TLS-Cert-Fix, LDAP-passdb entfernt (2FA-Bypass), App-Passwoerter als Auth, Clients verifiziert. Monitoring: Blackbox SMTP/IMAP Probes (interne Container-Aliases wegen Docker→Host Hairpin-NAT), Prometheus Exporter (Queue/Domains/Rspamd via prometheus-network), Loki Labels OK. php-fpm OOM gefixt (Pool 50→10, Container 2g Cap). Resource Limits fuer alle 17 Container (Memory + CPU Limits + Reservations, Gesamt ~12.25 GiB / 22 CPU). Borgmatic-Backup eingerichtet (7 Volumes auf StorageBox).

Phase 8 abgeschlossen (2026-02-27): - ✅ stack/mail2/ entfernt (DMS + Roundcube) - ✅ stack/sogo/ entfernt (Standalone SOGo) - ✅ stack/kopano/ entfernt — Volumes gelöscht, Netzwerk bereinigt, borgmatic angepasst - ✅ Kopano-Attribute aus LDAP entfernt (via clean-kopano-ldif.py beim LDAP-Image-Wechsel)

Abhängigkeiten: - → LDAP (#7): Nach Kopano-Sunset weniger LDAP-Konsumenten → Migration einfacher - → SOGo: Bestehendes stack/sogo/ wird durch mailcow's eingebettetes SOGo ersetzt - → Traefik: Routing für mail.philipp.info / Webmail muss auf mailcow umgestellt werden - → Borgmatic: Backup-Jobs für mailcow-Volumes hinzufügen

Gesamtaufwand: 6-8 Sessions


Empfohlene Reihenfolge

Phase 1 (Erledigt)            Phase 2 (Erledigt)                Phase 3 (Nach Kopano-Sunset)
━━━━━━━━━━━━━━━━━━━━          ━━━━━━━━━━━━━━━━━━━━━━━━━━        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#1 Mail Evaluierung ✅         #1 mailcow Migration (6-8S) ✅    #7 OpenLDAP Migration ✅ 
#5 /etc → Ansible ✅           #4 Label-Cleanup ✅               #2 NC Object Storage (SSHFS ablösen)
#6 scripts/ → Ansible ✅       #3 Synapse MAS ✅

Phase 1: Abgeschlossen (2026-02-15). Quick Wins #5/#6 umgesetzt, Mail-Evaluierung ergibt mailcow als Ziel (ADR-005).
Phase 2: Label-Cleanup und Synapse MAS abgeschlossen. mailcow-Migration ist der nächste große Arbeitspunkt (6-8 Sessions). Phase 3 abgeschlossen: LDAP auf nfrastack/openldap:2.6 migriert (2026-02-27). NC Object Storage noch offen.