13. August 2026

PM2 startet den Dienst endlos neu und schreibt nichts ins Log

Der Reverse Proxy liefert 503, pm2 list zeigt „online", und das Fehlerprotokoll ist wochenalt. Wenn ein Prozess vor dem Anwendungscode stirbt, steht die Diagnose in einer Spalte, die kaum jemand liest.

Ein Node-Dienst hinter einem Apache-Reverse-Proxy. Jede Anfrage darauf liefert 503, und zwar in 140 Millisekunden. pm2 list zeigt den Dienst als online. Der letzte Eintrag im Fehlerprotokoll ist fünf Wochen alt.

Der Prozess startete rund dreimal in zehn Sekunden neu und starb jedes Mal, bevor auch nur eine Zeile Anwendungscode lief. Schuld waren zwei PM2-Installationen auf demselben System — und ein fehlender Schrägstrich.

Getestet mit

Debian 12 (Bookworm), Managed Server ohne Root-Zugang
PM2 4.4.0 (Kommandozeile, aus Linuxbrew) und 7.0.3 (laufender Daemon, aus /usr/bin)
Apache als Reverse Proxy auf 127.0.0.1
Node-Dienst im cluster_mode, eine Instanz

Schritt 1: Prüfen, ob der Proxy oder der Dienst antwortet

Ein 503 sagt für sich genommen wenig. Interessant wird er im Vergleich, und die entscheidende Zahl ist die Antwortzeit.

curl -o /dev/null -s -w "%{http_code} %{time_total}n" https://DEINE-DOMAIN.AT/
curl -o /dev/null -s -w "%{http_code} %{time_total}n" https://DEINE-DOMAIN.AT/API-PFAD/
curl -o /dev/null -s -w "%{http_code} %{time_total}n" https://DEINE-DOMAIN.AT/API-PFAD/gibt-es-nicht

Bei mir kam:

200 3.512
503 0.140
503 0.140

Zwei Dinge stecken darin. Erstens antwortet die Hauptanwendung normal, das Problem ist also auf den Dienst begrenzt. Zweitens liefert auch ein Pfad, den es in der Anwendung gar nicht gibt, denselben 503 in derselben Zeit. Eine Anwendung, die läuft, würde darauf mit 404 antworten. Hier antwortet nie jemand — der Proxy bekommt vom Upstream ein Connection refused und gibt seine eigene Fehlerseite aus.

Damit fällt eine Reihe naheliegender Ursachen weg:

  • Zu große Datenmenge ergäbe 413, nicht 503.
  • Wartungsmodus der Anwendung lieferte deren eigene Fehlerseite, nicht die des Webservers.
  • Firewall oder mod_security ergäbe 403 oder 406, und nicht bei erfundenen Pfaden.
  • Überlast oder Timeout dauerte Sekunden, nicht 140 Millisekunden.

Gegenprobe direkt am Port, ohne Proxy dazwischen:

curl -i http://127.0.0.1:PORT/
curl: (7) Failed to connect to 127.0.0.1 port PORT: Connection refused

Der Dienst ist nicht da. Am Proxy liegt es nicht.

Schritt 2: Den Zähler lesen, nicht die Statusspalte

Das ist die Stelle, an der die meisten vorbeilaufen. PM2 startet einen abgestürzten Prozess sofort neu. In der Statusspalte steht deshalb fast immer online, auch wenn der Dienst im Sekundentakt stirbt. Du erwischst ihn schlicht in dem Bruchteil einer Sekunde, in dem er gerade lebt.

pm2 show DIENSTNAME
status              online     ← kippt sofort wieder auf "stopped"
restarts            997755
unstable restarts   0
uptime              0s
exec mode           cluster_mode

uptime 0s bei knapp einer Million Neustarts ist keine Statistik mehr, das ist die Diagnose. Rechne es kurz nach: 997.755 Neustarts verteilt auf fünf Wochen sind rund drei pro zehn Sekunden, ununterbrochen, Tag und Nacht.

Dazu kommt der zweite Befund:

pm2 logs DIENSTNAME --err --lines 200 --nostream

Der letzte Eintrag stammte vom 6. Juli. Ein Prozess, der permanent neu startet und dabei nichts protokolliert, stirbt vor der Initialisierung des Anwendungscodes. Alles, was du in deiner Anwendung an Fehlerbehandlung eingebaut hast, ist an dieser Stelle noch gar nicht geladen.

Bevor du weitersuchst, schließ die banalen Ursachen aus. Ein volles Dateisystem und aufgebrauchte Inodes erzeugen genau dasselbe Bild:

df -h
df -i

Bei mir war die Platte zu 63 Prozent belegt und die Inodes bei maximal sechs Prozent. Beides unauffällig.

Schritt 3: Die beiden PM2-Versionen vergleichen

Ganz unten in der Ausgabe von pm2 show stehen zwei Zeilen, die kaum jemand beachtet:

In memory PM2 version:  7.0.3
Local PM2 version:      4.4.0

Der laufende Daemon war Version 7.0.3. Das Kommandozeilenwerkzeug, mit dem ich ihn ansprach, war 4.4.0. Zwei Hauptversionen Unterschied, und zwischen 6.x und 7.x hat PM2 intern einiges ausgetauscht.

Was dabei genau bricht, habe ich nicht weiter zerlegt — mir reichte das Fehlerbild. Im cluster_mode startet PM2 die Arbeitsprozesse nicht direkt, sondern über einen eigenen Wrapper. Genau dieser Start scheiterte, der Arbeitsprozess starb sofort, PM2 startete ihn neu. Endlos, ohne Protokolleintrag, weil der Anwendungscode nie erreicht wurde.

Dass überhaupt zwei Versionen im Spiel sind, zeigt dir das hier:

which -a pm2
/usr/home/BENUTZER/.linuxbrew/bin/pm2
/usr/bin/pm2
/usr/home/BENUTZER/.linuxbrew/bin/pm2 -v
/usr/bin/pm2 -v
4.4.0
7.0.3

Schritt 4: Den Suchpfad prüfen, der den Dienst startet

Welche der beiden Installationen zum Zug kommt, entscheidet der Suchpfad. Und der wird nicht in deiner Login-Shell gesetzt, sondern dort, wo der Dienst automatisch gestartet wird.

crontab -l
@reboot export PATH="usr/home/BENUTZER/.linuxbrew/bin:$PATH" && pm2 resurrect

Da fehlt am Anfang ein Schrägstrich. usr/home/BENUTZER/.linuxbrew/bin ist ein relativer Pfad, und relativ zum Arbeitsverzeichnis eines Cronjobs existiert er nicht. Die Zuweisung lief ins Leere. pm2 wurde deshalb aus /usr/bin aufgelöst und startete den Daemon in Version 7.0.3 — angesprochen wurde er später von der Linuxbrew-Version 4.4.0.

Ein einzelnes fehlendes Zeichen, fünf Wochen Ausfall.

Prüf so eine Zeile, indem du sie in derselben Umgebung ausführst, in der Cron sie ausführt:

env -i /bin/sh -c 'export PATH="usr/home/BENUTZER/.linuxbrew/bin:$PATH"; command -v pm2'

Kommt hier /usr/bin/pm2 statt des erwarteten Pfades, ist die Zuweisung wirkungslos.

Korrigiert:

@reboot export PATH="/usr/home/BENUTZER/.linuxbrew/bin:$PATH" && pm2 resurrect

Schritt 5: Sauber neu aufsetzen

Den alten Daemon vollständig beenden. pm2 restart reicht nicht — der laufende Daemon bleibt dabei in seiner Version bestehen.

/usr/home/BENUTZER/.linuxbrew/bin/pm2 kill

Danach eine versionierte Prozesskonfiguration anlegen, statt den Dienst wieder von Hand zu starten:

module.exports = {
  apps: [{
    name: 'DIENSTNAME',
    script: './dist/index.js',
    cwd: '/PFAD/ZUR/ANWENDUNG',
    exec_mode: 'fork',
    instances: 1,
    env: {
      NODE_ENV: 'production'
    },
    max_restarts: 10,
    min_uptime: '30s',
    restart_delay: 5000,
    max_memory_restart: '1G'
  }]
};

Drei Entscheidungen darin, die den Unterschied machen:

fork statt cluster. Der Dienst startet einen Browser im Hintergrund und hält davon genau eine Instanz. Clustering bringt hier nichts und war überhaupt erst der Grund, warum der Versionsversatz zuschlug — im fork_mode startet PM2 den Prozess direkt, ohne den Wrapper.

max_restarts und min_uptime. Ohne die läuft eine Schleife wie diese ewig weiter. Mit ihnen gibt PM2 nach zehn Versuchen auf und stellt den Prozess auf errored. Ein Dienst, der als kaputt markiert ist, fällt bei der nächsten Kontrolle auf. Ein Dienst mit siebenstelligem Neustartzähler sieht dagegen aus wie einer, der läuft.

restart_delay. Fünf Sekunden Pause zwischen den Versuchen. Verhindert, dass eine Schleife die CPU auffrisst, und macht die Logs lesbar.

Starten und dauerhaft sichern:

/usr/home/BENUTZER/.linuxbrew/bin/pm2 start ecosystem.config.js
/usr/home/BENUTZER/.linuxbrew/bin/pm2 save

Von Hand lief der Dienst sofort

Mein erster Versuch war der naheliegende: Dienst gestoppt, von Hand im Vordergrund gestartet. Er lief auf Anhieb. Keine Fehlermeldung, kein Absturz.

Während er lief, habe ich die Endpunkte von außen geprüft:

/API-PFAD/                          404  (keine Route auf /, korrekt)
/API-PFAD/images/temp-upload        405  (GET auf POST-Route, korrekt)
/API-PFAD/configurator/add-to-cart  405  (korrekt)

Kein 503 mehr. Damit war bewiesen: Proxy und Portkonfiguration stimmen, der Anwendungscode ist gesund.

Und damit bin ich in die falsche Richtung gelaufen. Wenn dieselbe Anwendung von Hand läuft und unter PM2 nicht, dann liegt es an der Umgebung — dachte ich, und habe eine gute Stunde in Umgebungsvariablen, Benutzerrechte und die Node-Version investiert, unter der PM2 startet.

Der Punkt war ein anderer. Beim manuellen Start ist überhaupt kein PM2 im Spiel. Ich habe nicht bewiesen, dass die Umgebung unter PM2 anders ist. Ich habe bewiesen, dass es ohne PM2 funktioniert — und damit auf den Prozessmanager selbst gezeigt, statt auf das, was er dem Prozess mitgibt.

Gefunden habe ich es über die zwei Versionszeilen in pm2 show. Die stehen ganz unten in einer Ausgabe, die ich bis dahin dreimal überflogen hatte, ohne sie zu lesen.

Woran ich nächstes Mal zuerst denke: Ein manueller Start, der funktioniert, grenzt den Fehler nicht auf die Umgebung ein. Er grenzt ihn auf alles ein, was der manuelle Start weglässt — und der Prozessmanager selbst gehört dazu.

Kontrolle

Erstens: beide PM2-Versionen stimmen überein.

pm2 show DIENSTNAME | grep -i "pm2 version"
In memory PM2 version:  4.4.0
Local PM2 version:      4.4.0

Zweitens: der Zähler bleibt stehen. Zurücksetzen, ein paar echte Anfragen durchspielen, nochmal hinsehen.

pm2 reset all
pm2 list

Steht der Restart-Zähler danach immer noch auf 0 und die Uptime wächst, ist der Codepfad sauber.

Drittens: von außen, mehrfach.

for i in 1 2 3; do
  curl -o /dev/null -s -w "%{http_code} %{time_total}n" https://DEINE-DOMAIN.AT/API-PFAD/gibt-es-nicht
done
404 0.147
404 0.154
404 0.140

Wichtig ist hier nicht der Statuscode selbst, sondern dass er von der Anwendung kommt und nicht mehr vom Proxy. 404 auf einen erfundenen Pfad heißt: Es antwortet jemand.

Viertens: den Neustart nach Reboot testen, wenn du einen Wartungsslot dafür bekommst. Ein Dienst, der erst nach dem nächsten Serverneustart wieder in die alte Falle läuft, ist nicht repariert.

Häufige Fehler

pm2 list sagt online, der Dienst antwortet trotzdem nicht.
Die Statusspalte lügt bei einer Neustartschleife, weil sie dich im Moment des Neustarts erwischt. Lies stattdessen uptime und den Restart-Zähler. uptime 0s bei einem Dienst, der seit Monaten läuft, ist das eigentliche Alarmsignal.

Das Fehlerprotokoll ist leer oder wochenalt.
Dann stirbt der Prozess, bevor dein Code läuft. Such nicht weiter in der Anwendung. Die Ursache liegt im Start selbst: Interpreter, Suchpfad, Prozessmanager, fehlende Bibliothek.

Reicht nicht pm2 update statt pm2 kill?
Meistens ja, dafür ist der Befehl da — er speichert die Prozessliste, tauscht den Daemon aus und stellt alles wieder her. In meinem Fall lagen zwei getrennte Installationen an unterschiedlichen Orten. Da war es sauberer, den Daemon zu beenden und mit einer definierten Version neu zu starten, als zwei Werkzeuge raten zu lassen, wer gerade zuständig ist.

Woher weiß ich, welche PM2-Installation der Cronjob benutzt?
Schreib in der Cron-Zeile den absoluten Pfad zur Programmdatei hin, statt dich auf PATH zu verlassen. Dann kann sich der Suchpfad ändern, ohne dass dein Dienst mit einer anderen Version hochkommt. Das ist die eigentliche Lehre aus dem fehlenden Schrägstrich.

Wenn der Dienst danach immer noch hängt.
Bei mir kam nach der Prozessverwaltung ein zweiter, davon unabhängiger Fehler zutage: Der Dienst lief, aber jede Anfrage blieb stehen, weil dem Server eine Systembibliothek für den Browser im Hintergrund fehlte. Das steht in einem eigenen Beitrag: Fehlende Chrome-Bibliothek auf Debian 12 nachrüsten.

Falls du nicht weiterkommst

Ich mache das beruflich. Schreib mir kurz, worum es geht — ich sag dir ehrlich, ob ich helfen kann.
Zu purin.at
← Alle Beiträge