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-TypErweiterungZweck
Service.serviceDaemon-Prozesse und Einmalaufgaben
Timer.timerGeplante Ausführung (ersetzt Cron)
Socket.socketIPC- und Netzwerk-Socket-Aktivierung
Mount.mountDateisystem-Mountpunkte
Target.targetGruppen 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-abnormal oder no
  • 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:

AusdruckBedeutung
*-*-* 02:00:00Täglich um 2 Uhr morgens
Mon *-*-* 09:00:00Jeden Montag um 9 Uhr
*-*-01 00:00:00Erster Tag jedes Monats
*-*-* *:00/15:00Alle 15 Minuten
hourlyJede 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

EigenschaftsystemdSysVinitUpstart
Paralleler StartJaNeinTeilweise
AbhängigkeitsverwaltungDeklarative DirektivenManuelle NummerierungEreignisbasiert
DienstüberwachungIntegrierte NeustartrichtlinienKeine (externe Tools nötig)Respawn-Direktive
Protokollierungjournald (strukturiert, indexiert)syslog (Klartextdateien)syslog
Socket-AktivierungJaNeinNein
RessourcenkontrolleCgroups-IntegrationKeineKeine
Timer/PlanungIntegrierte TimerBenötigt CronBenötigt Cron
KonfigurationsformatINI-artige Unit-DateienShell-SkripteStanza-basierte Conf-Dateien
Boot-Analysesystemd-analyzeKeineKeine
VerbreitungsstatusStandard auf den meisten DistrosLegacy-SystemeEingestellt

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, nicht node
  • Type=forking braucht PIDFile: Wenn Ihr Daemon forkt, muss systemd die Kind-PID verfolgen. Setzen Sie PIDFile=/run/myapp.pid und stellen Sie sicher, dass Ihre App sie schreibt
  • Environment-Dateien vs inline: Für viele Variablen verwenden Sie EnvironmentFile=/etc/myapp/env statt mehrerer Environment=-Zeilen. Das Format ist KEY=value (ohne export)
  • Benutzer-Services vs System-Services: Benutzer-Units in ~/.config/systemd/user/ laufen ohne sudo, aber nur solange der Benutzer angemeldet ist (es sei denn loginctl enable-linger ist gesetzt)
  • Reload vs restart vs daemon-reload: systemctl restart myapp startet den Prozess neu. systemctl daemon-reload lädt Unit-Dateidefinitionen neu. Nach Bearbeitung einer Unit-Datei brauchen Sie zuerst daemon-reload
  • Das WantedBy-Target ist wichtig: Verwenden Sie multi-user.target für Server ohne GUI, graphical.target fü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:

SymptomUrsacheLösung
code=exited, status=203/EXECBinärdatei nicht gefunden oder nicht ausführbarPfad mit which prüfen, chmod +x setzen
code=exited, status=217/USERDer in User= angegebene Benutzer existiert nichtBenutzer mit useradd -r -s /sbin/nologin erstellen
Start request repeated too quicklyNeustartschleife hat StartLimitBurst erreichtRestartSec erhöhen, den Absturz beheben, dann systemctl reset-failed
code=exited, status=200/CHDIRWorkingDirectory existiert nichtVerzeichnis erstellen und Eigentümer korrigieren
Dienst startet aber Port nicht erreichbarFirewall oder falsche Bind-AdresseMit 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 immer daemon-reload nach Änderungen aus
  • Verwenden Sie Requires= zusammen mit After= 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=strict und PrivateTmp=true sollten in Produktions-Units Standard sein
  • Diagnostizieren Sie mit systemctl status, journalctl -u und systemd-analyze verify

Verwandte Artikel