Einfach erklärt
- Eine Unit ist eine kleine Textdatei. Sie beschreibt, was gestartet wird, wann und wovon es abhängt.
- Man schreibt kein Startskript mehr, sondern sagt systemd nur, was man haben will. Den Rest erledigt systemd.
- Units der Pakete liegen unter /usr/lib, eigene und geänderte Units unter /etc. Was in /etc liegt, gewinnt.
- Wants sagt, was mitgestartet wird. After sagt, was vorher fertig sein muss. Das sind zwei getrennte Dinge.
Animation
Wo Units liegen
| Verzeichnis |
Wer schreibt dort |
Vorrang
|
| /etc/systemd/system/ |
der Admin |
höchster
|
| /run/systemd/system/ |
Programme zur Laufzeit, weg nach dem Neustart |
mittel
|
| /usr/lib/systemd/system/ |
die Pakete, wird bei Updates überschrieben |
niedrigster
|
- Liegt eine Unit mit gleichem Namen in /etc, wird die aus /usr/lib ignoriert.
- Drop-ins ändern nur einzelne Zeilen: /etc/systemd/system/name.service.d/override.conf
- systemctl cat name zeigt die Unit samt allen Drop-ins, so wie systemd sie wirklich sieht.
Aufbau einer Unit
- vi /etc/systemd/system/simple-http.service
[Unit]
Description=Einfacher Webserver für Downloads
After=network.target
[Service]
Type=exec
User=kit
WorkingDirectory=/home/kit/Downloads
ExecStart=/usr/bin/python3 -m http.server 8000
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
| Zeile |
Bedeutung
|
| [Unit]: Beschreibung und Abhängigkeiten
|
| Description= |
Text, der bei status und im Journal erscheint
|
| After=network.target |
erst starten, wenn die Netzwerkverwaltung läuft
|
| [Service]: wie der Dienst läuft
|
| Type=exec |
systemd startet das Programm und wartet, bis es wirklich ausgeführt wird
|
| User=kit |
läuft als Benutzer kit, nicht als root
|
| WorkingDirectory= |
in diesem Verzeichnis startet der Prozess, http.server zeigt genau dieses an
|
| ExecStart= |
der Befehl, immer mit absolutem Pfad
|
| Restart=on-failure |
bei Absturz oder Fehlercode neu starten
|
| RestartSec=2 |
2 Sekunden warten vor dem Neustart
|
| [Install]: wird nur von enable und disable gelesen
|
| WantedBy=multi-user.target |
enable legt einen Link in multi-user.target.wants/ an
|
In Betrieb nehmen
- systemctl daemon-reload
- systemctl enable --now simple-http
- systemctl status simple-http
- journalctl -fu simple-http
- sudo ss -lntp | grep 8000
- curl -s http://localhost:8000/ | head -3
- Achtung: http.server lauscht auf allen Netzwerkkarten. Nur lokal: --bind 127.0.0.1
Prüfen vor dem Start
- systemd-analyze verify /etc/systemd/system/simple-http.service
- Keine Ausgabe heißt: keine Fehler gefunden.
Wants, Requires und After
| Anweisung |
Bedeutung |
Wenn die andere Unit scheitert
|
| Wants=b |
b wird mitgestartet |
egal, a läuft trotzdem
|
| Requires=b |
b wird mitgestartet |
a scheitert mit
|
| After=b |
a startet erst, wenn b fertig ist |
startet nichts von selbst
|
| Before=b |
a muss fertig sein, bevor b startet |
startet nichts von selbst
|
- Wants und Requires sagen was, After und Before sagen wann. Meist braucht man beides.
- After= allein startet nichts. Es ordnet nur, falls die andere Unit ohnehin gestartet wird.
- In der Praxis nimmt man fast immer Wants. Requires nur, wenn ohne die andere Unit wirklich nichts geht.
- network.target: Die Netzwerkverwaltung läuft. Für Server, die lauschen, reicht das.
- network-online.target: Es gibt wirklich eine Verbindung. Dann braucht man Wants= und After=.
Type=: wann ein Dienst als gestartet gilt
| Type |
Gestartet, wenn … |
Beispiel
|
| simple |
der Prozess angelegt ist |
einfache Programme (Standard)
|
| exec |
das Programm wirklich ausgeführt wird, meldet Tippfehler im Pfad als Fehler |
besser als simple
|
| forking |
der Startprozess sich beendet hat und ein Kind weiterläuft |
alte Daemons
|
| oneshot |
das Programm fertig ist |
Skripte, z.B. Firewall laden
|
| notify |
der Dienst selbst „bereit“ meldet |
nginx, postgresql
|
- Bei oneshot mit RemainAfterExit=yes bleibt die Unit nach dem Skript „active“. So kann ein späteres stop noch ExecStop= ausführen.
Restart=: wann neu gestartet wird
| Restart |
Exit-Code ≠ 0 |
Absturz durch Signal (kill -9, SIGSEGV) |
normales Ende
|
| no |
– |
– |
–
|
| on-failure |
ja |
ja |
–
|
| on-abort |
– |
ja |
–
|
| always |
ja |
ja |
ja
|
- on-abort startet bei einem Python-Fehler (Exit-Code 1) nicht neu. Meist will man on-failure.
- Startet ein Dienst mehr als 5-mal in 10 Sekunden, gibt systemd auf: Start request repeated too quickly. Danach hilft systemctl reset-failed name.
Units ändern
| Befehl |
Wirkung
|
| systemctl edit ssh |
legt einen Drop-in an, nur die Änderungen
|
| systemctl edit --full ssh |
kopiert die ganze Unit nach /etc
|
| systemctl revert ssh |
alle eigenen Änderungen verwerfen
|
| systemctl daemon-reload |
nach Änderungen mit vi oder nano, nicht nötig nach systemctl edit
|
- Besser edit als --full: Bei --full bekommt man Änderungen aus Paket-Updates nicht mehr mit.
- Einen Wert ersetzen statt ergänzen, z.B. ExecStart: erst leeren, dann neu setzen.
[Service]
ExecStart=
ExecStart=/usr/bin/python3 -m http.server 8080
Timer statt cron
- Ein Timer startet eine andere Unit. Beide heißen gleich: backup.timer startet backup.service.
- /etc/systemd/system/backup.service
[Unit]
Description=Nächtliches Backup
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/backup.sh
- /etc/systemd/system/backup.timer
[Unit]
Description=Backup jede Nacht um 2 Uhr
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
- systemctl daemon-reload
- systemctl enable --now backup.timer
- systemctl list-timers backup.timer
- Persistent=true: War der Rechner um 2 Uhr aus, läuft das Backup beim nächsten Start nach.
- Zeitangaben testen:
- systemd-analyze calendar "Mon *-*-* 02:00"
Original form: Mon *-*-* 02:00
Normalized form: Mon *-*-* 02:00:00
Next elapse: Mon 2026-10-19 02:00:00 CEST
Beispiel: Firewall vor dem Netz
- Eine Firewall muss stehen, bevor das Netz hochkommt, sonst gibt es ein Zeitfenster ohne Schutz.
- /etc/systemd/system/firewall.service
[Unit]
Description=Firewall-Regeln laden
Before=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/firewall start
ExecStop=/usr/local/sbin/firewall stop
[Install]
WantedBy=multi-user.target
Stolperfallen
- daemon-reload vergessen. systemd arbeitet dann mit der alten Version weiter. status warnt: changed on disk.
- Kein Shell. In ExecStart= gehen keine Umleitungen und Pipes. Dafür: ExecStart=/bin/bash -c "befehl >> /tmp/log"
- Relativer Pfad. ExecStart= braucht den vollen Pfad, sonst status=203/EXEC.
- Kein Zugriff. Darf User= nicht in WorkingDirectory=, kommt status=200/CHDIR.
- Beim Timer den Timer aktivieren, nicht den Service.
- Ganze Unit nach /etc kopiert. Updates des Pakets kommen nicht mehr an. Besser systemctl edit.
Aufgaben
Grundaufgaben
- Lassen Sie sich mit systemctl cat die Unit von cron anzeigen. Wo liegt sie, und wovon hängt sie ab?
- Legen Sie simple-http.service an, starten Sie den Dienst und rufen Sie die Seite mit curl auf.
- Beenden Sie den Prozess mit kill -9 und beobachten Sie status. Was macht systemd?
- Ändern Sie mit systemctl edit den Port auf 8080. Prüfen Sie mit systemctl cat, was daraus geworden ist.
- Bauen Sie einen Dienst für den iperf3-Server (iperf3 -s), der als Benutzer nobody läuft.
- Schreiben Sie einen Timer, der jede Minute das Datum an /tmp/zeit.log anhängt.
Für Schnelle
- Ersetzen Sie Restart=on-failure durch on-abort. Lassen Sie den Dienst einmal mit kill -9 und einmal mit einem Fehlercode enden. Was ist der Unterschied?
- Bauen Sie einen Dienst, der sofort mit exit 1 endet und Restart=always hat. Was passiert nach einigen Sekunden?
- Schreiben Sie die Firewall-Unit und prüfen Sie mit systemd-analyze critical-chain network-pre.target, dass sie vor dem Netz startet.
Weiter