Die Verwaltung von Diensten auf modernen Linux-Distributionen bedeutet die Arbeit mit systemd, dem Init-System und Dienstmanager, der zum Standard auf den meisten Distributionen geworden ist. Dieser Leitfaden behandelt alles, was Sie über das Erstellen benutzerdefinierter Service-Units, das Verwalten von Abhängigkeiten zwischen Diensten, die Analyse von Logs mit journalctl, den Ersatz von Cron durch systemd-Timer und die Diagnose von Problemen wissen müssen, wenn Dienste nicht starten oder unerwartet abstürzen.
Voraussetzungen
- Eine Linux-Distribution mit systemd (Ubuntu 16.04+, CentOS 7+, Debian 8+, Fedora 15+)
- Terminalzugang mit sudo-Privilegien
- Grundlegendes Verständnis von Linux-Dateiberechtigungen und Prozessverwaltung
- Ein installierter Texteditor (vim, nano oder ähnlich)
Systemd-Units verstehen
Systemd organisiert alles in Units — Ressourcen, die das System verwalten kann. Die häufigsten Unit-Typen sind:
| Unit-Typ | Erweiterung | Zweck |
|---|---|---|
| Service | .service | Daemon-Prozesse und Einmalaufgaben |
| Timer | .timer | Geplante Ausführung (ersetzt Cron) |
| Socket | .socket | IPC- und Netzwerk-Socket-Aktivierung |
| Mount | .mount | Dateisystem-Mountpunkte |
| Target | .target | Gruppen von Units (wie Runlevels) |
Unit-Dateien befinden sich an drei Orten mit absteigender Priorität:
/etc/systemd/system/ # Benutzerdefinierte Admin-Units (höchste Priorität)
/run/systemd/system/ # Laufzeit-Units
/lib/systemd/system/ # Distributions-Standard-Units
Um alle geladenen Units und ihre Zustände aufzulisten:
systemctl list-units --type=service
systemctl list-units --type=service --state=failed
Erstellen benutzerdefinierter Service-Units
Eine gut strukturierte Service-Unit-Datei hat drei Abschnitte. Hier ist ein vollständiges Beispiel für eine Node.js-Anwendung:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
Documentation=https://example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStartPre=/usr/bin/node --check /opt/myapp/server.js
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Sicherheitshärtung
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Wichtige Direktiven im Abschnitt [Service]:
- Type:
simple(Standard, Prozess im Vordergrund),forking(für Daemons die forken),oneshot(läuft einmal und beendet sich),notify(signalisiert Bereitschaft über sd_notify) - ExecStartPre: Befehl zur Validierung vor dem Start des Hauptprozesses
- Restart:
on-failure,always,on-abnormaloderno - RestartSec: Verzögerung zwischen Neustartversuchen in Sekunden
Nach dem Erstellen der Datei laden und starten Sie sie:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service
Verwaltung von Abhängigkeiten
Systemd verwendet mehrere Direktiven um Beziehungen zwischen Units auszudrücken:
[Unit]
# Reihenfolge (wann starten, nicht ob)
After=network-online.target # Nach dem Netzwerk starten
Before=nginx.service # Vor nginx starten
# Abhängigkeitsstärke
Requires=postgresql.service # Harte Abhängigkeit — scheitert wenn Postgres scheitert
Wants=redis.service # Weiche Abhängigkeit — fährt fort wenn Redis scheitert
BindsTo=docker.service # Verknüpfter Lebenszyklus — stoppt wenn Docker stoppt
# Konfliktlösung
Conflicts=iptables.service # Kann nicht gleichzeitig mit iptables laufen
Der Unterschied zwischen After und Requires ist entscheidend. After steuert die Reihenfolge (Sequenzierung) während Requires die Aktivierung steuert (ob eine Abhängigkeit einbezogen wird). Normalerweise benötigen Sie beides:
Requires=postgresql.service
After=postgresql.service
Requires allein garantiert keine Reihenfolge — beide Dienste können gleichzeitig starten. After allein zieht die Abhängigkeit nicht ein — es ordnet sie nur, wenn beide gerade starten.
Um den Abhängigkeitsbaum eines Dienstes zu visualisieren:
systemctl list-dependencies myapp.service
systemctl list-dependencies myapp.service --reverse
Loganalyse mit Journalctl
Journalctl ist das Werkzeug zur Abfrage des systemd-Journals. Hier sind die nützlichsten Muster:
# Logs eines bestimmten Dienstes in Echtzeit verfolgen
journalctl -u myapp.service -f
# Logs seit dem letzten Boot anzeigen
journalctl -u myapp.service -b
# Nach Zeitbereich filtern
journalctl -u myapp.service --since "2026-02-20 08:00" --until "2026-02-20 18:00"
# Nur Fehler und höher anzeigen
journalctl -u myapp.service -p err
# Ausgabe im JSON-Format zur Verarbeitung
journalctl -u myapp.service -o json-pretty --no-pager
# Kernel-Nachrichten bezüglich OOM-Kills anzeigen
journalctl -k --grep="Out of memory"
# Speichernutzung des Journals prüfen
journalctl --disk-usage
# Alte Logs bereinigen um Speicher freizugeben
sudo journalctl --vacuum-time=7d
sudo journalctl --vacuum-size=500M
Die Prioritätsstufen folgen der Syslog-Konvention: emerg (0), alert (1), crit (2), err (3), warning (4), notice (5), info (6), debug (7). Mit -p err werden err und alles Schwerwiegendere angezeigt.
Um persistente Journal-Speicherung zu konfigurieren (überlebt Neustarts):
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
Systemd-Timer
Systemd-Timer ersetzen Cron mit besserer Protokollierung, Abhängigkeitsverwaltung und Zuverlässigkeit. Ein Timer besteht aus zwei Dateien — der .timer und dem zugehörigen .service:
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily database backup timer
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=900
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Database backup job
[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal
Gängige OnCalendar-Ausdrücke:
| Ausdruck | Bedeutung |
|---|---|
*-*-* 02:00:00 | Täglich um 2 Uhr morgens |
Mon *-*-* 09:00:00 | Jeden Montag um 9 Uhr |
*-*-01 00:00:00 | Erster Tag jedes Monats |
*-*-* *:00/15:00 | Alle 15 Minuten |
hourly | Jede Stunde (Kurzform) |
Timer aktivieren und verwalten:
sudo systemctl enable --now backup.timer
systemctl list-timers --all
systemctl status backup.timer
Die Direktive Persistent=true stellt sicher, dass der Job sofort beim nächsten Start ausgeführt wird, wenn das System ausgeschaltet war als der Timer hätte auslösen sollen.
Vergleich: Systemd vs SysVinit vs Upstart
| Eigenschaft | systemd | SysVinit | Upstart |
|---|---|---|---|
| Paralleler Start | Ja | Nein | Teilweise |
| Abhängigkeitsverwaltung | Deklarative Direktiven | Manuelle Nummerierung | Ereignisbasiert |
| Dienstüberwachung | Integrierte Neustartrichtlinien | Keine (externe Tools nötig) | Respawn-Direktive |
| Protokollierung | journald (strukturiert, indexiert) | syslog (Klartextdateien) | syslog |
| Socket-Aktivierung | Ja | Nein | Nein |
| Ressourcenkontrolle | Cgroups-Integration | Keine | Keine |
| Timer/Planung | Integrierte Timer | Benötigt Cron | Benötigt Cron |
| Konfigurationsformat | INI-artige Unit-Dateien | Shell-Skripte | Stanza-basierte Conf-Dateien |
| Boot-Analyse | systemd-analyze | Keine | Keine |
| Verbreitungsstatus | Standard auf den meisten Distros | Legacy-Systeme | Eingestellt |
Praxisbeispiel
Sie haben einen Produktionsserver, der eine Python-API ausführt, die von PostgreSQL und Redis abhängt. Die API muss nach beiden Datenbanken starten, bei Abstürzen mit einer Back-off-Strategie neu starten und alle 6 Stunden einen Bereinigungsjob ausführen.
Service-Unit (/etc/systemd/system/api.service):
[Unit]
Description=Production Python API
After=network-online.target postgresql.service redis.service
Requires=postgresql.service
Wants=redis.service
[Service]
Type=simple
User=apiuser
WorkingDirectory=/opt/api
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/api/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 app:create_app()
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=5
[Install]
WantedBy=multi-user.target
Bereinigungs-Timer (/etc/systemd/system/api-cleanup.timer):
[Unit]
Description=API cleanup every 6 hours
[Timer]
OnBootSec=15min
OnUnitActiveSec=6h
Persistent=true
[Install]
WantedBy=timers.target
Diese Konfiguration stellt sicher, dass die API nur startet, wenn PostgreSQL läuft (harte Abhängigkeit) und optional Redis nutzt (weiche Abhängigkeit). Die Neustartrichtlinie erlaubt 5 Neustartversuche innerhalb von 5 Minuten bevor aufgegeben wird.
Stolperfallen und Sonderfälle
- ExecStart muss absolute Pfade verwenden: Relative Pfade verursachen stille Fehler. Verwenden Sie immer
/usr/bin/node, nichtnode - Type=forking braucht PIDFile: Wenn Ihr Daemon forkt, muss systemd die Kind-PID verfolgen. Setzen Sie
PIDFile=/run/myapp.pidund stellen Sie sicher, dass Ihre App sie schreibt - Environment-Dateien vs inline: Für viele Variablen verwenden Sie
EnvironmentFile=/etc/myapp/envstatt mehrererEnvironment=-Zeilen. Das Format istKEY=value(ohneexport) - Benutzer-Services vs System-Services: Benutzer-Units in
~/.config/systemd/user/laufen ohne sudo, aber nur solange der Benutzer angemeldet ist (es sei dennloginctl enable-lingerist gesetzt) - Reload vs restart vs daemon-reload:
systemctl restart myappstartet den Prozess neu.systemctl daemon-reloadlädt Unit-Dateidefinitionen neu. Nach Bearbeitung einer Unit-Datei brauchen Sie zuerstdaemon-reload - Das WantedBy-Target ist wichtig: Verwenden Sie
multi-user.targetfür Server ohne GUI,graphical.targetfür Desktop-Dienste
Fehlerbehebung
Wenn ein Dienst nicht startet, folgen Sie diesem Diagnose-Workflow:
# 1. Aktuellen Status und aktuelle Logs prüfen
systemctl status myapp.service
# 2. Detaillierte Fehlerinformationen abrufen
journalctl -u myapp.service -e --no-pager -n 50
# 3. Unit-Dateisyntax validieren
systemd-analyze verify /etc/systemd/system/myapp.service
# 4. Auf Reihenfolgezyklen prüfen
systemd-analyze critical-chain myapp.service
# 5. ExecStart-Befehl manuell als Dienstbenutzer testen
sudo -u appuser /usr/bin/node /opt/myapp/server.js
# 6. SELinux- oder AppArmor-Ablehnungen prüfen
sudo ausearch -m avc -ts recent 2>/dev/null || sudo journalctl -k --grep="apparmor"
# 7. Dateiberechtigungen überprüfen
ls -la /opt/myapp/
namei -l /opt/myapp/server.js
Häufige Fehlerursachen und Lösungen:
| Symptom | Ursache | Lösung |
|---|---|---|
code=exited, status=203/EXEC | Binärdatei nicht gefunden oder nicht ausführbar | Pfad mit which prüfen, chmod +x setzen |
code=exited, status=217/USER | Der in User= angegebene Benutzer existiert nicht | Benutzer mit useradd -r -s /sbin/nologin erstellen |
Start request repeated too quickly | Neustartschleife hat StartLimitBurst erreicht | RestartSec erhöhen, den Absturz beheben, dann systemctl reset-failed |
code=exited, status=200/CHDIR | WorkingDirectory existiert nicht | Verzeichnis erstellen und Eigentümer korrigieren |
| Dienst startet aber Port nicht erreichbar | Firewall oder falsche Bind-Adresse | Mit ss -tlnp prüfen, firewall-cmd oder ufw-Regeln verifizieren |
Zusammenfassung
- Systemd-Unit-Dateien verwenden deklarative INI-artige Abschnitte (
[Unit],[Service],[Install]) um das Dienstverhalten zu definieren - Benutzerdefinierte Units gehören nach
/etc/systemd/system/— führen Sie immerdaemon-reloadnach Änderungen aus - Verwenden Sie
Requires=zusammen mitAfter=für harte Abhängigkeiten mit korrekter Reihenfolge - Journalctl bietet leistungsstarke Filterung nach Unit, Priorität, Zeitbereich und Ausgabeformat
- Systemd-Timer bieten bessere Zuverlässigkeit als Cron mit persistenter Planung und Protokollierung pro Aufgabe
- Sicherheitshärtungs-Direktiven wie
ProtectSystem=strictundPrivateTmp=truesollten in Produktions-Units Standard sein - Diagnostizieren Sie mit
systemctl status,journalctl -uundsystemd-analyze verify