Vom Unit-File bis zum robusten Queue-Worker
Wer PHP-Worker und Queue-Consumer nur per nohup oder einer Screen-Session am Laufen hält, riskiert stille Ausfälle nach jedem Server-Neustart, Deployment oder Absturz. Eine sauber geschriebene systemd Unit-Datei mit klarer Restart-Policy, dediziertem Service-User und automatischer journald-Integration macht PHP-Hintergrundprozesse robust, beobachtbar und produktionsreif, ohne dass zusätzliche Tools wie Supervisor oder Monit nötig werden.
Inhaltsverzeichnis
- 1. Warum PHP-Worker einen eigenen systemd-Service brauchen
- 2. Anatomie einer Unit-Datei: Unit, Service, Install
- 3. ExecStart korrekt konfigurieren
- 4. Restart-Policy für robuste Worker-Prozesse
- 5. User und Group: nie als root betreiben
- 6. Umgebungsvariablen mit EnvironmentFile einbinden
- 7. Service aktivieren, starten und testen
- 8. Logging mit journald: automatisch statt manuell
- 9. Häufige Fehler bei Unit-Dateien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum PHP-Worker einen eigenen systemd-Service brauchen
Lang laufende PHP-Prozesse wie Queue-Consumer, Message-Worker oder Import-Skripte werden in vielen Projekten mit nohup php worker.php & oder in einer screen-Session gestartet. Das funktioniert, solange niemand den Server neu startet, das SSH-Terminal schließt oder der Prozess wegen eines unbehandelten Fehlers abstürzt. Genau in diesen Momenten fällt der Worker lautlos aus, die Queue füllt sich, und niemand bemerkt es, bis Kunden sich über ausbleibende Bestellbestätigungen oder verzögerte Exporte beschweren.
Ein eigener systemd-Service löst dieses Problem strukturell: Der Init-Prozess von Linux überwacht den Worker, startet ihn beim Boot automatisch, startet ihn nach einem Absturz neu und schreibt jede Ausgabe in ein zentrales Log. Anders als bei Cronjobs, die eine Aufgabe wiederholt kurz starten, läuft ein systemd-Service dauerhaft im Hintergrund und ist damit die richtige Wahl für Queue-Consumer, WebSocket-Server oder Scheduler-Daemons, die permanent aktiv bleiben müssen.
2. Anatomie einer Unit-Datei: Unit, Service, Install
Eine systemd-Unit-Datei gliedert sich in drei Abschnitte. Der [Unit]-Block beschreibt Metadaten und Abhängigkeiten: Description für eine lesbare Bezeichnung und After/Wants, um festzulegen, dass der Service erst startet, nachdem Netzwerk, Datenbank oder Redis verfügbar sind. Ohne diese Abhängigkeiten kann ein Worker starten, bevor die Datenbankverbindung steht, und sofort mit einer Exception abbrechen.
Der [Service]-Block enthält die eigentliche Konfiguration: welcher Befehl ausgeführt wird, unter welchem Benutzer, mit welchem Arbeitsverzeichnis und welcher Neustart-Strategie. Der [Install]-Block legt fest, in welchem Systemziel (Target) der Service eingebunden wird, meist multi-user.target für Server ohne grafische Oberfläche. Erst der [Install]-Block macht einen Service über systemctl enable dauerhaft aktivierbar, ohne ihn manuell nach jedem Neustart zu starten.
3. ExecStart korrekt konfigurieren
ExecStart ist die zentrale Direktive, die den eigentlichen Prozess startet. Der häufigste Fehler: ein relativer Befehl wie php worker.php ohne absoluten Pfad. systemd startet Services ohne Shell und ohne die üblichen Umgebungsvariablen wie PATH aus der interaktiven Login-Shell, deshalb müssen sowohl der PHP-Binary-Pfad als auch der Skriptpfad vollständig ausgeschrieben werden, etwa /usr/bin/php /var/www/html/bin/magento queue:consumers:start async.operations.all.
Für Magento-Projekte ist der Queue-Consumer-Befehl ein realistisches Beispiel: --max-messages begrenzt die Anzahl verarbeiteter Nachrichten pro Durchlauf und verhindert, dass sich über Tage Speicher im PHP-Prozess ansammelt, ähnlich einem Memory Leak durch langlebige Objekte. systemd startet den Prozess nach Erreichen des Limits automatisch neu, sofern Restart=always gesetzt ist. Wichtig ist außerdem Type=simple für Prozesse, die im Vordergrund laufen und sich nicht selbst forken, das ist bei den meisten PHP-CLI-Workern der Standardfall.
# /etc/systemd/system/mironsoft-queue-worker.service
[Unit]
Description=Mironsoft Magento Queue Consumer (async.operations.all)
After=network-online.target mysql.service redis-server.service
Wants=network-online.target mysql.service redis-server.service
[Service]
Type=simple
User=magento
Group=magento
WorkingDirectory=/var/www/html
EnvironmentFile=/etc/mironsoft/queue-worker.env
ExecStart=/usr/bin/php /var/www/html/bin/magento queue:consumers:start async.operations.all --max-messages=10000
Restart=always
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
TimeoutStopSec=30
KillSignal=SIGTERM
# Everything written to stdout/stderr lands in journald automatically
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mironsoft-queue-worker
[Install]
WantedBy=multi-user.target
4. Restart-Policy für robuste Worker-Prozesse
Standardmäßig ist Restart=no gesetzt, ein Service bleibt nach jedem Beenden inaktiv, egal ob gewollt oder durch einen Fehler. Für einen dauerhaft laufenden Queue-Worker ist Restart=always die richtige Wahl, weil sie den Prozess sowohl nach einem regulären Exit als auch nach einem Crash oder einem SIGKILL neu startet. RestartSec=5 wartet fünf Sekunden zwischen den Neustartversuchen und verhindert, dass ein dauerhaft fehlschlagender Worker die CPU mit endlosen Neustartschleifen belastet.
StartLimitIntervalSec und StartLimitBurst begrenzen zusätzlich, wie oft systemd innerhalb eines Zeitfensters neu starten darf, bevor der Service dauerhaft in den Zustand failed wechselt und ein Alarm ausgelöst werden sollte. Ohne diese Begrenzung würde ein Worker mit einem strukturellen Bug, etwa einer falschen Datenbank-Zugangsdaten, endlos neu starten und dabei Log-Dateien und Systemressourcen füllen, ohne dass jemand das Problem bemerkt.
# Restart-Verhalten unter realen Bedingungen testen
systemctl status mironsoft-queue-worker --no-pager
# Prozess simuliert abstürzen lassen
sudo kill -9 $(systemctl show --property MainPID --value mironsoft-queue-worker)
# Live beobachten, wie systemd den Worker automatisch neu startet
journalctl -u mironsoft-queue-worker -f --since "1 minute ago"
# Neustart-Zähler und letzten Exit-Code prüfen
systemctl show mironsoft-queue-worker --property=NRestarts,ExecMainStatus,ActiveState
5. User und Group: nie als root betreiben
Ohne explizite User-Direktive läuft ein systemd-Service standardmäßig als root, das gilt auch für PHP-Worker, die keinerlei privilegierte Operation benötigen. Ein Queue-Consumer, der Bestelldaten verarbeitet oder Dateien in var/import schreibt, braucht keinen Root-Zugriff, sondern lediglich Lese- und Schreibrechte auf die Magento-Installation. Das Prinzip der geringsten Rechte reduziert den Schaden erheblich, falls eine Sicherheitslücke im Code oder einer Abhängigkeit ausgenutzt wird.
Praktisch bedeutet das: denselben Benutzer verwenden, unter dem auch php-fpm und die Webserver-Prozesse laufen, typischerweise www-data oder ein dedizierter magento-User. User=magento und Group=magento in der Unit-Datei stellen sicher, dass Dateiberechtigungen konsistent bleiben und der Worker dieselben Zugriffsrechte hat wie der Rest der Anwendung, keine mehr und keine weniger. Zusätzlich lässt sich mit NoNewPrivileges=true und ProtectSystem=strict die Angriffsfläche weiter einschränken, ohne die Funktion des Workers zu beeinträchtigen.
6. Umgebungsvariablen mit EnvironmentFile einbinden
PHP-Anwendungen lesen Konfiguration häufig aus Umgebungsvariablen, etwa Datenbank-Zugangsdaten, API-Keys oder Feature-Flags. Eine interaktive Shell lädt diese aus ~/.bashrc oder einer .env-Datei, ein systemd-Service kennt diese Dateien jedoch nicht automatisch. Fehlt die passende Konfiguration in der Unit-Datei, startet der Worker mit leeren Variablen und schlägt oft erst nach Sekunden mit einer kryptischen Datenbankfehlermeldung fehl, obwohl derselbe Befehl in der Shell manuell einwandfrei läuft.
Die Direktive EnvironmentFile verweist auf eine separate Datei im KEY=VALUE-Format, die systemd vor dem Start einliest und als Umgebungsvariablen an den Prozess weitergibt. Alternativ funktioniert Environment=KEY=VALUE direkt in der Unit-Datei für einzelne Werte, für mehrere zusammengehörige Variablen ist eine separate Datei jedoch übersichtlicher und lässt sich unabhängig von der Unit-Datei mit restriktiveren Dateirechten schützen, etwa chmod 600 für Zugangsdaten.
# /etc/mironsoft/queue-worker.env
# File permissions: chmod 600, owner magento:magento
MAGENTO_MODE=production
PHP_MEMORY_LIMIT=756M
QUEUE_MAX_MESSAGES=10000
DATABASE_HOST=127.0.0.1
DATABASE_NAME=magento
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# Referenced in the unit file via:
# EnvironmentFile=/etc/mironsoft/queue-worker.env
7. Service aktivieren, starten und testen
Nachdem die Unit-Datei unter /etc/systemd/system/ liegt, muss systemd zunächst über die neue Datei informiert werden. systemctl daemon-reload liest alle Unit-Dateien neu ein, ein Schritt, der nach jeder Änderung an einer Unit-Datei zwingend notwendig ist und in der Praxis der häufigste vergessene Befehl beim Debugging ist: Ohne daemon-reload verwendet systemd weiterhin die alte, zwischengespeicherte Konfiguration, selbst wenn die Datei auf der Platte bereits aktualisiert wurde.
systemctl enable --now mironsoft-queue-worker.service aktiviert den Service für zukünftige Boots und startet ihn sofort in einem Schritt. Der Status-Befehl zeigt danach, ob der Prozess läuft, seit wann, unter welcher PID und mit welchem Exit-Code er zuletzt beendet wurde. Ein Service im Zustand activating (auto-restart) deutet auf ein Problem im Startvorgang hin, meist einen Fehler im ExecStart-Pfad, fehlende Berechtigungen oder eine nicht erreichbare Datenbank.
# Unit-Datei bekannt machen und Service aktivieren
sudo cp mironsoft-queue-worker.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mironsoft-queue-worker.service
# Status prüfen
sudo systemctl status mironsoft-queue-worker.service --no-pager
# Beispielausgabe
● mironsoft-queue-worker.service - Mironsoft Magento Queue Consumer (async.operations.all)
Loaded: loaded (/etc/systemd/system/mironsoft-queue-worker.service; enabled)
Active: active (running) since Sun 2026-07-12 08:14:02 UTC; 3min ago
Main PID: 48213 (php)
Tasks: 4 (limit: 4678)
Memory: 41.2M
CPU: 2.108s
# Nach jeder Aenderung an der Unit-Datei erneut ausfuehren
sudo systemctl daemon-reload
sudo systemctl restart mironsoft-queue-worker.service
8. Logging mit journald: automatisch statt manuell
Solange StandardOutput und StandardError nicht explizit umgeleitet werden, schreibt systemd jede Ausgabe eines Services automatisch in journald, den zentralen Logging-Dienst von systemd. Ein PHP-Worker muss dafür nichts Besonderes tun, jedes echo, jede unbehandelte Exception mit Stacktrace und jede Warnung landet ohne zusätzliche Konfiguration im Journal, inklusive Zeitstempel, PID und Zuordnung zur richtigen Unit. Das ersetzt manuelles Logfile-Handling mit eigener Rotation vollständig für die meisten Anwendungsfälle.
journalctl -u mironsoft-queue-worker -f folgt den Logs live, ähnlich wie tail -f, aber mit strukturierten Metadaten. Zeitfenster lassen sich präzise eingrenzen, Ausgaben nach Priorität filtern und für die Weiterverarbeitung in andere Systeme als JSON exportieren. Für produktive Setups mit mehreren Workern empfiehlt sich zusätzlich, das Journal dauerhaft persistent zu speichern statt nur im Arbeitsspeicher zu halten, über Storage=persistent in /etc/systemd/journald.conf, sonst gehen Logs bei einem Server-Neustart verloren.
# Logs eines einzelnen Services live verfolgen
journalctl -u mironsoft-queue-worker -f
# Zeitfenster eingrenzen
journalctl -u mironsoft-queue-worker --since "2026-07-12 08:00" --until "2026-07-12 09:00"
# Nur Fehler und kritische Meldungen anzeigen
journalctl -u mironsoft-queue-worker -p err
# Strukturiert als JSON exportieren, z.B. für Log-Shipping
journalctl -u mironsoft-queue-worker -o json-pretty -n 20
# Logs seit dem letzten Boot
journalctl -u mironsoft-queue-worker -b
9. Häufige Fehler bei Unit-Dateien im Vergleich
Die meisten Probleme mit selbst geschriebenen Unit-Dateien folgen wiederkehrenden Mustern. Ein Service, der lokal manuell in der Shell einwandfrei läuft, aber unter systemd sofort mit failed abbricht, hat fast immer eine dieser Ursachen: fehlendes Arbeitsverzeichnis, relative Pfade oder fehlende Umgebungsvariablen, die in der interaktiven Shell stillschweigend vorhanden waren.
Die folgende Übersicht zeigt die häufigsten Fehlerquellen und die jeweils korrekte Lösung, damit ein neuer Worker beim ersten Versuch startet, statt über mehrere Debugging-Runden mit journalctl mühsam eingegrenzt werden zu müssen.
| Fehlerquelle | Falsch | Richtig | Auswirkung |
|---|---|---|---|
| Arbeitsverzeichnis | kein WorkingDirectory |
WorkingDirectory=/var/www/html |
Relative Pfade im Code brechen sonst mit "file not found" |
| ExecStart-Pfad | ExecStart=php worker.php |
ExecStart=/usr/bin/php /var/www/html/worker.php |
Kein PATH ohne Login-Shell, Start scheitert sofort |
| Umgebungsvariablen | keine EnvironmentFile |
EnvironmentFile=/etc/mironsoft/worker.env |
Kryptische DB-Fehler statt klarer Konfigurationsfehler |
| Ausführender Benutzer | läuft implizit als root |
User=magento, Group=magento |
Unnötige Rechte, größere Angriffsfläche |
| Neustart nach Crash | Restart=no (Standard) |
Restart=always, RestartSec=5 |
Worker bleibt nach Absturz dauerhaft inaktiv |
Mironsoft
systemd-Services, Queue-Worker und Deployment-Infrastruktur für PHP und Magento
PHP-Worker, die zuverlässig im Hintergrund laufen?
Wir schreiben und härten systemd-Unit-Dateien für eure Queue-Consumer und Worker-Prozesse, mit korrekter Restart-Policy, dediziertem Service-User und journald-Integration, produktionsreif und ohne Zusatztools.
Unit-Dateien schreiben
ExecStart, Restart-Policy und Ressourcenlimits sauber konfiguriert
Security-Hardening
Dedizierte Service-User, NoNewPrivileges und ProtectSystem
Logging & Monitoring
journald-Persistenz, Alerting bei failed-Zuständen
10. Zusammenfassung
Eigene systemd-Services für PHP-Anwendungen lösen ein Problem, das mit nohup, screen oder tmux strukturell nicht lösbar ist: dauerhafte Verfügbarkeit über Neustarts und Abstürze hinweg. Eine Unit-Datei mit ExecStart, das auf absolute Pfade verweist, Restart=always mit sinnvollem RestartSec, einem dedizierten User statt root und einer EnvironmentFile für Konfigurationswerte deckt die meisten Anforderungen an einen produktionsreifen PHP-Worker ab.
Der größte Vorteil gegenüber Cronjobs oder manuell gestarteten Hintergrundprozessen liegt in der automatischen journald-Integration: Kein zusätzliches Logfile-Handling, keine eigene Rotation, keine verlorenen Fehlermeldungen. Wer die in diesem Artikel gezeigten Muster konsequent auf alle PHP-Worker eines Projekts anwendet, bekommt einheitliche, beobachtbare und robuste Hintergrundprozesse, die sich mit denselben Werkzeugen wie jeder andere systemd-Dienst überwachen lassen.
Eigene systemd-Services für PHP-Anwendungen: Das Wichtigste auf einen Blick
ExecStart
Immer absolute Pfade für Binary und Skript, kein PATH ohne Login-Shell verfügbar.
Restart-Policy
Restart=always mit RestartSec und StartLimitBurst gegen Endlosschleifen.
User & Group
Dedizierter Service-User statt root, gleiche Rechte wie php-fpm.
journald-Logging
Automatisch ohne Zusatzkonfiguration, filterbar über journalctl -u.