Systemd Service Units: Linux-Dienste richtig verwalten
Wenn ich eine Node-App, ein Python-Script oder irgendeinen sonstigen Long-Running-Prozess auf einem Server starten will, ist systemd meine erste Wahl. Nicht PM2, nicht Docker, nicht supervisord. Der Grund ist banal: systemd ist auf jedem aktuellen Linux-System schon da, das Logging integriert sich mit journalctl, und Auto-Restart, Resource-Limits, Dependencies sind alles One-Liner in der Unit-Datei.
PM2 ist okay, wenn man im Node-Universum lebt und einen Prozess-Manager mit Web-Interface will. Aber für alles andere — und sobald mehrere Sprachen im Spiel sind — ist systemd die robuste Wahl.
Eine simple Unit für eine Node-App
/etc/systemd/system/portfolio.service:
[Unit]
Description=Portfolio Next.js
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/root/portfolio
EnvironmentFile=/root/portfolio/.env.local
ExecStart=/usr/bin/node node_modules/.bin/next start -p 3003
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Aktivieren:
systemctl daemon-reload
systemctl enable --now portfolio.service
Und das war's. Der Service startet beim Boot, restartet bei Crashes, loggt nach journalctl -u portfolio.service.
Was die einzelnen Felder tun
Type=simple ist der Default und passt für die meisten modernen Apps. Der Prozess läuft im Vordergrund — systemd sieht den Hauptprozess als den ExecStart-Prozess. Andere Typen (forking, notify, oneshot) sind für spezielle Fälle.
Restart=on-failure — restartet nur bei Exit-Code != 0. Restart=always würde auch bei sauberen Exits restarten, was selten gewollt ist.
RestartSec=5 — wartet 5 Sekunden vor dem Restart. Verhindert Restart-Loops, die das System fluten.
EnvironmentFile= — lädt Variablen aus einer Datei. Ich nutze das für Secrets, statt sie direkt in die Unit zu schreiben.
After=network.target — startet nach dem Netzwerk. Bei Apps, die DB-Verbindungen brauchen: After=postgresql.service.
Logging mit journalctl
journalctl -u portfolio.service # alles
journalctl -u portfolio.service -f # follow (wie tail -f)
journalctl -u portfolio.service --since "1 hour ago"
journalctl -u portfolio.service -p err # nur Errors
journalctl -u portfolio.service -n 100 # letzte 100 Zeilen
journalctl ist mächtiger als reines tail -f über eine Logdatei. Strukturiert, filterbar, persistent über Reboots.
Ressourcen-Limits
Ein Node-Prozess, der unbeschränkt RAM nimmt, kann den Server killen. systemd kann das beschränken:
[Service]
MemoryMax=512M
CPUQuota=50%
TasksMax=100
MemoryMax killt den Prozess, wenn er die Grenze überschreitet (OOMKill). Bei MemoryHigh wird er nur throttled. Für Hobby-Apps reicht MemoryMax.
Security-Hardening
systemd hat eine ganze Reihe von Optionen, die den Service-Prozess in eine Art Sandbox stecken:
[Service]
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/myapp /var/lib/myapp
ProtectSystem=strict macht das gesamte Filesystem read-only, bis auf explizite Ausnahmen via ReadWritePaths. Eine kompromittierte App kann nichts überschreiben, was sie nicht explizit darf.
Diese Optionen sind underused. Sie kosten nichts (außer 5 Zeilen Konfiguration) und bringen echten Schutz.
Timer statt Cron
Für periodische Aufgaben nutze ich systemd-Timer statt Cron — bessere Logging-Integration und einfacheres Debugging.
/etc/systemd/system/backup.service:
[Unit]
Description=Daily Backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
/etc/systemd/system/backup.timer:
[Unit]
Description=Daily Backup Timer
[Timer]
OnCalendar=daily
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true heißt: wenn der Timer mal nicht laufen konnte (Server war aus), wird er beim nächsten Start nachgeholt. Das macht Cron nicht.
systemctl enable --now backup.timer aktiviert ihn. systemctl list-timers zeigt, wann er nächstes Mal feuert.
Häufige Fehler
Vergessen daemon-reload nach Änderungen. systemd cached Unit-Dateien — neue Änderungen werden nur nach systemctl daemon-reload aktiv.
Pfade als relative Pfade. ExecStart braucht absoluten Pfad. node reicht nicht, /usr/bin/node ja.
Falscher User. Wenn der User keine Rechte hat, auf das WorkingDirectory zuzugreifen oder den Port zu binden, scheitert der Start kommentarlos. journalctl -u service.name zeigt's, aber man muss hingucken.
Was systemd nicht ist
Kein Prozess-Manager fürs Frontend mit Real-Time-Charts. Kein Cluster-Tool. Kein Auto-Scaler. Es startet Prozesse, hält sie am Leben, loggt was sie ausgeben. Das reicht für 95 % aller Server-Workloads.
Wer mehr braucht, nimmt Kubernetes. Aber bis dahin: systemd, immer.