Einen systemd Service erstellen heißt: Du schreibst eine kleine Textdatei, die Linux sagt, welches Programm im Hintergrund laufen soll, wann es starten soll und was passieren soll, wenn es abstürzt. Danach startet dein Skript, deine Node-App oder dein Python-Server beim Booten automatisch, wird nach einem Absturz neu gestartet und schreibt seine Ausgabe in ein zentrales Log. Kein screen, kein nohup, kein vergessenes Terminal-Fenster mehr.
Auf unserem eigenen Server laufen über hundert selbst angelegte Units: Webseiten, APIs, Bots, kleine Werkzeuge. Für diesen Artikel haben wir am 5. Oktober 2026 einen Test-Service gebaut und absichtlich kaputt gemacht: per kill -9 abgeschossen, in eine Absturzschleife geschickt, beim Speicher begrenzt und gehärtet. Außerdem haben wir unsere eigenen Units mit systemds eingebautem Sicherheits-Check vermessen. Drei Befunde vorweg:
1.
Restart=on-failurestartet einen Dienst nicht neu, wenn er mit einem normalenkill(SIGTERM) beendet wird. Für systemd ist das ein sauberes Ende. Nurkill -9hat in unserem Test einen Neustart ausgelöst.2. Ein Dienst, der ständig abstürzt, wird nach 5 Versuchen in 10 Sekunden endgültig aufgegeben. Ohne Alarm. Er steht dann einfach auf
failed.3. Von 104 unserer eigenen Unit-Dateien hatte keine einzige einen guten Sicherheitswert. Median: 9,6 von 10 („UNSAFE”). Mit 20 Zeilen Härtung kam unser Test-Service auf 1,5 und lief weiter wie vorher.

Was ist systemd überhaupt?
systemd ist das Programm, das auf fast jedem modernen Linux als Erstes startet (es hat die Prozessnummer 1) und dann alles andere hochfährt: Netzwerk, Logging, SSH, Datenbanken, deine eigenen Dienste. Ubuntu, Debian, Fedora, Arch, Rocky Linux, openSUSE: Sie alle nutzen systemd. Wenn du gerade erst einen Server aufsetzt, steht in unserem Artikel zum Linux-Server einrichten, was davor kommt.
Jede Sache, die systemd verwaltet, heißt Unit. Es gibt verschiedene Arten:
.service: ein Programm, das läuft (oder einmal ausgeführt wird). Darum geht es hier..timer: ein Zeitplan, der einen Service auslöst. Der moderne Ersatz für viele Cronjobs..socket: startet einen Dienst erst, wenn jemand auf einem Port anklopft..target: eine Gruppe von Units, etwamulti-user.target(„System ist normal hochgefahren”)..mount,.path,.sliceund ein paar weitere für Spezialfälle.
Unser Testsystem lief mit systemd 255 (Ubuntu 24.04). Alles in diesem Artikel funktioniert ab etwa Version 240, auf Debian 12 und neuer also problemlos. Die Version findest du mit systemctl --version heraus.
Vortrag der Dresden OpenSource UserGroup: was systemd ist, und was Services, Timer, Sockets und Targets sind. Gut als Hintergrund, bevor du selbst Units schreibst.
systemd Service erstellen: die Kurzanleitung
Wenn du nur schnell ein Programm als Dienst laufen lassen willst, sind es fünf Schritte. Danach erklären wir jede Zeile.
1. Unit-Datei anlegen (als root bzw. mit sudo):
sudo nano /etc/systemd/system/meine-app.service
2. Inhalt einfügen:
[Unit]
Description=Meine App
After=network.target
[Service]
Type=simple
User=meineapp
WorkingDirectory=/opt/meine-app
ExecStart=/usr/bin/node /opt/meine-app/server.js
Environment=PORT=3000
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
3. systemd die neue Datei bekannt machen:
sudo systemctl daemon-reload
4. Starten und für den Autostart aktivieren:
sudo systemctl enable --now meine-app
5. Prüfen:
systemctl status meine-app
journalctl -u meine-app -f
Das war’s. Bei uns dauerte systemctl start für einen kleinen Python-Webserver 19 Millisekunden, und der laufende Dienst belegte 9,1 MB Arbeitsspeicher. systemd selbst kostet also praktisch nichts.
Wo liegen systemd-Service-Dateien?
Das ist eine der häufigsten Fragen, und die Antwort hat drei Teile:
| Ort | Wofür | Anfassen? |
|---|---|---|
/etc/systemd/system/ | Deine eigenen Units und Überschreibungen | Ja, hier gehören deine Dateien hin |
/usr/lib/systemd/system/ (bzw. /lib/systemd/system/) | Units, die Pakete mitbringen (nginx, ssh, …) | Nein, wird bei Updates überschrieben |
/run/systemd/system/ | Zur Laufzeit erzeugte Units | Nein, verschwindet beim Neustart |
~/.config/systemd/user/ | User-Services ohne root | Ja, für eigene Benutzerdienste |
Wenn eine Datei mit gleichem Namen an mehreren Orten liegt, gewinnt /etc/. So kannst du eine Paket-Unit komplett ersetzen, ohne die Original-Datei zu verändern. Meistens willst du aber nur ein paar Zeilen ändern, dafür gibt es Drop-ins:
sudo systemctl edit nginx
Das öffnet einen Editor und speichert deine Änderungen in /etc/systemd/system/nginx.service.d/override.conf. Die Original-Unit bleibt unangetastet, und ein Paket-Update macht deine Anpassung nicht kaputt. Was am Ende tatsächlich gilt, zeigt:
systemctl cat nginx
Auf unserem Server liegen 113 Service-Dateien in /etc/systemd/system/. Zehn davon sind nur Verknüpfungen (zum Beispiel Aliase), der Rest sind eigene Dienste.

Der Aufbau einer Unit-Datei, Zeile für Zeile
Eine Service-Datei hat fast immer drei Abschnitte. Groß- und Kleinschreibung zählt, und Kommentare beginnen mit #.
[Unit]: Beschreibung und Abhängigkeiten
[Unit]
Description=Meine App
After=network.target
Wants=network-online.target
Description=: Ein lesbarer Name. Taucht insystemctl statusund im Log auf.After=: Reihenfolge. „Starte mich erst, nachdem X gestartet wurde.” Das ist keine Abhängigkeit: Wenn X gar nicht gestartet wird, startet dein Dienst trotzdem.Wants=/Requires=: Das sind die Abhängigkeiten.Wants=zieht X mit hoch, ist aber nicht beleidigt, wenn X scheitert.Requires=ist strenger: Fällt X aus, wird dein Dienst mit gestoppt.
Der häufigste Anfängerfehler hier: nur After=postgresql.service zu schreiben und sich zu wundern, warum die Datenbank beim Start nicht da ist. Du brauchst beides, After= für die Reihenfolge und Wants= oder Requires= dafür, dass sie überhaupt gestartet wird.
Für Dienste, die beim Start eine echte Netzwerkverbindung brauchen (etwa eine entfernte Datenbank), ist network.target übrigens zu früh. Es bedeutet nur „Netzwerk-Verwaltung ist gestartet”, nicht „es gibt eine IP-Adresse”. Dafür gibt es network-online.target, und zwar als Paar: Wants=network-online.target plus After=network-online.target. Für einen Webserver, der nur lokal auf einem Port lauscht, reicht network.target völlig.
[Service]: Was läuft, und wie
[Service]
Type=simple
User=meineapp
Group=meineapp
WorkingDirectory=/opt/meine-app
ExecStart=/usr/bin/node /opt/meine-app/server.js
Environment=PORT=3000 NODE_ENV=production
EnvironmentFile=/etc/meine-app/env
Restart=on-failure
RestartSec=2
ExecStart=: Der Befehl. Immer mit absolutem Pfad zum Programm (/usr/bin/node, nichtnode). systemd nutzt keine Shell, also funktionieren~,&&, Pipes und$(...)hier nicht. Brauchst du die, schreibExecStart=/bin/sh -c '...'oder besser ein kleines Startskript.User=/Group=: Unter welchem Benutzer der Dienst läuft. Fehlt die Zeile, läuft er als root. Dazu unten mehr, denn genau hier haben wir bei uns selbst ein Problem gefunden.WorkingDirectory=: Das Arbeitsverzeichnis. Wichtig für Programme, die relative Pfade wie./databenutzen.Environment=/EnvironmentFile=: Umgebungsvariablen. Passwörter und API-Schlüssel gehören in eineEnvironmentFilemit Rechten600, nicht direkt in die Unit, denn die Unit kann jeder Benutzer mitsystemctl catlesen.Restart=undRestartSec=: Was nach einem Ende passiert. Dazu gleich ein eigener Abschnitt mit Messungen.
Daneben gibt es ExecStartPre= (läuft vorher, etwa für eine Konfigurationsprüfung), ExecReload= (für systemctl reload) und ExecStop= (falls dein Programm einen speziellen Stopp-Befehl braucht; sonst schickt systemd SIGTERM und nach 90 Sekunden SIGKILL).
[Install]: Wann es beim Booten startet
[Install]
WantedBy=multi-user.target
Dieser Abschnitt wird nur von systemctl enable gelesen. WantedBy=multi-user.target heißt: „Wenn das System normal hochfährt, starte mich mit.” Für fast alle Server-Dienste ist das die richtige Zeile. Für grafische Anwendungen auf einem Desktop wäre es graphical.target.
Fehlt der [Install]-Abschnitt, kannst du den Dienst zwar starten, aber nicht aktivieren. systemctl enable meldet dann, dass die Unit „keine Installationsangaben” hat.
Welche Service-Typen gibt es?
Type= sagt systemd, woran es erkennt, dass dein Dienst „fertig gestartet” ist. Das ist wichtig, weil andere Units mit After= darauf warten.
| Type | Wann „gestartet”? | Wofür |
|---|---|---|
simple (Standard) | Sofort, sobald der Prozess erzeugt wurde | Die meisten eigenen Apps (Node, Python, Go) |
exec | Sobald das Programm erfolgreich ausgeführt wurde | Wie simple, meldet aber Fehler wie „Datei nicht gefunden” sauberer |
forking | Wenn der Startprozess sich in den Hintergrund verabschiedet | Alte Daemons, die sich selbst „forken” |
oneshot | Wenn der Befehl beendet ist | Skripte, die einmal laufen (z. B. von einem Timer) |
notify | Wenn das Programm selbst „bereit” meldet | Programme mit systemd-Unterstützung (nginx, PostgreSQL) |
Wenn du unsicher bist: Nimm exec oder simple. Und wichtig: Bei simple/exec darf dein Programm nicht selbst in den Hintergrund gehen. Wer sein Startskript mit & am Ende schreibt oder --daemon setzt, bekommt einen Dienst, der für systemd sofort „beendet” ist, während der eigentliche Prozess irgendwo verwaist weiterläuft.
systemctl: die wichtigsten Befehle
sudo systemctl start meine-app # starten
sudo systemctl stop meine-app # stoppen
sudo systemctl restart meine-app # neu starten
sudo systemctl reload meine-app # Konfiguration neu laden (wenn ExecReload= existiert)
sudo systemctl enable meine-app # beim Booten starten
sudo systemctl disable meine-app # nicht mehr beim Booten starten
sudo systemctl enable --now meine-app # aktivieren UND sofort starten
systemctl status meine-app # Zustand + letzte Logzeilen
systemctl is-active meine-app # nur "active"/"inactive"/"failed" (gut für Skripte)
systemctl list-units --type=service --state=failed # alles, was gerade kaputt ist
sudo systemctl daemon-reload # nach JEDER Änderung an einer Unit-Datei
enable und start sind zwei verschiedene Dinge, das verwirrt anfangs fast jeden. start startet jetzt, enable sorgt nur dafür, dass der Dienst beim nächsten Booten startet. Ein Dienst kann aktiviert und gestoppt sein, oder laufend und nicht aktiviert. Letzteres ist die klassische Falle: Läuft wunderbar, bis zum ersten Neustart des Servers.
Die daemon-reload-Falle, live erwischt
Wir haben in unserem Test das Speicherlimit von 100M auf 90M geändert und dann nur systemctl restart aufgerufen, ohne daemon-reload. Ergebnis: systemd gab eine Warnung aus („The unit file … changed on disk. Run ‘systemctl daemon-reload’”), startete den Dienst aber trotzdem neu, und zwar mit dem alten Limit von 100 MB. Erst nach daemon-reload stand dort 90 MB.
Die Warnung erscheint nur im Terminal. In einem Deploy-Skript, das die Ausgabe nicht anzeigt, läuft dein Dienst danach mit der alten Konfiguration, und alles sieht gut aus. Deshalb: Nach jeder Änderung an einer Unit-Datei immer daemon-reload.
Logs: journalctl statt Logdateien
Alles, was dein Programm auf die Standardausgabe oder Standardfehlerausgabe schreibt (console.log, print, echo), landet automatisch im Journal. Du brauchst keine eigenen Logdateien.
journalctl -u meine-app # alle Logs des Dienstes
journalctl -u meine-app -f # live mitlesen (wie tail -f)
journalctl -u meine-app -n 50 # die letzten 50 Zeilen
journalctl -u meine-app --since "1 hour ago"
journalctl -u meine-app -b # nur seit dem letzten Booten
journalctl -u meine-app -p err # nur Fehler
Ein Python-Stolperstein, den wir selbst kennen: Python puffert seine Ausgabe, wenn sie nicht in ein Terminal geht. Dann siehst du im Journal minutenlang nichts. Lösung: print(..., flush=True), oder Environment=PYTHONUNBUFFERED=1 in die Unit.
Damit das Journal nicht unbegrenzt wächst, kannst du in /etc/systemd/journald.conf eine Obergrenze wie SystemMaxUse=1G setzen. Wie viel Platz es gerade belegt, zeigt journalctl --disk-usage.
Neustart bei Absturz: was Restart= wirklich tut
Das ist der Grund, warum die meisten Leute überhaupt einen systemd Service erstellen: Der Dienst soll von selbst wieder hochkommen. Die Optionen:
| Restart= | Startet neu bei … |
|---|---|
no (Standard!) | nie |
on-failure | Exit-Code ungleich 0, Absturz durch Signal (z. B. SIGKILL, SIGSEGV), Timeout |
on-abnormal | Signal oder Timeout, aber nicht bei Exit-Code ungleich 0 |
on-abort | nur bei unerwartetem Signal |
always | immer, auch nach sauberem Beenden |
Wichtig: Der Standard ist no. Wer Restart= weglässt, hat keinen automatischen Neustart.
Bei uns verteilt sich das so: Von unseren Service-Dateien mit Restart-Zeile nutzen 74 always, 26 on-failure und eine on-abort.
Unser Test: kill -9 gegen kill
Wir haben einen kleinen Python-Webserver als Dienst mit Restart=on-failure und RestartSec=2 gestartet und dann zwei Arten von „Absturz” ausgelöst.
kill -9 (SIGKILL): Das Journal zeigte Main process exited, code=killed, status=9/KILL, dann Failed with result 'signal', und genau zwei Sekunden später Scheduled restart job, restart counter is at 1. Der Dienst lief wieder, mit neuer Prozessnummer.
kill ohne Option (SIGTERM): Der Dienst stand danach auf inactive, Meldung Deactivated successfully. Kein Neustart. Für systemd sind SIGTERM, SIGINT, SIGHUP und SIGPIPE ein sauberes Ende, denn genau so stoppt systemd selbst Dienste.
Das ist kein Fehler, aber es überrascht. Wenn irgendein Prozess (ein anderes Skript, ein übereifriger Admin, ein OOM-Mechanismus außerhalb von systemd) deinen Dienst mit SIGTERM beendet, bleibt er bei on-failure aus. Wer das nicht will, nimmt Restart=always. systemctl stop funktioniert auch dann zuverlässig: systemd weiß, dass es selbst gestoppt hat, und startet nicht neu.

Die Absturzschleife: wann systemd aufgibt
Zweiter Test: Wir haben ExecStart= durch ein Programm ersetzt, das sofort mit Exit-Code 3 endet, und RestartSec=100ms gesetzt. Nach vier Sekunden stand im Journal:
gm-demo.service: Scheduled restart job, restart counter is at 5.
gm-demo.service: Start request repeated too quickly.
gm-demo.service: Failed with result 'exit-code'.
Failed to start gm-demo.service - getmind systemd demo.
systemctl show bestätigte: NRestarts=5, StartLimitBurst=5, StartLimitIntervalUSec=10s. Das sind die Standardwerte. Mehr als fünf Starts innerhalb von zehn Sekunden, dann ist Schluss. Der Dienst steht auf failed und bleibt dort. systemd versucht es nie wieder von selbst, und es schickt dir auch keine Nachricht.
Das ist eine bewusste Schutzfunktion (ein kaputter Dienst soll den Server nicht in einer Endlosschleife beschäftigen). Aber in Kombination mit einem kurzen RestartSec= kann eine kurze Störung (Datenbank startet langsamer, Netz ist kurz weg) deinen Dienst dauerhaft lahmlegen. Unsere Empfehlung:
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=10
[Service]
Restart=on-failure
RestartSec=5
Achtung: StartLimitIntervalSec= und StartLimitBurst= gehören in [Unit], nicht in [Service]. In alten Anleitungen steht noch StartLimitInterval= im Service-Abschnitt, das ist veraltet.
Nach einem Start-Limit-Fehler setzt du den Zähler so zurück:
sudo systemctl reset-failed meine-app
sudo systemctl start meine-app
Und damit du von so einem Ausfall erfährst, kannst du mit OnFailure= eine andere Unit auslösen lassen, die dir zum Beispiel eine Mail oder Chat-Nachricht schickt:
[Unit]
OnFailure=benachrichtigung@%n.service
%n wird dabei durch den Namen der ausgefallenen Unit ersetzt. Ein einfacheres Mittel für den Anfang: regelmäßig systemctl list-units --state=failed prüfen.
Als welcher Benutzer läuft dein Dienst?
Fehlt User=, läuft der Dienst als root. Das heißt: Eine Sicherheitslücke in deiner App ist sofort eine Lücke mit vollen Rechten auf dem ganzen Server. Wie schnell das real wird, haben wir in unserem Bericht über einen fünf Tage gekaperten Server beschrieben.
Dann haben wir bei uns nachgezählt, und es war unangenehm: Von 113 Service-Dateien in /etc/systemd/system/ haben 89 gar keine User=-Zeile, weitere 18 setzen ausdrücklich User=root. Nur 6 laufen unter einem eigenen Benutzer. Der Grund ist der übliche: Beim schnellen Aufsetzen läuft es als root sofort, mit eigenem Benutzer muss man erst Rechte auf Verzeichnisse vergeben. Und danach fasst man die Datei nie wieder an.
So geht es richtig:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin meineapp
sudo chown -R meineapp:meineapp /opt/meine-app/data
[Service]
User=meineapp
Group=meineapp
Noch einfacher ist DynamicUser=yes: systemd erzeugt beim Start einen Wegwerf-Benutzer mit einer zufälligen ID und löscht ihn danach wieder. In unserem Test lief der Dienst damit als Benutzer-ID 61657 statt 0. Braucht der Dienst dauerhaft beschreibbare Daten, gibst du sie mit StateDirectory=meine-app an. systemd legt dann /var/lib/meine-app an und gibt dem dynamischen Benutzer die Rechte darauf.
Programme, die einen Port unter 1024 brauchen (etwa 80 oder 443), müssen dafür übrigens nicht als root laufen: AmbientCapabilities=CAP_NET_BIND_SERVICE reicht. Oder du lässt sie auf einem hohen Port lauschen und stellst einen Reverse Proxy davor, was wir meistens tun.
systemd-Service härten: von 9,6 auf 1,5
systemd bringt einen eingebauten Sicherheits-Check mit:
systemd-analyze security meine-app
Er listet rund 80 Einstellungen auf und vergibt eine Gesamtnote von 0 (sehr gut abgeschottet) bis 10 (alles erlaubt). Eine normale Unit wie aus der Kurzanleitung oben bekommt 9,6, „UNSAFE”.
Wir haben alle unsere eigenen Units durch den Check geschickt. Ergebnis: 104 Units bewertet, Median 9,6. Unter 5 lag keine einzige außer unserem Test-Service. Zum Vergleich: Die systemd-eigenen Dienste auf demselben System (systemd-resolved, systemd-timesyncd, systemd-networkd) lagen zwischen 2,1 und 2,8. Auch die Unit, die Ubuntu für SSH mitliefert, bekommt 9,6, und Caddy kommt auf 8,8. Ein hoher Wert ist also eher die Regel als die Ausnahme. Er heißt nicht, dass der Dienst gehackt ist, sondern dass er im Fall eines Einbruchs fast alles darf.
Diese Zeilen haben wir unserem Test-Service hinzugefügt:
[Service]
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
CapabilityBoundingSet=
UMask=0077
Ergebnis: 1,5, „OK”. Und der Dienst beantwortete Anfragen genau wie vorher.
Was die wichtigsten Zeilen bewirken:
ProtectSystem=strict: Das gesamte Dateisystem ist für den Dienst schreibgeschützt. Schreiben darf er nur dort, wo du es mitReadWritePaths=oderStateDirectory=erlaubst.ProtectHome=yes:/homeund/rootsind unsichtbar. Eine gekaperte Web-App kann dann keine SSH-Schlüssel aus Home-Verzeichnissen lesen.PrivateTmp=yes: Ein eigenes, leeres/tmpnur für diesen Dienst.NoNewPrivileges=yes: Der Prozess kann sich nie mehr Rechte verschaffen, auch nicht über setuid-Programme wiesudo.RestrictAddressFamilies=: Nur die genannten Netzwerkarten. Ein Webdienst braucht kein Bluetooth und keine Raw-Sockets.SystemCallFilter=@system-service: Eine von systemd gepflegte Liste der Systemaufrufe, die normale Dienste brauchen. Alles andere wird blockiert.
Ehrlicher Hinweis: Nicht jede Zeile passt zu jedem Programm. MemoryDenyWriteExecute=yes zum Beispiel verträgt sich nicht mit Programmen, die zur Laufzeit Maschinencode erzeugen, darunter Node.js und Java (JIT-Compiler). Unser Test-Dienst war in Python geschrieben, da ging es. Bei Node musst du diese Zeile weglassen. Geh deshalb schrittweise vor: ein paar Zeilen hinzufügen, daemon-reload, neu starten, Funktion testen, Journal lesen. Wenn etwas blockiert wird, steht es meistens dort als „Permission denied” oder „Operation not permitted”.

Ressourcen begrenzen: Speicher und CPU
systemd steckt jeden Dienst in eine eigene Kontrollgruppe (cgroup). Darüber kannst du ihm Grenzen setzen:
[Service]
MemoryMax=500M
CPUQuota=50%
TasksMax=100
Wir haben das mit einem Programm getestet, das in einer Schleife immer mehr Speicher belegt, gestartet mit MemoryMax=50M (und ohne Swap). Nach rund 40 MB hatte es ein Ende:
gm-hog.service: A process of this unit has been killed by the OOM killer.
gm-hog.service: Main process exited, code=killed, status=9/KILL
gm-hog.service: Failed with result 'oom-kill'.
Getroffen wurde nur dieser eine Dienst, der Rest des Servers merkte nichts. Ohne Limit hätte das Programm so lange Speicher gefressen, bis der Kernel irgendeinen Prozess abschießt, und das ist nicht immer der Schuldige. Wie viel RAM ein Server überhaupt braucht, haben wir in einem eigenen Artikel gemessen.
Praktisch zum Ausprobieren ist systemd-run: Es startet einen Befehl als vorübergehenden Dienst, ganz ohne Unit-Datei:
sudo systemd-run --unit=test -p MemoryMax=50M /usr/bin/python3 skript.py
Den aktuellen Verbrauch aller Dienste zeigt systemd-cgtop.
Ein unsichtbarer Fehler, den wir bei uns gefunden haben
Bevor du eine Unit aktivierst, lohnt sich ein Befehl, den kaum jemand kennt:
systemd-analyze verify /etc/systemd/system/meine-app.service
Als wir unseren Test-Service damit geprüft haben, meldete der Befehl einen Fehler in einer ganz anderen Unit auf unserem Server, einer Drop-in-Datei eines Produktivdienstes:
override.conf:6: Invalid environment assignment, ignoring: Maps
override.conf:6: Invalid environment assignment, ignoring: <hello@...>
In der Datei stand eine Zeile wie Environment=MAIL_FROM=Projektname Maps <hello@...>, ohne Anführungszeichen. systemd trennt Environment= an Leerzeichen. Die Variable MAIL_FROM enthielt deshalb nur das erste Wort des Absendernamens, der Rest wurde verworfen. Der Dienst lief die ganze Zeit, systemctl status war grün, nirgends ein Fehler. Die Meldung erscheint nur beim Laden der Konfiguration im Journal, und dort sucht niemand.
Richtig wäre:
Environment="MAIL_FROM=Projektname Maps <hello@example.com>"
Oder, sauberer: solche Werte in eine EnvironmentFile auslagern, dort gelten die üblichen Anführungsregeln. Wir haben das an dem Produktivdienst für diesen Artikel bewusst nicht im Vorbeigehen geändert, sondern als Aufgabe notiert. Der Punkt ist: Ein laufender Dienst ist kein Beweis für eine richtige Konfiguration.
systemd Timer: der moderne Cronjob
Ein Timer ist eine zweite Unit-Datei, die einen Service nach Zeitplan auslöst. Beispiel: ein Backup-Skript jeden Tag um 3:15 Uhr.
/etc/systemd/system/backup.service:
[Unit]
Description=Nächtliches Backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
/etc/systemd/system/backup.timer:
[Unit]
Description=Backup jeden Tag um 3:15
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=5m
[Install]
WantedBy=timers.target
Aktivieren musst du den Timer, nicht den Service:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
Den Ausdruck in OnCalendar= kannst du vorher prüfen, das hat uns schon mehrmals vor falschen Zeitplänen bewahrt:
systemd-analyze calendar 'Mon..Fri 03:15'
# Normalized form: Mon..Fri *-*-* 03:15:00
# Next elapse: Tue 2026-10-06 03:15:00 UTC
In unserem Test lief ein Timer mit OnCalendar=*:*:0/20 (alle 20 Sekunden) exakt um 07:03:40, 07:04:00 und 07:04:20. Dafür mussten wir allerdings AccuracySec=1s setzen. Standardmäßig fasst systemd Timer in einem Fenster von einer Minute zusammen, um Strom zu sparen. Für ein nächtliches Backup egal, für sekundengenaue Abläufe nicht.
Was Timer gegenüber Cron besser machen: Ausgabe landet automatisch im Journal, Persistent=true holt verpasste Läufe nach (Server war um 3:15 aus), Läufe können sich nicht überlappen (ein Service läuft nie doppelt), und die Härtungs- und Ressourcenoptionen aus diesem Artikel gelten auch hier. Was Cron besser macht: eine Zeile statt zwei Dateien. Für einfache Aufgaben ist Cron völlig in Ordnung.

User-Services: ohne root, im eigenen Konto
Du hast keinen root-Zugriff, oder der Dienst soll ausdrücklich unter deinem eigenen Benutzer laufen? Dann gibt es User-Units:
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/meine-app.service
systemctl --user daemon-reload
systemctl --user enable --now meine-app
journalctl --user -u meine-app
Die Datei sieht genauso aus, nur WantedBy=default.target statt multi-user.target, und User= entfällt.
Die Falle: User-Services laufen standardmäßig nur, solange du eingeloggt bist. Loggst du dich per SSH aus, stoppt systemd sie. Damit sie auch ohne Login und nach einem Neustart laufen, brauchst du Lingering:
sudo loginctl enable-linger deinbenutzer
loginctl show-user deinbenutzer -p Linger # Linger=yes
Auf unserem eigenen Server läuft genau so ein Fall: Ein zentraler Dienst ist eine User-Unit, und ohne Linger wäre er nach jedem Neustart weg gewesen, bis sich jemand anmeldet. Merke außerdem: systemctl restart xy und systemctl --user restart xy sind zwei verschiedene Welten. Wer den falschen Befehl nimmt, startet den falschen (oder gar keinen) Dienst neu.
Kurze deutsche Praxis-Anleitung von CoderTALK (Jan Hill): eine eigene Anwendung mit Autostart als systemd-Dienst einrichten. Ergänzt die Kurzanleitung oben gut, wenn du lieber zuschaust.
Fehlersuche: wenn der Service nicht startet
Wenn systemctl start scheitert, gehen wir immer in dieser Reihenfolge vor:
systemctl status meine-app: zeigt den Zustand, den Exit-Code und die letzten zehn Logzeilen. Oft steht die Antwort schon da.journalctl -u meine-app -n 100 --no-pager: mehr Kontext.systemd-analyze verify /etc/systemd/system/meine-app.service: Tippfehler und ungültige Zeilen.- Den
ExecStart=-Befehl von Hand ausführen, und zwar als derselbe Benutzer:sudo -u meineapp /usr/bin/node /opt/meine-app/server.js. Läuft es so nicht, liegt es nicht an systemd.
Die häufigsten Fehlermeldungen und was sie bedeuten:
| Meldung | Ursache |
|---|---|
status=203/EXEC | Programm nicht gefunden oder nicht ausführbar. Relativer Pfad, fehlendes chmod +x oder falsche Shebang-Zeile |
status=200/CHDIR | WorkingDirectory= existiert nicht oder ist für den Benutzer nicht zugänglich |
status=217/USER | Der Benutzer aus User= existiert nicht |
Start request repeated too quickly | Start-Limit erreicht (siehe oben), erst reset-failed |
Unit … not found | Datei falsch benannt (Endung .service?) oder daemon-reload vergessen |
| Läuft per Hand, nicht als Dienst | Fehlende Umgebungsvariablen (PATH, HOME), anderer Benutzer, anderes Arbeitsverzeichnis |
Der letzte Punkt ist der tückischste. Ein Dienst startet mit einer fast leeren Umgebung, ohne deine .bashrc, ohne die Pfade, die dein Node-Versionsmanager dort einträgt. Darum funktioniert node im Terminal, aber nicht in ExecStart=. Mit which node findest du den absoluten Pfad heraus.
systemd Service vs. Docker vs. PM2
Muss es überhaupt eine systemd-Unit sein? Kurze Einordnung:
- systemd: Ist schon da, kostet nichts, kann Neustart, Logs, Ressourcen-Limits, Härtung und Zeitpläne. Ideal für einzelne Programme direkt auf dem Server.
- Docker / Podman: Sinnvoll, wenn eine App viele Abhängigkeiten mitbringt oder auf mehreren Servern identisch laufen soll. Podman kann Container sogar selbst als systemd-Units betreiben (Quadlet), mehr dazu in Docker vs. Podman. Den Docker-Daemon selbst startet übrigens auch: systemd.
- PM2, forever, supervisord: Prozessmanager aus der Zeit vor systemd oder für Systeme ohne systemd. Auf einem normalen Linux-Server sind sie eine zusätzliche Schicht, die auch wieder gestartet werden muss (meist mit einer systemd-Unit).
Unser Weg: Einzelne Node- und Python-Dienste laufen bei uns direkt als systemd-Units, größere Pakete mit Datenbank und Nebenkram als Container.
Häufige Fragen
Wie erstelle ich einen systemd Service?
Lege eine Datei /etc/systemd/system/NAME.service mit den Abschnitten [Unit], [Service] (mindestens ExecStart= mit absolutem Pfad) und [Install] (WantedBy=multi-user.target) an. Danach sudo systemctl daemon-reload und sudo systemctl enable --now NAME.
Wo liegen die systemd-Service-Dateien?
Eigene Units gehören nach /etc/systemd/system/. Units aus Paketen liegen in /usr/lib/systemd/system/ (dort nichts ändern), User-Units in ~/.config/systemd/user/. Mit systemctl cat NAME siehst du, welche Datei wirklich benutzt wird.
Was ist der Unterschied zwischen enable und start?
start startet den Dienst jetzt. enable sorgt dafür, dass er beim nächsten Booten startet. enable --now macht beides.
Muss ich nach jeder Änderung daemon-reload ausführen?
Ja. Ohne systemctl daemon-reload benutzt systemd weiter die alte Version der Datei, auch bei einem restart. In unserem Test lief der Dienst nach einem Neustart ohne Reload weiter mit dem alten Speicherlimit.
Wie starte ich einen Dienst automatisch neu, wenn er abstürzt?
Mit Restart=on-failure (oder Restart=always) und RestartSec=5 im [Service]-Abschnitt. Beachte, dass on-failure bei einem Beenden per SIGTERM nicht neu startet und dass nach 5 Abstürzen in 10 Sekunden standardmäßig endgültig Schluss ist.
Was bedeutet „Start request repeated too quickly”?
Der Dienst ist zu oft hintereinander abgestürzt und hat das Start-Limit erreicht (Standard: 5 Starts in 10 Sekunden). Ursache im Journal suchen, beheben, dann systemctl reset-failed NAME und neu starten. Großzügigere Werte setzt du mit StartLimitIntervalSec= und StartLimitBurst= im [Unit]-Abschnitt.
Wie sehe ich die Logs eines systemd Service?
Mit journalctl -u NAME. Live mitlesen mit -f, die letzten Zeilen mit -n 50, nur seit dem letzten Booten mit -b.
Wie lasse ich einen Dienst als normaler Benutzer laufen?
Entweder User= und Group= in der Unit setzen (der Benutzer muss existieren), oder DynamicUser=yes für einen automatisch erzeugten Wegwerf-Benutzer. Alternativ als User-Unit unter ~/.config/systemd/user/, dann aber mit loginctl enable-linger.
Was ist besser, systemd Timer oder Cronjob?
Timer bieten Logging im Journal, Nachholen verpasster Läufe, Schutz vor Überlappung und dieselben Härtungsoptionen wie Services. Cron ist einfacher (eine Zeile). Für wichtige Aufgaben wie Backups nehmen wir Timer, für Kleinkram reicht Cron.
Wie lösche ich einen systemd Service?
sudo systemctl disable --now NAME, dann die Datei aus /etc/systemd/system/ (und ein eventuelles Verzeichnis NAME.service.d/) entfernen, sudo systemctl daemon-reload und sudo systemctl reset-failed.
Wie sicher ist ein systemd Service standardmäßig?
Nicht besonders. Ohne User= läuft er als root, ohne Härtungsoptionen darf er fast alles. systemd-analyze security NAME zeigt eine Note von 0 bis 10. Eine Standard-Unit hat 9,6, mit den Optionen aus diesem Artikel kamen wir auf 1,5.
Fazit: fünf Zeilen für den Anfang, zwanzig für ein gutes Gewissen
Einen systemd Service erstellen ist schnell gemacht: eine Datei, daemon-reload, enable --now. Damit ist dein Programm schon besser aufgehoben als in jeder screen-Sitzung. Die eigentliche Arbeit fängt danach an, und unsere Tests zeigen, wo die stillen Lücken sitzen:
Restart=ist standardmäßig aus, undon-failurereagiert nicht auf jedes Ende.- Das Start-Limit legt einen Dienst nach fünf schnellen Abstürzen dauerhaft still, ohne dass dir jemand Bescheid sagt.
- Ohne
User=läuft alles als root, und bei uns war das bei 107 von 113 Service-Dateien so. - Härtung kostet 20 Zeilen und hat unseren Test-Dienst von 9,6 auf 1,5 gebracht, ohne dass er etwas davon gemerkt hat.
- Ein grüner
statusist kein Beweis:systemd-analyze verifyhat bei uns eine halbierte Umgebungsvariable gefunden, die bis dahin niemandem aufgefallen war.
Wenn dein Dienst ein Web-Dienst ist, kommt als Nächstes ein Reverse Proxy davor (Caddy oder nginx) und eine Firewall (UFW einrichten). Und wenn dein Dienst SSH-Anmeldungen ins Log schreibt, hilft Fail2ban, daraus Sperren zu machen.