Eigenen systemd-Dienst anlegen: Skripte dauerhaft starten
Python-Skript oder Bot als systemd-Dienst: startet beim Booten und nach Absturz neu, schreibt Logs. Mit getesteter Unit-Vorlage und Timer.
Von Nathan Base
Dein Python-Skript läuft, solange das Terminal offen ist. Schließt Du die SSH-Sitzung, ist es weg. Startet der Raspberry Pi neu, musst Du es von Hand wieder anwerfen, und stürzt es nachts ab, merkst Du es erst am Morgen. Die Lösung für all das ist ein eigener systemd-Dienst. systemd ist der Dienstverwalter, der auf praktisch jedem aktuellen Linux läuft, auf Raspberry Pi OS genauso wie auf Debian und Ubuntu. Er startet Dein Skript beim Booten, startet es nach einem Absturz neu und sammelt seine Ausgaben in einem Log.
In dieser Anleitung legen wir Schritt für Schritt einen Dienst für ein Python-Skript an, zum Beispiel einen Bot, erklären jede Zeile der Unit-Datei und richten am Ende einen Timer als Ersatz für einen Cronjob ein. Getestet habe ich alle Dateien und Befehle unter Debian 13 „Trixie“ mit systemd 257 auf arm64, also auf derselben Basis wie das aktuelle Raspberry Pi OS. Dort habe ich auch den automatischen Neustart nach einem Absturz und die typischen Fehlermeldungen nachgestellt.
Das Skript an seinen Platz bringen
Ein Dienst sollte nicht aus Deinem Home-Verzeichnis laufen und nicht als root. Wir legen deshalb einen eigenen Systembenutzer an, der sich nicht anmelden kann und nur für dieses Skript da ist. Sein Home-Verzeichnis /opt/meinbot ist gleichzeitig der Ordner für das Skript:
sudo useradd --system --create-home --home-dir /opt/meinbot --shell /usr/sbin/nologin meinbotErsetze meinbot hier und in allen folgenden Befehlen durch einen Namen, der zu Deinem Projekt passt. Kopiere dann Dein Skript in den Ordner und lege eine virtuelle Python-Umgebung an, damit die Pakete des Skripts nicht mit dem System kollidieren:
sudo cp bot.py /opt/meinbot/
sudo python3 -m venv /opt/meinbot/venv
sudo /opt/meinbot/venv/bin/pip install python-telegram-botDie letzte Zeile ist ein Beispiel. Installiere dort die Pakete, die Dein Skript braucht. Geheime Werte wie ein Bot-Token gehören nicht ins Skript, sondern in eine eigene Datei, die nur der Dienstbenutzer lesen darf. Öffne sie mit sudo nano /opt/meinbot/.env und trag eine Zeile pro Wert ein, etwa BOT_TOKEN=123456:ABC. Zum Schluss gibst Du dem Benutzer den Ordner und schützt die Datei mit dem Token:
sudo chown -R meinbot:meinbot /opt/meinbot
sudo chmod 600 /opt/meinbot/.envWie Du so einen Bot überhaupt baust, zeigt Telegram-Bot mit Python bauen: Anleitung für Einsteiger.
Die Unit-Datei schreiben
Eigene Dienste gehören nach /etc/systemd/system/. Der Dateiname ist gleichzeitig der Name des Dienstes und endet auf .service. Öffne die Datei mit:
sudo nano /etc/systemd/system/meinbot.serviceFüge diesen Inhalt ein:
[Unit]
Description=Mein Python-Bot
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
User=meinbot
Group=meinbot
WorkingDirectory=/opt/meinbot
EnvironmentFile=-/opt/meinbot/.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/meinbot/venv/bin/python /opt/meinbot/bot.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.targetSpeichere mit Strg + O, bestätige mit Enter und schließe mit Strg + X.
Was jede Zeile bewirkt
Im Abschnitt [Unit] steht, was der Dienst ist und wann er starten darf:
Descriptionist der Name, den Du später in Statusmeldungen und Logs siehst.Wants=network-online.targetundAfter=network-online.targetsorgen gemeinsam dafür, dass der Bot erst startet, wenn das Netzwerk wirklich steht. Ein Bot, der vor dem WLAN startet, scheitert sonst beim ersten Verbindungsversuch.Afterallein legt nur die Reihenfolge fest, erstWantsholt das Netzwerk-Ziel überhaupt dazu.StartLimitIntervalSec=300undStartLimitBurst=5sind die Notbremse: Stürzt der Dienst innerhalb von fünf Minuten mehr als fünfmal ab, gibt systemd auf, statt ihn endlos neu zu starten.
Im Abschnitt [Service] steht, wie das Skript läuft:
Type=execmeldet den Dienst erst dann als gestartet, wenn das Programm wirklich ausgeführt werden konnte. Ein Tippfehler im Pfad fällt so sofort beim Start auf. Die systemd-Doku empfiehlt diesen Typ für dauerhaft laufende Dienste.UserundGrouplassen das Skript als der eben angelegte Benutzer laufen statt alsroot.WorkingDirectoryist der Ordner, in dem das Skript startet. Relative Pfade im Skript, etwa zu einer Datenbankdatei, beziehen sich darauf.EnvironmentFileliest die Werte aus der.env-Datei als Umgebungsvariablen ein. Das Minus vor dem Pfad bedeutet: Fehlt die Datei, startet der Dienst trotzdem.Environment=PYTHONUNBUFFERED=1sorgt dafür, dass jede Ausgabe vonprint()sofort im Log landet. Ohne diese Zeile puffert Python die Ausgaben, und das Log bleibt scheinbar leer.ExecStartist der eigentliche Befehl, immer mit vollständigen Pfaden. Weil wir den Python-Interpreter aus der virtuellen Umgebung aufrufen, nutzt das Skript automatisch deren Pakete. Einsource venv/bin/activatebrauchst Du nicht.Restart=on-failurestartet das Skript neu, wenn es mit einem Fehler endet, undRestartSec=10wartet davor zehn Sekunden.
Der Abschnitt [Install] mit WantedBy=multi-user.target sagt schließlich, dass der Dienst beim normalen Systemstart mit hochfahren soll.
Prüfen, aktivieren, starten
Bevor Du den Dienst startest, lass systemd die Datei prüfen:
sudo systemd-analyze verify /etc/systemd/system/meinbot.serviceKommt keine Ausgabe, ist alles in Ordnung. Dann liest systemd die neue Datei ein, und Du aktivierst und startest den Dienst in einem Schritt:
sudo systemctl daemon-reload
sudo systemctl enable --now meinbot.serviceenable sorgt für den Start bei jedem Booten, --now startet ihn zusätzlich sofort. Ob er läuft, zeigt:
systemctl status meinbot.serviceIn der Zeile Active muss active (running) stehen, und darunter siehst Du die letzten Ausgaben Deines Skripts. Mit q verlässt Du die Anzeige.
Logs lesen mit journalctl
Alles, was Dein Skript ausgibt, landet im Journal von systemd. Die Ausgaben live mitlesen kannst Du mit:
journalctl -u meinbot.service -fBeenden mit Strg + C. Für die Meldungen seit dem letzten Systemstart nimmst Du journalctl -u meinbot.service -b, für einen Zeitraum etwa journalctl -u meinbot.service --since "1 hour ago".
Den automatischen Neustart habe ich im Test mit einem absichtlichen Absturz geprüft. Das Journal zeigt dann diese Abfolge, und nach zehn Sekunden läuft der Bot wieder:
meinbot.service: Main process exited, code=exited, status=1/FAILURE
meinbot.service: Failed with result 'exit-code'.
meinbot.service: Scheduled restart job, restart counter is at 1.
Started meinbot.service - Mein Python-Bot.Nach jeder Änderung an der Unit-Datei brauchst Du sudo systemctl daemon-reload und danach sudo systemctl restart meinbot.service. Änderst Du nur das Skript, reicht der Neustart.
Häufige Fehler
status=203/EXEC: systemd konnte das Programm aus ExecStart nicht ausführen. Fast immer ist der Pfad falsch, etwa ein Tippfehler im Interpreter oder eine virtuelle Umgebung, die an einem anderen Ort liegt. Prüf den Pfad mit ls -l /opt/meinbot/venv/bin/python. Dank Type=exec meldet schon systemctl start den Fehler, statt einen laufenden Dienst vorzutäuschen.
status=217/USER: Der Benutzer aus User= existiert nicht. Leg ihn an oder korrigiere den Namen.
Permission denied im Log: Der Dienstbenutzer darf eine Datei nicht lesen oder in einen Ordner nicht schreiben. Prüf mit ls -l, wem die Dateien gehören, und gib sie ihm mit chown.
Das Skript läuft von Hand, als Dienst aber nicht: Im Terminal hast Du Umgebungsvariablen, einen PATH und ein Arbeitsverzeichnis, die der Dienst nicht hat. Nutze im Skript vollständige Pfade oder Pfade relativ zu WorkingDirectory, und hinterlege fehlende Variablen in der .env.
Start request repeated too quickly: Der Dienst ist zu oft hintereinander abgestürzt, und die Notbremse hat gegriffen. Such im Journal nach der eigentlichen Ursache, behebe sie und setz den Zähler mit sudo systemctl reset-failed meinbot.service zurück.
Dienste ohne root: User-Services
Wenn Du keine Root-Rechte hast oder ein Skript bewusst unter Deinem eigenen Benutzer laufen soll, gibt es User-Services. Die Datei liegt dann in ~/.config/systemd/user/, die Zeilen User und Group entfallen, und als Ziel unter [Install] trägst Du WantedBy=default.target ein. Bedient wird alles mit systemctl --user statt sudo systemctl, also etwa systemctl --user enable --now meinbot.service.
Ein User-Service läuft normalerweise nur, solange Du angemeldet bist. Damit er beim Booten startet und nach dem Abmelden weiterläuft, erlaubst Du das einmalig mit:
sudo loginctl enable-linger $USERTimer statt Cronjob
Für Aufgaben, die nicht dauerhaft laufen, sondern zu festen Zeiten, bietet systemd Timer. Sie haben gegenüber cron zwei Vorteile: Die Ausgaben landen im Journal, und ein verpasster Lauf wird nachgeholt, wenn der Pi zur geplanten Zeit aus war. Ein Timer besteht aus zwei Dateien mit demselben Namen. Die erste beschreibt die Aufgabe, hier das Löschen alter Logdateien. Leg sie mit sudo nano /etc/systemd/system/aufraeumen.service an:
[Unit]
Description=Alte Logdateien aufräumen
[Service]
Type=oneshot
ExecStart=/usr/bin/find /var/log/meinbot -name "*.log" -mtime +14 -deleteDer Befehl löscht alle .log-Dateien, die älter als 14 Tage sind. Ersetze /var/log/meinbot durch den Ordner, in den Dein Skript seine Logdateien schreibt, oder trag in ExecStart einen ganz anderen Befehl ein, etwa den Aufruf eines Backup-Skripts. Type=oneshot bedeutet, dass der Befehl einmal läuft und dann endet. Die zweite Datei legt fest, wann. Öffne sie mit sudo nano /etc/systemd/system/aufraeumen.timer:
[Unit]
Description=Täglich alte Logdateien aufräumen
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.targetOnCalendar=*-*-* 03:00:00 heißt jeden Tag um 3 Uhr. Persistent=true holt einen verpassten Lauf nach, und RandomizedDelaySec verteilt den Start zufällig auf bis zu zehn Minuten danach. Aktiviert wird der Timer, nicht der Dienst:
sudo systemctl daemon-reload
sudo systemctl enable --now aufraeumen.timer
systemctl list-timers aufraeumen.timerDie letzte Zeile zeigt in der Spalte NEXT, wann der Timer das nächste Mal auslöst. Ob ein Zeitausdruck das meint, was Du denkst, prüfst Du vorher mit systemd-analyze calendar "Mon..Fri *-*-* 07:30". Die Ausgabe nennt den nächsten Termin, in diesem Beispiel den nächsten Werktag um 7:30 Uhr.
Für einfache Aufgaben bleibt cron trotzdem eine gute Wahl, siehe Anleitung: Cronjobs unter Raspberry Pi OS einrichten. Am Mac übernimmt launchd die Rolle von systemd, das zeigt Hintergrund-Jobs am Mac mit launchd statt cron.
Weiterlesen.
-
Selfhosting & WebTelegram-Bot mit Python bauen: Anleitung für Einsteiger
Eigenen Telegram-Bot mit Python und python-telegram-bot 22 bauen: Token vom BotFather, Befehle, Antworten und Dauerbetrieb auf dem Raspberry Pi.
Nathan Base -
Raspberry Pi & LinuxAnleitung: Cronjobs unter Raspberry Pi OS einrichten
Cronjobs unter Raspberry Pi OS Trixie einrichten: Zeitzone setzen, crontab verstehen, Bash- und Python-Scripts samt venv starten und Fehler protokollieren.
Nathan Base -
Apple MacHintergrund-Jobs am Mac mit launchd statt cron
Skripte am Mac per Zeitplan oder beim Anstecken eines Laufwerks starten: LaunchAgent mit launchd anlegen, testen und Fehler finden.
Nathan Base -
Raspberry Pi & LinuxRaspberry Pi 5: Unterspannung und das richtige Netzteil
Unterspannung am Raspberry Pi 5 erkennen: vcgencmd get_throttled Bit für Bit dekodiert, die 5-A-Regel erklärt, usb_max_current_enable richtig genutzt.
Nathan Base -
Raspberry Pi & LinuxRaspberry Pi startet nicht: LED-Blinkcodes und Lösungen
Dein Raspberry Pi bootet nicht? Alle LED-Blinkcodes von Pi 4 und Pi 5 auf Deutsch, dazu Netzteil, SD-Karte, Bootloader und Boot-Reihenfolge prüfen.
Nathan Base -
Raspberry Pi & LinuxMit Raspberry Pi und Server Geld verdienen: Was 2026 geht
Lässt sich mit einem Raspberry Pi oder Server 2026 noch Geld verdienen? Mining, Solo-Lotterie und Bandbreite vermieten im ehrlichen Vergleich.
Nathan Base