Bash-Skripte richtig als Daemon betreiben
AI generated
$_
#!/
Bash · Daemon · setsid · systemd
Bash-Skripte richtig als Daemon betreiben
von setsid und PID-Dateien bis zur systemd-Unit

Ein Skript mit & im Hintergrund zu starten macht noch keinen Daemon. Ein echter Daemon löst sich vollständig vom Terminal, verhindert Mehrfachstart über eine PID-Datei, reagiert kontrolliert auf Signale wie SIGTERM und SIGHUP, und schreibt sein Log an eine Stelle, die auch ohne Terminal erreichbar bleibt.

18 Min. Lesezeit setsid · PID-Datei · trap · systemd Bash 4.x · 5.x · Linux

1. Was einen Daemon von einem Hintergrundskript unterscheidet

Ein Daemon ist ein Prozess, der dauerhaft und unabhängig von jedem Terminal im Hintergrund läuft, typischerweise für die gesamte Laufzeit des Systems oder bis er explizit beendet wird. Ein einfaches ./skript.sh & ist noch kein Daemon, weil der Prozess weiterhin an das startende Terminal gebunden bleibt und bei dessen Schließen ein SIGHUP erhält, das ihn in den meisten Fällen sofort beendet.

Ein echter Daemon braucht mehrere Eigenschaften gleichzeitig: vollständige Trennung vom Terminal, eine neue Sitzung ohne Controlling-Terminal, umgeleitete Standard-Datenströme, ein bekanntes Arbeitsverzeichnis, und meist eine PID-Datei, über die andere Prozesse ihn identifizieren und steuern können. Keine dieser Eigenschaften entsteht automatisch durch einfaches Starten im Hintergrund, sie müssen im Skript selbst implementiert werden.

In der Praxis stellt sich oft die Frage, ob man einen Daemon in reinem Bash von Hand baut oder auf systemd setzt, das viele dieser Aufgaben übernimmt. Dieser Artikel zeigt beide Wege: die klassische manuelle Daemonisierung für Umgebungen ohne systemd oder für portable Skripte, und den Übergang zu systemd als robustere moderne Alternative.

2. Double-Fork und setsid im Detail

Die klassische Technik zur Daemonisierung eines Prozesses ist der Double-Fork, kombiniert mit setsid. Der erste fork erzeugt einen Kindprozess und beendet den Elternprozess sofort, wodurch das Kind zum Waisen wird und von init beziehungsweise systemd adoptiert wird. setsid im Kind erzeugt eine neue Sitzung, in der der Prozess zum Sitzungsführer wird, aber noch kein Controlling-Terminal besitzt. Ein zweiter fork verhindert, dass der Prozess später versehentlich ein Controlling-Terminal erhält, weil nur Sitzungsführer eines erwerben können und der zweite Fork-Nachkomme keiner ist.

In Bash lässt sich dieser klassische Double-Fork-Ansatz nachbilden, wobei setsid als externes Kommando meist einfacher ist als der reine fork-Ansatz, weil Bash selbst kein direktes fork-Builtin anbietet. setsid befehl startet den Befehl in einer neuen Sitzung ohne Terminal, was für die meisten Daemon-Anwendungsfälle bereits ausreicht, ohne die volle Komplexität eines echten Double-Forks in C nachzubilden.


#!/usr/bin/env bash
set -euo pipefail

daemonize() {
  local cmd="$1"
  local pidfile="$2"
  local logfile="$3"

  # setsid detaches from the controlling terminal and starts a new session
  setsid bash -c "
    exec >> '$logfile' 2>&1
    exec < /dev/null
    echo \$\$ > '$pidfile'
    exec $cmd
  " &

  disown
  echo "Daemon started, see $logfile"
}

daemonize "./worker-loop.sh" "/var/run/myapp.pid" "/var/log/myapp.log"

Entscheidend im Beispiel ist die Kombination aus setsid für die neue Sitzung, der Umleitung aller Standard-Datenströme vor dem eigentlichen exec, und dem Schreiben der PID in eine Datei, sobald der neue Prozess seine endgültige Identität hat. exec ersetzt dabei den bash -c Wrapper-Prozess durch den eigentlichen Daemon-Befehl, ohne einen weiteren Kindprozess zu erzeugen.

3. PID-Dateien und Locking gegen Mehrfachstart

Eine PID-Datei speichert die Prozess-ID eines laufenden Daemon, üblicherweise unter /var/run/ oder /run/, und dient zwei Zwecken: anderen Skripten zu erlauben, den Daemon gezielt zu stoppen oder Signale an ihn zu senden, und dem Daemon selbst zu erlauben, beim Start zu prüfen, ob bereits eine Instanz läuft. Ohne diese Prüfung könnten mehrere Instanzen desselben Daemons parallel laufen, was bei exklusiven Ressourcen wie Ports oder Lockfiles zu Konflikten führt.

Die robuste Prüfung liest die PID aus der Datei, sofern vorhanden, und verifiziert mit kill -0, ob der Prozess tatsächlich noch existiert. Eine verwaiste PID-Datei von einem abgestürzten Daemon darf nicht dazu führen, dass ein Neustart verweigert wird, deshalb muss die Existenzprüfung zwingend zur reinen Dateiprüfung dazukommen. flock auf die PID-Datei selbst bietet eine zusätzliche, race-condition-freie Absicherung gegenüber der reinen PID-Prüfung.


#!/usr/bin/env bash
set -euo pipefail

PIDFILE="/var/run/myapp.pid"
LOCKFILE="/var/run/myapp.lock"

check_already_running() {
  if [[ -f "$PIDFILE" ]]; then
    local existing_pid
    existing_pid="$(cat "$PIDFILE")"
    if kill -0 "$existing_pid" 2>/dev/null; then
      echo "[ERROR] Already running with PID $existing_pid" >&2
      exit 1
    else
      echo "[WARN] Stale PID file found, removing" >&2
      rm -f "$PIDFILE"
    fi
  fi
}

# Additional race-condition-free guard using flock
exec 9>"$LOCKFILE"
flock -n 9 || { echo "[ERROR] Could not acquire lock, already running" >&2; exit 1; }

check_already_running
echo $$ > "$PIDFILE"

trap 'rm -f "$PIDFILE" "$LOCKFILE"' EXIT

4. Signal-Handling: SIGTERM, SIGHUP und sauberes Beenden

Ein sauber implementierter Daemon reagiert auf mindestens zwei Signale gezielt: SIGTERM zum kontrollierten Beenden und SIGHUP häufig zum Neuladen der Konfiguration, ohne den Prozess komplett neu zu starten. Ohne expliziten trap für SIGTERM würde der Standardhandler den Prozess sofort ohne Cleanup beenden, was offene Dateien, Locks oder unvollständige Schreiboperationen hinterlassen kann.

Die trap-Funktion für SIGTERM sollte laufende Arbeit kontrolliert abschließen, offene Ressourcen freigeben und die PID-Datei entfernen, bevor der Prozess tatsächlich endet. SIGHUP wird traditionell genutzt, um einem Daemon mitzuteilen, seine Konfigurationsdatei neu einzulesen, ohne laufende Verbindungen oder Verarbeitungen zu unterbrechen, ein Muster, das viele klassische Unix-Daemons wie nginx oder rsyslog implementieren.


#!/usr/bin/env bash
set -euo pipefail

running=true
config_file="/etc/myapp/config.conf"

load_config() {
  # shellcheck disable=SC1090
  source "$config_file"
  echo "$(date -Iseconds) Config reloaded" >> /var/log/myapp.log
}

shutdown_gracefully() {
  echo "$(date -Iseconds) Received SIGTERM, finishing current task" >> /var/log/myapp.log
  running=false
}

reload_config() {
  echo "$(date -Iseconds) Received SIGHUP, reloading config" >> /var/log/myapp.log
  load_config
}

trap shutdown_gracefully TERM
trap reload_config HUP

load_config

while $running; do
  process_next_task
  sleep 1
done

echo "$(date -Iseconds) Daemon stopped cleanly" >> /var/log/myapp.log

5. Logging ohne Terminal: Dateien und Syslog

Ohne Terminal-Verbindung landen Ausgaben eines Daemon nirgendwo, sofern sie nicht explizit umgeleitet werden. Der einfachste Ansatz leitet stdout und stderr direkt an eine Logdatei um, entweder bereits beim Start des Daemons oder mit exec > logfile 2>&1 als erste Zeile im Skript selbst. Für Log-Rotation muss der Daemon zusätzlich auf SIGHUP oder ein eigenes Signal reagieren, um die Logdatei neu zu öffnen, sonst schreibt er nach einer Rotation weiterhin in den alten, bereits umbenannten Dateideskriptor.

Eine robustere Alternative ist logger, das Nachrichten direkt an syslog beziehungsweise journald weiterreicht, ohne dass der Daemon selbst Rotation verwalten muss. echo "Nachricht" | logger -t myapp schreibt einen Log-Eintrag mit dem Tag myapp, der über journalctl -t myapp oder klassische Syslog-Dateien einsehbar ist. Dieser Ansatz delegiert Rotation, Kompression und Aufbewahrungsfristen komplett an die Systemd-Journal- oder Syslog-Infrastruktur.


#!/usr/bin/env bash
set -euo pipefail

log_message() {
  local level="$1"
  local message="$2"
  # Delegate rotation and retention to syslog/journald instead of
  # writing raw files that need manual rotation handling
  logger -t myapp -p "user.${level}" "$message"
}

log_message "info" "Daemon started with PID $$"

process_item() {
  local item="$1"
  if ! do_work "$item"; then
    log_message "err" "Failed to process $item"
    return 1
  fi
  log_message "info" "Processed $item successfully"
}

6. Health-Checks und Restart-Strategien

Ein produktiver Daemon sollte seinen eigenen Gesundheitszustand nach außen sichtbar machen, damit externes Monitoring erkennen kann, ob der Prozess noch sinnvoll arbeitet, nicht nur, ob er noch existiert. Eine einfache Methode ist eine Heartbeat-Datei, die der Daemon in regelmäßigen Abständen mit dem aktuellen Zeitstempel aktualisiert. Ein externes Monitoring-Skript prüft dann, ob diese Datei innerhalb eines erwarteten Zeitfensters aktualisiert wurde, und schlägt Alarm, falls der Daemon zwar als Prozess existiert, aber intern hängt.

Für automatische Neustarts nach Abstürzen reicht ein in Bash gebauter Daemon allein nicht aus, weil ein abgestürzter Prozess sich nicht selbst neu starten kann. Hier braucht es entweder eine übergeordnete Watchdog-Instanz, die den Daemon überwacht und bei Bedarf neu startet, oder den Wechsel zu einem Process Supervisor wie systemd, der genau diese Aufgabe standardmäßig übernimmt.

7. systemd als moderne Alternative

Für die meisten neuen Projekte ist ein systemd-Service die robustere Alternative zur manuellen Bash-Daemonisierung, weil systemd PID-Datei-Verwaltung, Restart-Strategien, Logging über journald und Signal-Handling bereits als Bordmittel bereitstellt. Das Bash-Skript muss dann selbst keinen Daemon-Modus mehr implementieren, sondern kann im Vordergrund laufen, während systemd über Type=simple oder Type=notify die Prozessüberwachung übernimmt.

Der Umstieg reduziert den eigenen Code erheblich: kein Double-Fork, keine manuelle PID-Datei, kein manuelles Log-Rotation-Handling. Restart=on-failure sorgt automatisch für einen Neustart nach einem Absturz, was ein reiner Bash-Daemon ohne externen Watchdog nicht leisten kann. Für neue Projekte auf Linux-Systemen mit systemd ist dieser Weg fast immer vorzuziehen, während die manuelle Double-Fork-Technik für portable Skripte oder Container ohne systemd relevant bleibt.


#!/usr/bin/env bash
set -euo pipefail

# Generate a systemd unit instead of hand-rolling daemonization logic
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Bash worker daemon
After=network.target

[Service]
Type=simple
ExecStart=/opt/myapp/worker-loop.sh
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now myapp.service
systemctl status myapp.service --no-pager

8. Typische Fehler beim Bau eines Bash-Daemons

Der häufigste Fehler ist, stdin nicht umzuleiten. Ein Daemon ohne exec < /dev/null kann bei einem versehentlichen Lesezugriff auf stdin unerwartet blockieren oder ein SIGTTIN erhalten, wenn er noch an ein Terminal gebunden ist. Der zweite häufige Fehler ist, die PID-Datei zu früh zu schreiben, bevor der eigentliche Zielprozess seine endgültige PID über exec erreicht hat, was zu einer PID-Datei mit der PID des Wrapper-Prozesses statt des tatsächlichen Daemons führt.

Ein dritter Fehler betrifft fehlendes Signal-Handling: Ein Daemon ohne trap für SIGTERM wird beim Stoppen abrupt beendet, ohne Cleanup, ohne das Schließen offener Dateien und ohne Entfernen der PID-Datei. Das führt beim nächsten Start zu einer verwaisten PID-Datei, die fälschlich einen bereits laufenden Prozess suggeriert, wenn die Existenzprüfung mit kill -0 nicht sorgfältig implementiert ist.

9. Daemonisierungsansätze im Vergleich

Die folgende Tabelle vergleicht die wichtigsten Ansätze, einen Prozess dauerhaft im Hintergrund zu betreiben.

Ansatz Automatischer Neustart Log-Verwaltung Aufwand
& allein Nein Keine Minimal, aber unzuverlässig
setsid + PID-Datei Nein (ohne Watchdog) Manuell, Rotation nötig Mittel
setsid + logger/syslog Nein (ohne Watchdog) Über syslog/journald Mittel
systemd-Service Ja, konfigurierbar Automatisch über journald Niedrig

Für neue Bash-Daemons auf einem System mit systemd ist die letzte Zeile fast immer die richtige Wahl. Die manuelle Kombination aus setsid, PID-Datei und Signal-Handling bleibt relevant für Container ohne Init-System, ältere Systeme oder portable Skripte, die auf mehreren Plattformen ohne systemd laufen müssen.

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Bash-Skripte, die zuverlässig als Dienst laufen sollen?

Wir bauen robuste Daemonisierung mit PID-Dateien, Signal-Handling und Logging in eure Bash-Skripte ein, oder migrieren sie auf systemd-Units mit automatischem Neustart und zentralem Logging über journald.

Daemon-Review

Bestehende Hintergrundskripte auf Robustheit prüfen

systemd-Migration

Units mit Restart-Strategie und journald-Logging aufsetzen

Signal-Handling

Sauberes Beenden und Config-Reload ohne Prozessneustart

10. Zusammenfassung

Ein echter Daemon braucht mehr als ein & am Zeilenende: vollständige Trennung vom Terminal über setsid, eine PID-Datei mit robuster Existenzprüfung gegen Mehrfachstart, sauberes Signal-Handling für SIGTERM und SIGHUP, sowie Logging, das auch ohne Terminal erreichbar bleibt. Jede dieser Eigenschaften muss im Bash-Skript explizit implementiert werden, sie entstehen nicht automatisch durch einfaches Starten im Hintergrund.

Für neue Projekte auf Systemen mit systemd ist ein systemd-Service in den meisten Fällen die robustere und wartungsärmere Alternative, weil automatischer Neustart, Logging über journald und Prozessüberwachung bereits als Bordmittel vorhanden sind. Die manuelle Daemon-Implementierung bleibt relevant für portable Skripte, Container ohne Init-System und Umgebungen, in denen systemd nicht verfügbar ist.

Bash-Skripte als Daemon betreiben — Das Wichtigste auf einen Blick

Trennung vom Terminal

setsid startet eine neue Sitzung ohne Controlling-Terminal, exec < /dev/null verhindert stdin-Blockaden.

PID-Datei

Existenzprüfung mit kill -0 vor dem Start, flock als zusätzliche race-condition-freie Absicherung.

Signale

trap für SIGTERM zum sauberen Beenden, SIGHUP häufig für Config-Reload ohne Neustart.

Alternative

systemd übernimmt Restart, Logging und Prozessüberwachung als Bordmittel, meist die robustere Wahl.

11. FAQ: Bash-Skripte als Daemon

1Was unterscheidet einen Daemon von & im Hintergrund?
Vollständige Terminaltrennung, eigene Sitzung, umgeleitete Datenströme und PID-Datei statt bloßer Hintergrundausführung.
2Was macht setsid genau?
Startet eine neue Sitzung ohne Controlling-Terminal, trennt den Prozess vollständig vom Terminal.
3Warum eine PID-Datei?
Ermöglicht Stoppen und Signalversand von außen, verhindert Mehrfachstart durch Existenzprüfung.
4Verwaiste PID-Datei erkennen?
Mit kill -0 prüfen, ob der Prozess noch existiert, sonst Datei sicher entfernen.
5Was, wenn SIGTERM nicht abgefangen wird?
Sofortiges Beenden ohne Cleanup, offene Ressourcen und PID-Datei bleiben zurück.
6Wofür SIGHUP bei Daemons?
Traditionell zum Config-Reload ohne kompletten Neustart des Prozesses.
7Warum exec < /dev/null setzen?
Verhindert Blockaden oder SIGTTIN bei versehentlichem stdin-Lesezugriff.
8Automatischer Neustart nach Absturz?
Nur mit externem Watchdog oder systemd Restart=on-failure, nicht durch das Skript selbst.
9Wann systemd statt manueller Lösung?
Fast immer bei neuen Linux-Projekten mit systemd, spart eigenen Code für Restart und Logging.
10Logging ohne manuelle Rotation?
logger -t name an syslog/journald weiterreichen, Rotation übernimmt die System-Infrastruktur.