Eigene systemd-Services für PHP-Anwendungen erstellen
$
/etc
Linux · systemd · PHP · DevOps
Eigene systemd-Services für PHP-Anwendungen erstellen
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.

16 Min. Lesezeit ExecStart · Restart-Policy · journald systemd 249+ · PHP 8.4 · Debian/Ubuntu

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.

11. FAQ: Eigene systemd-Services für PHP-Anwendungen

1Warum reicht ein Cronjob nicht für einen Queue-Consumer?
Cronjobs starten kurz und enden wieder. Ein Queue-Consumer muss dauerhaft laufen und auf Nachrichten warten. systemd hält den Prozess aktiv, startet nach Abstürzen neu und übernimmt das Logging.
2Wo müssen eigene Unit-Dateien abgelegt werden?
Unter /etc/systemd/system/. Nach jeder Änderung ist systemctl daemon-reload zwingend notwendig, sonst nutzt systemd weiter die alte Konfiguration.
3Warum funktioniert ein Befehl in der Shell, aber nicht als Service?
systemd startet ohne Login-Shell und ohne deren Umgebungsvariablen wie PATH. Relative Pfade und fehlende .env-Werte führen deshalb zu Fehlern, die in der Shell nicht auftreten.
4Was bewirkt Restart=always genau?
Neustart bei jedem Beenden, ob regulär, Crash oder Signal. RestartSec und StartLimitBurst halten das Verhalten trotzdem kontrolliert.
5Warum sollte ein PHP-Worker nicht als root laufen?
Ohne explizite User-Direktive läuft ein Service als root. Das vergrößert den Schaden bei einer Sicherheitslücke. Ein dedizierter Service-User mit den Rechten von php-fpm reicht aus.
6Wie kommen Umgebungsvariablen in einen systemd-Service?
Über EnvironmentFile mit einer separaten KEY=VALUE-Datei oder Environment=KEY=VALUE direkt in der Unit-Datei. Eine .env-Datei im Projekt wird nicht automatisch gelesen.
7Landen PHP-Logs automatisch in journald?
Ja, solange StandardOutput/StandardError nicht umgeleitet werden. Jede Ausgabe inklusive Exceptions landet strukturiert im Journal, abrufbar über journalctl -u.
8Wie teste ich, ob die Restart-Policy funktioniert?
Mit kill -9 auf die Haupt-PID den Prozess hart beenden und mit journalctl -u <service> -f beobachten, ob systemd innerhalb von RestartSec neu startet.
9Was bedeutet activating (auto-restart)?
Der Service startet wiederholt und scheitert kurz danach. Typische Ursachen: falscher ExecStart-Pfad, fehlende Berechtigungen oder nicht erreichbare Datenbank/Redis-Verbindung.
10Warum sollte ich WorkingDirectory explizit setzen?
Ohne WorkingDirectory startet der Prozess im Root-Verzeichnis. Relative Pfade im Code laufen dann ins Leere und erzeugen schwer nachvollziehbare file-not-found-Fehler.