TL;DR — Kurzzusammenfassung
Systemd-Dienste unter Linux meistern: Unit-File-Aufbau, Node.js- und Python-Beispiele, Neustart-Richtlinien, Ressourcenlimits und Sicherheitshaertung.
Langlebige Prozesse auf einem Linux-Server am Laufen zu halten erforderte fruher komplexe SysVinit-Skripte, Supervisor-Konfigurationen oder fragile Shell-Hacks. systemd hat das alles veraendert. Als Standard-Init-System auf allen grossen Linux-Distributionen — Ubuntu, Debian, RHEL, Fedora, Arch — bietet systemd eine einheitliche, deklarative Moeglichkeit, genau zu definieren, wie jeder Prozess gestartet, gestoppt, neugestartet, protokolliert und mit dem Rest des Systems interagiert werden soll.
Dieser Leitfaden behandelt alles, was Sie benoetigen, um produktionsreife eigene systemd-Dienste zu erstellen: den Aufbau einer Unit-Datei, konkrete Beispiele fuer Node.js- und Python-Anwendungen, Diensttypen, Neustart-Richtlinien, Ressourcenlimits, Sicherheitsdirektiven, Socket-Aktivierung, Timer-Units als Cron-Alternative und das Debuggen von Fehlern mit journalctl.
Voraussetzungen
Stellen Sie vor Beginn sicher, dass Sie Folgendes haben:
- Ein Linux-System mit systemd (Ubuntu 16.04+, Debian 8+, CentOS 7+, RHEL 7+, Fedora 15+ oder eine moderne Distribution)
- Ein Terminal mit
sudo- oder Root-Zugriff - Grundlegende Vertrautheit mit der Kommandozeile und einem Texteditor (
nanoodervim) - Eine Anwendung, die als Dienst ausgefuehrt werden soll (Node.js, Python, Go-Binary usw.)
Pruefen Sie, ob systemd laeuft:
systemctl --version
# Sollte ausgeben: systemd 249 (oder aehnliche Versionsnummer)
Aufbau der Unit-Datei
Jeder von systemd verwaltete Dienst wird durch eine Unit-Datei beschrieben — eine einfache Textkonfigurationsdatei im INI-Format. Benutzerdefinierte Dienst-Unit-Dateien befinden sich in /etc/systemd/system/. Der Dateiname endet auf .service.
Eine vollstaendige Unit-Datei hat drei Abschnitte:
[Unit]
Description=Mein Anwendungsdienst
Documentation=https://beispiel.com/docs
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=meinapp
Group=meinapp
WorkingDirectory=/opt/meinapp
ExecStart=/usr/bin/node /opt/meinapp/server.js
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Der [Unit]-Abschnitt
| Direktive | Zweck |
|---|---|
Description= | Lesbarer Name in der systemctl status-Ausgabe |
After= | Startet diese Unit nach den aufgelisteten Units |
Wants= | Weiche Abhaengigkeit — aufgelistete Units werden gestartet, Fehler toleriert |
Requires= | Harte Abhaengigkeit — wenn die aufgelistete Unit fehlschlaegt, schlaegt diese auch fehl |
BindsTo= | Wie Requires=, stoppt diese Unit aber auch wenn die Abhaengigkeit stoppt |
Der [Service]-Abschnitt
| Direktive | Zweck |
|---|---|
Type= | Prozess-Lebenszyklus-Modell (simple, forking, oneshot, notify) |
User= / Group= | Fuehrt den Prozess als diesen Benutzer/Gruppe aus |
ExecStart= | Der auszufuehrende Befehl (muss absoluter Pfad sein) |
ExecStartPre= | Befehle, die vor ExecStart ausgefuehrt werden |
Restart= | Wann automatisch neugestartet werden soll |
Environment= | Setzt Umgebungsvariablen direkt |
EnvironmentFile= | Laedt Umgebungsvariablen aus einer Datei |
StandardOutput= | Wohin stdout gesendet wird (journal, file:, null) |
Der [Install]-Abschnitt
| Direktive | Zweck |
|---|---|
WantedBy= | Welches Target diesen Dienst aktiviert (ueblicherweise multi-user.target) |
Alias= | Zusaetzliche Namen fuer diese Unit |
Einen Node.js-Dienst erstellen
Angenommen, Sie haben eine Node.js-API unter /opt/meinapi/server.js, die auf Port 3000 lauscht.
Schritt 1: Dedizierten Benutzer erstellen
sudo useradd --system --no-create-home --shell /usr/sbin/nologin nodeapi
sudo chown -R nodeapi:nodeapi /opt/meinapi
Schritt 2: Unit-Datei erstellen
sudo nano /etc/systemd/system/meinapi.service
[Unit]
Description=Mein Node.js API-Dienst
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=nodeapi
Group=nodeapi
WorkingDirectory=/opt/meinapi
EnvironmentFile=/etc/meinapi/environment
ExecStart=/usr/bin/node /opt/meinapi/server.js
ExecStartPre=/usr/bin/node --check /opt/meinapi/server.js
Restart=on-failure
RestartSec=5s
TimeoutStopSec=10s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=meinapi
MemoryMax=512M
CPUQuota=50%
LimitNOFILE=65536
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=/opt/meinapi/logs /var/lib/meinapi
[Install]
WantedBy=multi-user.target
Schritt 3: Umgebungsdatei erstellen
sudo mkdir -p /etc/meinapi
sudo nano /etc/meinapi/environment
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://benutzer:passwort@localhost/meindb
JWT_SECRET=ihr-geheimnis-hier
sudo chmod 600 /etc/meinapi/environment
Schritt 4: Aktivieren und starten
sudo systemctl daemon-reload
sudo systemctl enable meinapi.service
sudo systemctl start meinapi.service
sudo systemctl status meinapi.service
Einen Python-Dienst erstellen
Fuer einen Python-Worker unter /opt/worker/worker.py:
[Unit]
Description=Python-Hintergrund-Worker
After=network.target redis.service
Requires=redis.service
[Service]
Type=simple
User=pyworker
Group=pyworker
WorkingDirectory=/opt/worker
ExecStart=/opt/worker/venv/bin/python -u worker.py
EnvironmentFile=/etc/worker/environment
Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=60s
StartLimitBurst=3
StandardOutput=journal
StandardError=journal
MemoryMax=256M
CPUQuota=25%
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Das -u-Flag beim Python-Befehl deaktiviert die Ausgabepufferung, sodass Logs sofort in journald erscheinen.
Diensttypen
| Typ | Wann verwenden | Wie systemd die Bereitschaft verfolgt |
|---|---|---|
simple | Die meisten modernen Apps | Betrachtet die Unit als gestartet, sobald ExecStart einen Fork macht |
exec | Wie simple, aber wartet auf exec-Erfolg | Wartet bis das Binary erfolgreich ausgefuehrt wird |
forking | Traditionelle Daemons, die forken | Wartet bis der Elternprozess beendet ist |
oneshot | Skripte, die einmal laufen und beenden | Wartet auf ExecStart-Beendigung vor der Aktivierung |
notify | Apps, die Bereitschaftsbenachrichtigung senden | Wartet auf die Benachrichtigung vor der Aktivierung |
Neustart-Richtlinien und Ressourcenlimits
Neustart-Richtlinien
Restart=always # Neustart bei jeder Beendigung des Dienstes
Restart=on-failure # Neustart nur bei Fehler (nicht bei sauberem Exit)
Restart=on-abnormal # Neustart bei Fehler + Watchdog-Timeout + anormale Signale
Restart=no # Kein Neustart
Mit Ratenbegrenzung, um zu verhindern, dass ein abstuerzeender Dienst das System ueberlastet:
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=5
Ressourcenlimits
MemoryMax=1G # Hartes Speicherlimit — Prozess wird beendet wenn ueberschritten
MemoryHigh=800M # Weiches Speicherlimit — Kernel aktiviert Rueckgewinnung
MemorySwapMax=0 # Swap fuer diesen Dienst deaktivieren
CPUQuota=200% # Auf 2 volle CPU-Kerne begrenzen
LimitNOFILE=65536 # Maximale offene Dateideskriptoren
LimitNPROC=512 # Maximale Anzahl von Prozessen/Threads
Sicherheitshaertung
[Service]
NoNewPrivileges=true # Verhindert neue Berechtigungen via setuid/setgid
ProtectSystem=strict # Haengt /usr, /boot, /etc schreibgeschuetzt ein
PrivateTmp=true # Privates /tmp fuer diesen Dienst
DynamicUser=true # Dynamischer unprivilegierter Benutzer
ProtectHome=true # Blockiert Zugriff auf /home-Verzeichnisse
SystemCallFilter=@system-service # Filtert verfuegbare Systemaufrufe
ProtectKernelTunables=true # Verhindert Schreiben in Kernelvariablen
ProtectKernelModules=true # Verhindert Laden von Kernelmodulen
Analysieren Sie den Sicherheitsscore eines laufenden Dienstes:
systemd-analyze security meinapi.service
Timer-Units: systemd als Cron-Alternative
sudo nano /etc/systemd/system/naechtiches-backup.service
[Unit]
Description=Naechtliches Datenbank-Backup
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
sudo nano /etc/systemd/system/naechtiches-backup.timer
[Unit]
Description=Naechtliches Backup um 2 Uhr starten
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now naechtiches-backup.timer
systemctl list-timers --all
systemd vs. andere Prozessmanager
| Funktion | systemd | SysVinit / init.d | supervisord | PM2 |
|---|---|---|---|---|
| Im OS enthalten | Ja (alle grossen Distros) | Ja (Legacy) | Nein (pip install) | Nein (npm install) |
| Abhaengigkeitsreihenfolge | Vollstaendiger Graph | Manuelle Reihenfolge | Begrenzt | Nein |
| Socket-Aktivierung | Ja | Nein | Nein | Nein |
| Cgroup-Ressourcenlimits | Ja (nativ) | Nein | Nein | Nein |
| Kernel-Level-Sandbox | Ja (umfangreich) | Nein | Nein | Nein |
| Zentrales Logging | journald (strukturiert) | syslog / Dateien | Nur Dateien | PM2-Dateien |
| Geplante Jobs | Ja (Timer-Units) | Nein | Nein | Ja (Cron-aehnlich) |
| Node.js-Integration | Gut | Schlecht | Gut | Ausgezeichnet |
| Lernkurve | Moderat | Niedrig | Niedrig | Sehr niedrig |
Praxisbeispiel
Sie haben einen Produktionsserver mit einer Python-FastAPI-Anwendung, die Jobs aus einer Redis-Queue verarbeitet. Der Dienst soll bei Absturz automatisch neu starten und niemals mehr als 512 MB RAM verbrauchen.
[Unit]
Description=FastAPI Job Worker
After=network.target redis.service
Requires=redis.service
StartLimitIntervalSec=120s
StartLimitBurst=4
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/fastapi-worker
ExecStart=/opt/fastapi-worker/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2
EnvironmentFile=/etc/fastapi-worker/environment
Restart=on-failure
RestartSec=8s
TimeoutStartSec=30s
TimeoutStopSec=15s
StandardOutput=journal
SyslogIdentifier=fastapi-worker
MemoryMax=512M
CPUQuota=100%
LimitNOFILE=32768
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadWritePaths=/opt/fastapi-worker/data
[Install]
WantedBy=multi-user.target
Fallstricke und Sonderfaelle
ExecStart muss absoluter Pfad sein. Sie koennen keine Shell-Built-ins, Pipes oder Umleitungen direkt in ExecStart verwenden. Kapseln Sie komplexe Befehle in einem Skript.
Expansion von Umgebungsvariablen. systemd expandiert $VAR in ExecStart, wenn die Variable ueber Environment= oder EnvironmentFile= gesetzt wird, laedt aber KEINE Shell-Initialisierungsdateien. Ihr ~/.bashrc wird niemals geladen.
Prozentzeichen in Unit-Dateien. In Unit-Dateien ist % ein Spezifikatorzeichen. Um ein echtes Prozentzeichen zu verwenden, escapen Sie es als %%.
DynamicUser und Dateiberechtigungen. Bei DynamicUser=true aendert sich die zugeteilte UID bei jedem Neustart. Verwenden Sie Direktiven wie StateDirectory= oder LogsDirectory=, damit systemd persistente Verzeichnisse korrekt verwaltet.
After= vs. Requires=. After= kontrolliert nur die Reihenfolge — es erstellt keine Abhaengigkeit. Requires= erstellt die Abhaengigkeit, kontrolliert aber nicht die Reihenfolge. Sie wollen fast immer beide zusammen.
Fehlerbehebung
Dienst startet nicht
sudo systemctl status meinapi.service
journalctl -u meinapi.service -b
journalctl -u meinapi.service --since "10 minutes ago"
journalctl -u meinapi.service -f
Dienst startet, stuerzt aber sofort ab
journalctl -u meinapi.service -n 100 --no-pager
ls -la /usr/bin/node
sudo -u nodeapi /usr/bin/node /opt/meinapi/server.js
Fehlgeschlagenen Dienst zuruecksetzen
sudo systemctl reset-failed meinapi.service
sudo systemctl start meinapi.service
Sicherheitsexposition pruefen
systemd-analyze security meinapi.service
systemd-analyze verify /etc/systemd/system/meinapi.service
Zusammenfassung
- Unit-Dateien befinden sich in
/etc/systemd/system/und haben drei Abschnitte:[Unit],[Service]und[Install] - Fuehren Sie immer
systemctl daemon-reloadnach dem Bearbeiten einer Unit-Datei aus - Verwenden Sie
Type=simplefuer die meisten modernen Apps; verwenden SieType=notifyfuer Apps, die Bereitschaft signalisieren - Speichern Sie Geheimnisse in einer separaten
EnvironmentFile=mit Berechtigung600, nicht direkt in der Unit-Datei - Konfigurieren Sie Neustart-Richtlinien mit
Restart=on-failureundStartLimitBurstum vorueber gehende Fehler zu ueberstehen - Wenden Sie Sicherheitsdirektiven auf jeden Produktionsdienst an
- Ersetzen Sie Cron-Jobs durch Timer-Units fuer besseres Logging
- Debuggen Sie mit journalctl —
journalctl -u dienstname -fist Ihr erster Anlaufpunkt bei Fehlern