systemd ist nicht ein Programm, sondern eine Familie von rund neunzig Binaries um einen gemeinsamen Kern: einen Prozess-Manager als PID 1, der jeden Dienst in eine eigene cgroup sperrt, Abhängigkeiten als Transaktion auflöst und alles Weitere über D-Bus delegiert. Dieser Artikel zeigt, wer zu dieser Familie gehört, was jedes Mitglied tut und welche Datei man anfasst, um es zu ändern.
1. Vier Ideen, aus denen alles Weitere folgt
Fast jede Eigenart von systemd lässt sich auf eine dieser vier Entscheidungen zurückführen. Wer sie kennt, kann den Rest ableiten.
Alles ist eine Unit
Dienst, Socket, Mountpunkt, Gerät, Timer, Ressourcen-Gruppe: alles wird als Objekt desselben Typsystems beschrieben, in derselben INI-Syntax, mit denselben Abhängigkeitsverben. Ein Mount ist keine Zeile in einer Tabelle mehr, sondern ein Ding mit Zustand, das andere Dinge starten kann.
Abhängigkeit ist nicht Reihenfolge
Requires= sagt was mitlaufen muss, After= sagt wann. Beides ist getrennt — und genau deshalb kann systemd alles parallel starten, was nicht ausdrücklich geordnet wurde. Die häufigste Fehlerursache in eigenen Units ist ein Requires= ohne das passende After=.
cgroups statt PID-Raten
Jede Unit bekommt ihre eigene Kernel-cgroup. Damit ist zuverlässig bekannt, welche Prozesse zu einem Dienst gehören — auch doppelt geforkte. Daraus folgen sauberes Beenden, Ressourcenlimits und Verbrauchszahlen, ohne PID-Dateien.
D-Bus als Rückgrat
PID 1 tut selbst wenig Fachliches. Uhrzeit, Hostname, Netz, Sitzungen und Namensauflösung liegen in eigenen kleinen Daemons, die eine D-Bus-Schnittstelle anbieten. Die *ctl-Werkzeuge sind bloß Clients dieser Schnittstellen.
2. Die Landschaft auf einen Blick
Links die Werkzeuge, mit denen man redet. In der Mitte der Manager, der als einziger Prozesse startet. Rechts die Fachdaemons, die selbst wieder ganz normale Units sind. Unten der cgroup-Baum, in den jeder gestartete Prozess einsortiert wird.
Der einzige Prozess, der Dienste startet, ist PID 1. Alles andere — auch systemctl — schickt nur Aufträge über D-Bus dorthin und wartet auf das Ergebnis. Deshalb bricht der Bootvorgang nicht ab, wenn dbus-daemon noch nicht läuft: PID 1 hört zusätzlich auf einem privaten Socket unter /run/systemd/private.
3. PID 1 und der Manager pro Benutzer
Dasselbe Binary läuft in zwei Rollen: einmal als Systemmanager mit UID 0, und einmal pro angemeldetem Benutzer als dessen persönlicher Manager. Beide haben eigene Unit-Suchpfade und eigene Konfigurationsdateien.
| Rolle | Konfiguration | Unit-Verzeichnisse | Anwenden |
systemd (PID 1) | /etc/systemd/system.conf /etc/systemd/system.conf.d/*.conf | /etc/systemd/system/ /run/systemd/system/ /usr/lib/systemd/system/ | Units: systemctl daemon-reload system.conf: systemctl daemon-reexec |
| systemd --user | /etc/systemd/user.conf ~/.config/systemd/user.conf | ~/.config/systemd/user/ /etc/systemd/user/ /usr/lib/systemd/user/ | Gestartet als user@UID.service. Ohne Login laufen lassen:
loginctl enable-linger <user> |
Zusätzlich steuerbar ist PID 1 über die Kernel-Kommandozeile: systemd.unit=, systemd.mask=, systemd.log_level=. systemctl selbst startet nichts, sondern übersetzt Unterbefehle in Methodenaufrufe auf org.freedesktop.systemd1. Mit --user, --machine= und --host= wählt man den Manager, den man anspricht.
Die Abhängigkeitsverben — der Teil, an dem eigene Units scheitern
Es gibt zwei unabhängige Achsen: wer muss mitlaufen und in welcher Reihenfolge. Wer nur die erste setzt, bekommt einen Dienst, der parallel zu seiner Voraussetzung startet und mit einer Verbindungsfehlermeldung stirbt.
| Direktive | Achse | Bedeutung |
| Wants= | Existenz | Zieht die andere Unit mit hoch. Scheitert sie, läuft man trotzdem weiter. Der Normalfall. |
| Requires= | Existenz | Zieht mit hoch; scheitert sie, wird auch diese Unit gestoppt. Ohne After= nutzlos scharf. |
| Requisite= | Existenz | Wie Requires=, startet die andere Unit aber nicht — sie muss schon aktiv sein, sonst sofortiger Fehlschlag. |
| BindsTo= | Existenz | Wie Requires=, aber gekoppelt auch an unerwartetes Wegfallen — typisch bei .device-Units. |
| PartOf= | Existenz | Stop und Restart der anderen Unit ziehen diese mit, der Start nicht. Klassisch für Instanzen unter einem Sammel-Target. |
| Conflicts= | Existenz | Das Starten dieser Unit stoppt die andere. So schließen sich rescue.target und der Normalbetrieb aus. |
Before= After= | Reihenfolge | Rein zeitlich, ohne jede Aussage darüber, ob die andere Unit überhaupt läuft. Fast immer paarweise mit einem Existenzverb nötig. |
WantedBy= RequiredBy= | Installation | Steht in [Install] und wirkt erst bei systemctl enable — dann wird ein Symlink im .wants/-Verzeichnis des Ziels angelegt. |
Diagnose statt Raten. systemctl list-dependencies foo.service zeigt den Baum nach unten, --reverse nach oben, und --after beziehungsweise --before zeigen ausschließlich die Reihenfolgekanten — genau die Ansicht, die man bei „startet zu früh“ braucht.
4. Die elf Unit-Typen
Die Dateiendung bestimmt den Typ. Jeder Typ hat neben den gemeinsamen Abschnitten [Unit] und [Install] einen eigenen Abschnitt mit den typspezifischen Direktiven.
| Endung | Abschnitt | Wofür | Woher es meist kommt |
| .service | [Service] | Ein Prozess oder eine Prozessgruppe mit Lebenszyklus, Restart-Politik und Sandbox. | Paket oder selbst geschrieben |
| .socket | [Socket] | Lauschender Socket, der bei der ersten Verbindung den passenden Dienst startet — Socket-Aktivierung. | Paket; Gegenstück heißt gleich mit .service |
| .target | — | Reiner Synchronisationspunkt ohne eigene Prozesse. Der Ersatz für Runlevel. | Paket; eigene Targets sind legitim |
| .timer | [Timer] | Zeit- oder ereignisgesteuerter Auslöser für eine Unit. Der cron-Ersatz, mit Journal und Nachholen. | Paket oder selbst geschrieben |
| .mount | [Mount] | Ein Mountpunkt als verwaltetes Objekt. Der Name ist der escapte Pfad. | Generiert aus /etc/fstab |
| .automount | [Automount] | Mountet erst beim ersten Zugriff, mit optionalem Leerlauf-Timeout. | x-systemd.automount in fstab |
| .swap | [Swap] | Swap-Gerät oder -Datei. | Generiert aus /etc/fstab |
| .path | [Path] | Startet eine Unit, sobald ein Pfad existiert, sich ändert oder nicht mehr leer ist (inotify). | Selbst geschrieben |
| .device | [Unit] | Spiegel eines udev-Geräts. Nicht schreibbar — entsteht durch SYSTEMD_WANTS in udev-Regeln. | udev, automatisch |
| .slice | [Slice] | Knoten im cgroup-Baum, an dem Ressourcenlimits für eine ganze Gruppe von Units hängen. | Eingebaut; eigene sind sinnvoll |
| .scope | [Scope] | Gruppe fremdgestarteter Prozesse, die systemd nur noch verwaltet — Login-Sitzungen, VMs, systemd-run --scope. | Nur zur Laufzeit erzeugt |
Templates. Ein @ vor der Endung macht eine Unit zur Vorlage: getty@.service wird mit systemctl start getty@tty3 instanziiert, und %i beziehungsweise %I in der Datei stehen für den Teil hinter dem @. Weitere Platzhalter: %n Unitname, %H Hostname, %t Laufzeitverzeichnis, %S Zustandsverzeichnis. Pfade als Instanznamen müssen mit systemd-escape -p kodiert werden.
5. Die Boot-Kette
Die obere Reihe ist echte Sequenz — jede Stufe übergibt an die nächste. Die untere Reihe sieht auch nach Sequenz aus, ist aber nur eine Ordnungsbeziehung: innerhalb jeder Stufe läuft alles parallel, was nicht ausdrücklich geordnet wurde.
Warum das praktisch zählt: Ein Dienst, der ins Netz muss, gehört hinter network-online.target — nicht hinter network.target, das nur besagt, dass die Netzwerkverwaltung gestartet wurde. Und wer den Bootvorgang vermessen will, nimmt systemd-analyze critical-chain: das zeigt die Ordnungskette, die die Bootzeit tatsächlich bestimmt hat, statt nur die langsamsten Units aus systemd-analyze blame.
Notausstieg. An der Kernel-Kommandozeile im Bootloader: systemd.unit=rescue.target (einbenutzer, mit Dateisystemen), systemd.unit=emergency.target (nur Root, read-only), systemd.mask=<unit> für einen einzelnen Störenfried, oder init=/bin/bash, wenn systemd selbst nicht mehr hochkommt. Für Ausführlichkeit: systemd.log_level=debug und systemd.log_target=console.
6. Komponenten-Katalog mit allen Konfigurationspfaden
Alle *.conf-Dateien unter /etc/systemd/ folgen demselben Muster: Hauptdatei plus ein gleichnamiges .conf.d/-Verzeichnis daneben, dessen Schnipsel obendrauf gelegt werden.
Protokollierung, Sitzungen und Benutzer
| Komponente | Aufgabe | Konfiguration | Werkzeug |
systemd- journald | Sammelt Meldungen aus fünf Quellen (Unit-stdout/stderr, /dev/log, /dev/kmsg, native API, Audit) in einem binären, indizierten Format mit strukturierten Feldern. | /etc/systemd/journald.conf /etc/systemd/journald.conf.d/ Ablage: /run/log/journal/ oder /var/log/journal/ Storage=, SystemMaxUse=, MaxRetentionSec=, ForwardToSyslog= | journalctl systemd-cat |
systemd- logind | Verwaltet Sitzungen, Seats und Benutzer. Legt pro Anmeldung eine session-N.scope an, vergibt Gerätezugriff an die aktive Sitzung, behandelt Deckel- und Einschalttaste. | /etc/systemd/logind.conf /etc/systemd/logind.conf.d/ HandleLidSwitch=, HandlePowerKey=, IdleAction=, KillUserProcesses=, RemoveIPC= Eintritt: pam_systemd.so | loginctl systemd-inhibit |
systemd- homed | Selbsttragende Heimatverzeichnisse: Benutzerdatensatz und Daten zusammen in einem LUKS-Abbild, das beim Login entschlüsselt wird. | /etc/systemd/homed.conf /home/<user>.home braucht nss-systemd und pam_systemd_home.so | homectl |
systemd- userdbd | Vereinheitlicht Benutzerdatensätze als JSON — aus /etc/passwd, aus homed, aus DynamicUser=. | /etc/userdb/ /run/userdb/ /usr/lib/userdb/ | userdbctl |
Persistentes Journal ist kein Schalter, sondern ein Verzeichnis. Bei Storage=auto schreibt journald genau dann nach /var/log/journal/, wenn dieses Verzeichnis existiert. Anlegen und systemctl restart systemd-journald genügt. Umgekehrt räumt journalctl --vacuum-size=500M oder --vacuum-time=30d sofort auf, ohne auf die Rotation zu warten.
Netz, Namen und Zeit
| Komponente | Aufgabe | Konfiguration | Werkzeug |
systemd- networkd | Deklarative Netzkonfiguration. .link benennt und konfiguriert die Schnittstelle, .netdev erzeugt virtuelle Geräte (Bridge, VLAN, Bond, WireGuard), .network weist Adressen und Routen zu. | /etc/systemd/network/*.link /etc/systemd/network/*.netdev /etc/systemd/network/*.network lexikalisch sortiert, die erste passende [Match] gewinnt | networkctl status networkctl reload networkctl reconfigure <if> |
systemd- resolved | Caching-Resolver mit Stub auf 127.0.0.53:53, pro Link eigene DNS-Server, Split-DNS über Suchdomänen, optional DNSSEC und DNS-over-TLS. | /etc/systemd/resolved.conf /etc/systemd/resolved.conf.d/ /etc/resolv.conf → /run/systemd/ resolve/stub-resolv.conf DNS=, FallbackDNS=, Domains=, DNSSEC=, DNSOverTLS= | resolvectl status resolvectl query resolvectl flush-caches |
systemd- timesyncd | Schlanker SNTP-Client — hält die lokale Uhr nach, stellt aber keine Zeit bereit und kann nicht als Server dienen. Für Zeitserver oder strenge Genauigkeit nimmt man chrony. | /etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf.d/ NTP=, FallbackNTP=, PollIntervalMinSec= | timedatectl timesync-status timedatectl set-ntp true |
hostnamed localed timedated | Drei winzige, bus-aktivierte Dienste, deren einziger Zweck es ist, klassische /etc-Dateien über eine geprüfte Schnittstelle schreibbar zu machen — damit auch unprivilegierte Werkzeuge sie über polkit ändern können. | /etc/hostname, /etc/machine-info /etc/locale.conf, /etc/vconsole.conf /etc/localtime, /etc/adjtime | hostnamectl localectl timedatectl |
Geräte, Boot und Speicher
| Komponente | Aufgabe | Konfiguration | Werkzeug |
systemd- udevd | Hört auf Kernel-uevents, legt Gerätedateien an, vergibt Namen und Symlinks, setzt Rechte und kann Units anstossen. Die berechenbaren Schnittstellennamen wie enp3s0 kommen von hier, nicht von networkd. | /etc/udev/rules.d/*.rules /usr/lib/udev/rules.d/*.rules /etc/udev/udev.conf /etc/udev/hwdb.d/ | udevadm control --reload udevadm trigger udevadm info -a -n udevadm monitor |
systemd- boot | Minimaler EFI-Bootmanager. Kein eigenes Dateisystemwissen, keine Skriptsprache — er liest Einträge von der EFI-Partition und ruft den Kernel auf. | <ESP>/loader/loader.conf <ESP>/loader/entries/*.conf /etc/kernel/cmdline /etc/kernel/install.d/ | bootctl status bootctl list kernel-install ukify |
systemd- cryptsetup systemd- repart | Verschlüsselte Geräte werden aus /etc/crypttab zu generierten Units; repart vergrößert oder erzeugt Partitionen deklarativ beim Boot. | /etc/crypttab /etc/veritytab /etc/repart.d/*.conf | systemd-cryptenroll (TPM2, FIDO2) systemd-repart |
Isolation und Ressourcen
| Komponente | Aufgabe | Konfiguration | Werkzeug |
systemd-nspawn systemd-machined | nspawn startet einen Container aus einem Verzeichnisbaum oder Image, machined führt Buch über laufende Container und VMs und macht sie für systemctl -M adressierbar. | /var/lib/machines/ /etc/systemd/nspawn/<name>.nspawn systemd-nspawn@<name>.service | machinectl start machinectl shell |
| systemd-oomd | Greift ein, bevor der Kernel-OOM-Killer zuschlägt: beobachtet den PSI-Druck ganzer cgroups und beendet gezielt die verursachende Gruppe, nicht den Prozess mit dem höchsten Score. | /etc/systemd/oomd.conf pro Unit: ManagedOOMMemoryPressure= ManagedOOMSwap= | oomctl |
systemd-portabled systemd-sysext | Zwei Wege, Software als Image auszuliefern: portable Dienste hängen ein Image ein und erzeugen daraus Units mit fester Sandbox; Systemerweiterungen legen sich per overlayfs über /usr, ohne es zu verändern. | /var/lib/portables/ /var/lib/extensions/ | portablectl attach systemd-sysext merge |
Einmal-Dienste beim Boot
Diese laufen einmal durch, hinterlassen einen Zustand und beenden sich. Alle folgen demselben Muster: ein Verzeichnis mit Schnipseln unter /usr/lib/ für Pakete und unter /etc/ für den Administrator, wobei eine gleichnamige Datei in /etc/ die des Pakets vollständig ersetzt.
| Werkzeug | Verzeichnisse | Aufgabe |
| systemd-tmpfiles | /etc/tmpfiles.d/ /usr/lib/tmpfiles.d/ | Legt Verzeichnisse, Dateien, Symlinks und Gerätedateien an, setzt Rechte, räumt Altes nach Alter weg. Ersetzt die halbe rc.local. |
| systemd-sysusers | /etc/sysusers.d/ /usr/lib/sysusers.d/ | Deklariert Systembenutzer und -gruppen, die beim Boot angelegt werden, statt sie im Paket-Postinst zu erzeugen. |
| systemd-sysctl | /etc/sysctl.d/ /usr/lib/sysctl.d/ | Setzt Kernel-Parameter. /etc/sysctl.conf wird weiterhin gelesen, gilt aber als Altlast. |
systemd- modules-load | /etc/modules-load.d/ /usr/lib/modules-load.d/ | Lädt Kernelmodule, die nicht automatisch kommen. Optionen dazu weiterhin in /etc/modprobe.d/. |
| systemd-binfmt | /etc/binfmt.d/ | Registriert Interpreter für fremde Binärformate — die Grundlage für qemu-user und Cross-Builds. |
| systemd-coredump | /etc/systemd/coredump.conf | Hängt sich in kernel.core_pattern, legt Abstürze komprimiert ab und macht sie über coredumpctl auffindbar. |
systemd- random-seed | /var/lib/systemd/random-seed | Rettet Entropie über den Reboot — wichtig auf VMs, die sonst früh im Boot beim Zufall blockieren. |
| systemd-firstboot | /etc/machine-id | Fragt beim allerersten Start Zeitzone, Locale, Hostname und root-Passwort ab — oder bekommt sie per Credential vorgesetzt. |
7. Generatoren — die unsichtbare Übersetzungsschicht
Vor jedem daemon-reload ruft PID 1 alle ausführbaren Dateien in den Generator-Verzeichnissen auf. Jeder darf Units nach /run/systemd/generator*/ schreiben. So werden Formate zu Units, die nie als Unit gedacht waren — und deshalb findet man eine .mount-Unit, die man nirgends angelegt hat.
| Generator | Übersetzt | Ergebnis |
systemd-fstab- generator | /etc/fstab und die Kernel-Parameter root=, resume= | .mount-, .automount- und .swap-Units |
systemd-cryptsetup- generator | /etc/crypttab | systemd-cryptsetup@<name>.service |
systemd-getty- generator | Konsolen aus /sys und console= | getty@- und serial-getty@-Instanzen |
systemd-gpt-auto- generator | GPT-Partitionstypen (Discoverable Partitions) | Mounts für /home, /srv, ESP, Swap — ganz ohne fstab |
systemd-sysv- generator | alte Init-Skripte in /etc/init.d/ | Kompatibilitäts-Units in /run/systemd/generator.late/ |
systemd-debug- generator | Kernel-Parameter systemd.mask=, systemd.wants=, systemd.debug-shell | Maskierungen und Zusatz-Units für genau diesen Boot |
Eigene Generatoren liegen in /etc/systemd/system-generators/ und müssen ausführbar sein. Sie laufen sehr früh, ohne Netz, mit strengem Timeout und ohne die Möglichkeit, andere Units zu starten — sie dürfen ausschließlich Dateien schreiben. systemd-analyze unit-paths zeigt, wo ihre Ausgabe in der Rangfolge landet.
8. Konfigurationsmodell und Drop-ins
Das ganze Modell beruht auf einer Trennung: /usr/lib/ gehört der Distribution und wird bei jedem Update überschrieben, /etc/ gehört dem Administrator und wird nie angefasst, /run/ gilt nur bis zum Neustart. Dazu kommt ein zweiter Mechanismus, der oft übersehen wird — Drop-ins.
Die zwei Mechanismen verhalten sich gegensätzlich, und das ist die häufigste Verwirrung: bei der Unit-Datei gewinnt genau eine und verdeckt die anderen vollständig; bei Drop-ins werden alle gefundenen Schnipsel aus allen drei Ebenen angewandt, sortiert nach ihrem Dateinamen. Trägt ein Drop-in denselben Dateinamen wie eines aus /usr/lib/, ersetzt die Fassung aus /etc/ es punktgenau.
Der Weg, den man in der Praxis nimmt
# Nie die Paketdatei editieren - ein Update überschreibt sie kommentarlos.
# Stattdessen ein Drop-in anlegen; der Editor öffnet eine leere Datei:
systemctl edit nginx.service # -> /etc/systemd/system/nginx.service.d/override.conf
systemctl edit --drop-in=limits nginx.service # eigener Dateiname statt override.conf
systemctl edit --runtime nginx.service # nur bis zum Reboot, nach /run
systemctl edit --full nginx.service # doch die ganze Datei - Kopie nach /etc
# Ergebnis kontrollieren
systemctl cat nginx.service # Basisdatei + alle Drop-ins, in Reihenfolge
systemctl show nginx.service -p ExecStart -p MemoryMax
systemd-analyze verify /etc/systemd/system/eigen.service
# Was hat systemd überhaupt geladen und woher?
systemd-analyze unit-paths # alle Suchpfade in Rangfolge
systemd-analyze cat-config systemd/journald.conf # dasselbe für *.conf.d
# Nach jeder Änderung an einer Unit-Datei:
systemctl daemon-reload # neu einlesen
systemctl restart nginx.service # erst das wendet sie auf den Prozess an
Die Falle bei Listen. Direktiven, die mehrfach vorkommen dürfen — ExecStart=, Environment=, After=, ReadWritePaths= — ergänzen im Drop-in, sie ersetzen nicht. Wer einen anderen Startbefehl will, schreibt erst ExecStart= ohne Wert, um die Liste zu leeren, und dann die neue Zeile. Bei Type=oneshot sind mehrere ExecStart= übrigens ausdrücklich erlaubt und laufen nacheinander.
9. Eine Unit-Datei, Zeile für Zeile
Drei Abschnitte, jeder mit einer klaren Zuständigkeit: [Unit] beschreibt Beziehungen zu anderen Units, der typspezifische Abschnitt beschreibt das Verhalten, [Install] beschreibt nur, was beim enable passieren soll.
# /etc/systemd/system/backup-export.service
[Unit]
Description=Nächtlicher Export nach S3
Documentation=https://wiki.intern/backup-export
After=network-online.target postgresql.service # Reihenfolge ...
Wants=network-online.target # ... und wer mitkommen soll
Requires=postgresql.service # harte Kopplung: DB weg => wir auch
ConditionPathExists=/srv/export # Condition: still überspringen ...
AssertPathIsDirectory=/srv/export # ... Assert: laut scheitern
StartLimitIntervalSec=300
StartLimitBurst=3 # mehr als 3 Fehlstarts in 5 min => Aufgabe
[Service]
Type=oneshot # simple | exec | forking | oneshot | notify | dbus | idle
User=backup
Group=backup
WorkingDirectory=/srv/export
EnvironmentFile=-/etc/default/backup-export # führendes "-": darf fehlen
Environment="LC_ALL=C"
ExecStartPre=/usr/local/bin/export-check
ExecStart=/usr/local/bin/export-run --target=s3
ExecStopPost=/usr/local/bin/export-cleanup # läuft auch nach Fehlschlag
TimeoutStartSec=30min
Restart=on-failure # no | on-failure | always | on-abnormal
RestartSec=30
# Verzeichnisse, die systemd anlegt, aufräumt und rechtemässig dem User gibt:
RuntimeDirectory=backup-export # /run/backup-export
StateDirectory=backup-export # /var/lib/backup-export
LogsDirectory=backup-export # /var/log/backup-export
CacheDirectory=backup-export # /var/cache/backup-export
# Sandbox - Details im Abschnitt "Härten"
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes
ReadWritePaths=/srv/export
[Install]
WantedBy=multi-user.target # wirkt erst bei "systemctl enable"
Der zugehörige Timer — der cron-Ersatz
# /etc/systemd/system/backup-export.timer
# aktivieren mit: systemctl enable --now backup-export.timer
[Unit]
Description=Startet den Export täglich um 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true # verpasste Läufe nachholen (Maschine war aus)
RandomizedDelaySec=15min # entzerrt viele Hosts mit derselben Zeit
AccuracySec=1min # Standard ist 1min, nicht sekundengenau
Unit=backup-export.service # optional; sonst gleicher Name mit .service
[Install]
WantedBy=timers.target
Warum Timer statt cron. Der Lauf landet in einer eigenen cgroup und im Journal (journalctl -u backup-export zeigt jeden Lauf), er kann Abhängigkeiten haben, er lässt sich mit systemctl start backup-export.service unverändert von Hand auslösen, und systemctl list-timers zeigt für alle Timer den letzten und den nächsten Termin. Ausdrücke prüft man vorher mit systemd-analyze calendar "Mon *-*-* 03:00".
Type= richtig wählen — die häufigste Ursache für hängende Starts
| Type= | systemd hält die Unit für gestartet, sobald ... | Passt zu |
| simple | ... der Prozess geforkt wurde. Kein Warten, keine Prüfung. | Vordergrundprozesse, wenn nichts von ihnen abhängt |
| exec | ... execve() erfolgreich war. Fängt wenigstens „Binary fehlt“ ab. | bessere Vorgabe als simple |
| notify | ... der Dienst selbst READY=1 über sd_notify() meldet. | alles, worauf andere Units warten müssen |
| forking | ... der Elternprozess sich beendet hat. Braucht meist PIDFile=. | alte Daemons, die sich selbst in den Hintergrund legen |
| oneshot | ... der Prozess fertig ist. Mit RemainAfterExit=yes gilt die Unit danach als aktiv. | Skripte, Migrationen, Einrichtungsschritte |
| dbus | ... der in BusName= genannte Name auf dem Bus erscheint. | bus-aktivierte Dienste |
| idle | ... wie simple, wartet aber bis zu 5 s auf ruhige Konsole. | nur Kosmetik beim Boot |
10. cgroups: Buchführung und Grenzen
Weil ohnehin jede Unit ihre eigene cgroup hat, kosten Limits nichts extra — sie sind ein paar Zeilen in derselben Datei. Der Baum ist hierarchisch: was am Slice hängt, gilt für alle Units darin gemeinsam.
| Direktive | Wirkung | Typischer Einsatz |
| MemoryHigh= | Weiche Grenze: der Kernel bremst und räumt aggressiv auf, tötet aber nicht. | Erste Wahl — Dienst wird langsam statt tot |
| MemoryMax= | Harte Grenze; darüber greift der OOM-Killer innerhalb der cgroup. | Als letzte Sicherung über MemoryHigh= |
| CPUQuota= | Absolute Obergrenze, 200% entspricht zwei vollen Kernen. | Backup- und Indexläufe deckeln |
| CPUWeight= | Relatives Gewicht (Vorgabe 100) — wirkt nur bei Konkurrenz. | Wichtiges bevorzugen, ohne zu verschenken |
IOWeight= IOReadBandwidthMax= | Anteil beziehungsweise absolute Obergrenze an Datenträger-Durchsatz, pro Gerät. | Ein Rsync, das den Host lahmlegt |
| TasksMax= | Obergrenze an Prozessen und Threads. | Fork-Bomben und Runaway-Worker |
| Slice= | Hängt die Unit unter ein anderes Slice, dessen Limits dann gemeinsam gelten. | Mehrere Dienste zusammen deckeln |
# Sehen, wer was verbraucht
systemd-cgls # der Baum, mit Prozessen
systemd-cgtop # wie top, aber pro Unit statt pro Prozess
systemctl status nginx.service # zeigt Memory, Tasks, CPU der cgroup
# Grenze setzen, ohne eine Datei zu öffnen - schreibt ein Drop-in nach /etc/...d/
systemctl set-property nginx.service MemoryHigh=2G MemoryMax=3G
systemctl set-property --runtime backup.service CPUQuota=40% # nur bis Reboot
# Ein eigenes Slice für eine Gruppe von Diensten:
# /etc/systemd/system/batch.slice
# [Slice]
# CPUQuota=300%
# MemoryMax=8G
# ... dann in jeder Unit: Slice=batch.slice
# Einmalig etwas eingesperrt laufen lassen, ohne Unit-Datei
systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./import.sh
systemd-run --unit=nachtlauf --on-calendar="*-*-* 03:00" /usr/local/bin/job
Zahlen gibt es nur, wenn mitgezählt wird. Auf älteren Systemen ist DefaultMemoryAccounting= und DefaultCPUAccounting= in /etc/systemd/system.conf nicht überall aktiv — dann bleibt systemctl status bei der Speicherangabe stumm. Sobald eine Grenze gesetzt ist, wird für diese Unit ohnehin gezählt.
11. Härten ohne Container
Dieselben Kernel-Mechanismen, die Container ausmachen — Namespaces, Capabilities, seccomp, Bind-Mounts — stehen jeder Unit als Direktiven zur Verfügung. Ein Dienst lässt sich damit deutlich enger führen als in vielen Container-Setups, ohne ein Image zu bauen.
| Direktive | Wirkung |
| ProtectSystem=strict | Das gesamte Dateisystem wird nur lesbar eingehängt, außer /dev, /proc, /sys. Schreibrechte gibt es nur zurück über ReadWritePaths=. |
| ProtectHome=yes | /home, /root und /run/user sind leer. read-only als mildere Variante. |
| PrivateTmp=yes | Eigenes /tmp und /var/tmp im Namespace — beseitigt eine ganze Klasse von Symlink-Angriffen. |
| PrivateDevices=yes | Nur noch Pseudo-Geräte, kein Zugriff auf echte Hardware. |
| PrivateNetwork=yes | Eigener Netzwerk-Namespace mit nur Loopback. Hart, aber für lokale Batch-Jobs ideal. |
| NoNewPrivileges=yes | Kein setuid-Aufstieg mehr, für den Dienst und alle seine Kinder. Sollte fast überall stehen. |
| CapabilityBoundingSet= | Weißliste der verbleibenden Capabilities, zum Beispiel nur CAP_NET_BIND_SERVICE für Port 80. |
SystemCallFilter= @system-service | seccomp-Filter über vordefinierte Syscall-Gruppen; mit ~@privileged gezielt ausschließen. |
| RestrictAddressFamilies= | Nur die gebrauchten Socket-Familien, meist AF_INET AF_INET6 AF_UNIX. |
ProtectKernelTunables= ProtectKernelModules= ProtectKernelLogs= | /proc/sys und /sys nur lesbar, kein Modulladen, kein dmesg. |
| DynamicUser=yes | UID nur für die Laufzeit, kein Eintrag in /etc/passwd, automatisch mit privaten StateDirectory= und PrivateTmp=. |
IPAddressDeny=any IPAddressAllow= | Paketfilter pro Unit über eBPF — Netzzugriff auf konkrete Ziele einschränken, unabhängig von der Firewall. |
# Bewertung der eigenen Units - 0 = uneinnehmbar, 10 = völlig offen
systemd-analyze security # alle Dienste, sortiert
systemd-analyze security nginx.service # zeilenweise, mit Begründung je Direktive
# Vorgehen: schrittweise zuschrauben, nach jedem Schritt neu starten und prüfen.
# Fehlschläge stehen im Journal - bei seccomp meist als "Operation not permitted".
journalctl -u nginx -b --since "5 min ago" -p warning
Reihenfolge in der Praxis. Erst NoNewPrivileges=, PrivateTmp= und ProtectSystem=full — die brechen fast nie etwas. Dann ProtectSystem=strict mit passenden ReadWritePaths=. Zuletzt Capabilities und Syscall-Filter, denn dort scheitert es am ehesten und die Meldungen sind am unspezifischsten. Alles davon gehört in ein Drop-in, nicht in die Paketdatei.
12. Werkzeugkasten
| Bereich | Befehle |
Zustand systemctl | systemctl status <unit> systemctl list-units --failed systemctl is-active / is-enabled / is-failed (skripttauglich) systemctl reset-failed systemctl list-unit-files --state=enabled systemctl list-timers --all · systemctl list-sockets |
Journal journalctl | journalctl -u <unit> -b · -b -1 (voriger Boot) journalctl --since "-2h" --until "10:00" journalctl -p err · -k (nur Kernel) · -f (folgen) journalctl -o json-pretty · _PID= · _UID= journalctl --disk-usage · --vacuum-time=30d · --verify |
Analyse systemd-analyze | systemd-analyze time · blame · critical-chain systemd-analyze plot > boot.svg systemd-analyze dot | dot -Tsvg > units.svg systemd-analyze verify <datei> · security <unit> systemd-analyze calendar · timespan · condition systemd-analyze unit-paths · cat-config |
Ad hoc systemd-run | systemd-run --unit=test /pfad/cmd systemd-run --scope -p MemoryMax=1G cmd systemd-run --on-active=90s cmd systemd-run --on-calendar="*:0/15" cmd systemd-run --user --scope cmd (lange Laeufe ueberleben so das Abbrechen der SSH-Verbindung) |
13. Praxis auf Debian- und Proxmox-Systemen
Zwei Pfade, ein Verzeichnis
/lib/systemd/system ist seit dem usr-merge nur noch ein Symlink auf /usr/lib/systemd/system. Ältere Anleitungen nennen den alten Pfad — das ist dasselbe Verzeichnis, nicht ein zweiter Suchpfad.
/etc/default lebt weiter
Debian-Pakete binden ihre alten /etc/default/<dienst>-Dateien meist per EnvironmentFile=-/etc/default/... ein. Änderungen dort wirken also — aber nur, wenn die Unit die Variablen auch tatsächlich benutzt. systemctl cat verrät es.
Alte Init-Skripte funktionieren
Was in /etc/init.d/ liegt, wird vom sysv-Generator in eine Unit übersetzt und taucht in systemctl auf. Diese Units liegen in /run/systemd/generator.late/ und lassen sich nicht editieren — man schreibt stattdessen eine echte Unit in /etc/, die sie verdrängt.
Netz wird nicht von networkd verwaltet
Auf Debian und Proxmox macht das weiterhin ifupdown mit /etc/network/interfaces, auf Desktops NetworkManager. networkd ist installiert, aber ab Werk deaktiviert. Vor dem Umstellen prüfen, wer gerade zuständig ist — zwei Verwalter auf einer Schnittstelle enden im Flattern.
resolv.conf ist ein Symlink
Wenn resolved läuft, steht in /etc/resolv.conf nur 127.0.0.53. Die echten Server zeigt resolvectl status. Manuelles Editieren wird beim nächsten Neustart überschrieben — die Server gehören in resolved.conf oder ans Interface.
Proxmox bootet über systemd-boot
Bei Installation auf ZFS mit UEFI verwaltet proxmox-boot-tool die EFI-Partitionen und schreibt die systemd-boot-Einträge — bootctl und /etc/kernel/cmdline direkt anzufassen, geht an diesem Werkzeug vorbei. Der richtige Weg ist proxmox-boot-tool refresh.
Ablauf, wenn eine Unit nicht startet
systemctl status foo.service # 1. Zustand, Exit-Code, letzte 10 Journal-Zeilen
journalctl -u foo.service -b --no-pager # 2. der ganze Verlauf dieses Boots
systemctl cat foo.service # 3. was ist wirklich konfiguriert, inkl. Drop-ins
systemd-analyze verify foo.service # 4. Syntax und unbekannte Direktiven
systemctl list-dependencies foo.service --after # 5. wartet sie auf das Richtige?
systemctl show foo.service -p Conditions -p ExecMainStatus # 6. still übersprungen?
# Läuft der Dienst von Hand, aber nicht als Unit, ist es fast immer die Sandbox
# oder die Umgebung. Zum Gegenprüfen mit identischer Umgebung starten:
systemd-run --pty --same-dir --wait --collect --service-type=exec \
--property=User=backup /usr/local/bin/export-run --target=s3
Maßgeblich ist immer das Handbuch der eingesetzten Version. Direktiven und Voreinstellungen unterscheiden sich zwischen den systemd-Versionen. man systemd.directives ordnet jede einzelne Direktive der passenden Handbuchseite zu; die wichtigsten Seiten sind man systemd.unit, man systemd.service, man systemd.exec und man systemd.resource-control.