Wenn systemd in Schleifen geht — Service-Loops, fehlende Abhängigkeiten und wie ich den Boot wieder stabil bekomme

Ein Mechaniker mit Schraubenschlüssel vor einem geöffneten Computer Bild: Warum systemd manchmal aussieht wie ein Wartungstechniker, der alle Schrauben gleichzeitig aufmachen will.

Incident

Dienstag, 6:42 Uhr. Der Monitoring-Ping ist rot. Nginx antwortet nicht mehr. Ein kurzer systemctl status nginx zeigt: inactive (dead). Kein Fehler, keine Absturz-Meldung — der Service ist einfach nicht gestartet.

Ich versuche ihn manuell zu starten: systemctl start nginx. Sofort wieder inactive. Kein Log, kein Hinweis. Ich wechsle zu journalctl -u nginx — leer. Gar nichts. Das System schweigt.

Nach 10 Minuten des Herumprobierens fällt mir auf: Der Server hat gerade neu gebootet. Und systemd hat Nginx beim Boot übersprungen, ohne Fehler, ohne Warnung. Warum?

Root Cause

Die Antwort stand in /var/log/syslog, nicht im nginx-Log:

systemd[1]: nginx.service: Found ordering cycle on nginx.service/start
systemd[1]: nginx.service: Dependency network would create a cycle.

Nginx hatte eine After=network.target-Abhängigkeit — aber auch eine Requires=postgresql.service. Und Postgres wiederum hatte eine After=nginx.service-Abhängigkeit, weil eine Applikation bei Postgres-Start einen Health-Check gegen Nginx macht.

Ein klassischer systemd-Dependency-Loop. systemd erkennt den Zyklus, bricht die Schleife — und deaktiviert dabei den schwächeren Service. In diesem Fall Nginx. Ohne Fehlermeldung im Service-Log, ohne sichtbaren Hinweis für mich.

Lösung

Der Fix besteht aus zwei Teilen:

1. Dependency-Zyklus auflösen

Statt einer direkten Abhängigkeit zwischen Nginx und Postgres verwende ich eine Weiche Kopplung:

# /etc/systemd/system/nginx.service.d/override.conf
[Unit]
# Entferne die direkte Require= Beziehung
# Stattdessen: Nur noch After=network.target
After=network.target

# Wenn du wirklich sicherstellen willst, dass Postgres läuft,
# verwende ExecStartPre mit einem Timeout
ExecStartPre=/bin/sh -c 'timeout 5s bash -c "until systemctl is-active --quiet postgresql; do sleep 0.5; done"'
# /etc/systemd/system/postgresql.service.d/override.conf
[Unit]
# Keine Abhängigkeit mehr zu Nginx
# Falls Health-Check nötig ist: ExecStartPost verwenden
ExecStartPost=/bin/sh -c 'sleep 2 && curl -sf http://localhost:8080/health > /dev/null || true'

Wichtig: systemctl edit nginx und systemctl edit postgresql erstellen die Override-Dateien sauber, ohne die Haupt-Unit zu überschreiben.

2. systemd-Konfiguration neu laden

# Alle Unit-Dateien neu laden
systemctl daemon-reload

# Prüfe, ob noch immer ein Zyklus besteht
systemctl show nginx -p After,Requires

# Dependency-Graph visualisieren
systemctl list-dependencies nginx --all

# Starte beide Services in der korrekten Reihenfolge
systemctl restart postgresql
systemctl restart nginx

Verifikation

# 1. Prüfe, dass beide Services laufen
systemctl is-active nginx postgresql
# Erwartete Ausgabe: active

# 2. Prüfe den Boot-Log auf Dependency-Zyklen
journalctl -b -p warning | grep -i "ordering cycle\|dependency network"
# Erwartete Ausgabe: KEINE Treffer

# 3. Simuliere einen Reboot und prüfe den Status
systemctl reboot
# Nach Reboot:
systemctl status nginx postgresql
# Beide müssen active sein

Fazit

systemd-Zyklen sind still. Sie werfen keine Exception, sie schreiben keinen Stacktrace — sie deaktivieren einfach den Service, der in der Abhängigkeitskette „schwächer” ist. Bei mir war das Nginx, bei dir könnte es der Datenbank-Service sein.

Die Lehre: Prüfe nach jedem Reboot mit systemctl list-dependencies --all und journalctl -b -p warning, ob systemd etwas übersprungen hat. Und vermeide direkte Requires=-Beziehungen zwischen Services, die sich gegenseitig starten müssen. Ein ExecStartPre mit Timeout ist meistens die bessere Wahl.

Offene Frage an dich: Hast du schon mal einen systemd-Zyklus gehabt, der nicht sofort sichtbar war? Welches Tool nutzt du, um Dependency-Graphen zu prüfen — systemd-analyze plot oder etwas anderes?