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.env → mariadb_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: source → app umbenannt (konsistent mit Schema), Typo roles → role 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.alloy — stage.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.j2 — log.* weg, format weg, site weg, Port-Variable, label_drop
- stack/itop/config/alloy/90-output.alloy — loki.process "cleanup" entfernt (service_name bleibt)
- ansible/roles/monitoring_agents/defaults/main.yml — monitoring_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:2 → nfrastack/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 (slapcat → scripts/clean-kopano-ldif.py → ldapadd -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.