Bestimmte Programme oder selbst entwickelte Skripte möchte man automatisch zusammen mit dem Raspberry Pi starten und ggf. im Hintergrund laufen lassen. Dafür muss nicht zwingend ein vergleichsweise komplexes Systemd-Unit entwickelt werden: Ich zeige dir in diesem Beitrag verschiedene Wege, mit und ohne Docker-Container. Außerdem einen verbotenen, den du besser nicht nutzen solltest, inklusive Erklärung warum.
Wie werden Programme automatisch gestartet?
Der Raspberry Pi nutzt eine eigene Firmware, statt des in der X86-Welt üblichen BIOS/UEFI. Dies startet den Linux-Kernel. Nach Linux übernimmt ein Init-System: Es startet alle weiteren Programme bis zum Anmeldebildschirm und kümmert sich um deren Lebenszyklus (Starten, Stoppen, Neu starten usw).1 Bereits auf einem frisch installierten GNU/Linux-System laufen einige Prozesse im Hintergrund, wie beispielsweise ein SSH-Server für den Fernzugriff oder DHCP-Client, damit automatisch eine IP-Adresse fürs Netzwerk bezogen werden kann. Oder für WLAN-Netzwerke eine Software, welche die gängige WPA-Verschlüsselung unterstützt. Auf Systemen mit grafischer Oberfläche finden sich noch deutlich mehr laufende Programme.
Es gibt verschiedene Init-Systeme.2 Lange Zeit war init.d stark verbreitet, viele Distributionen haben es durch Systemd abgelöst – darunter auch Debian und damit das Raspberry Pi OS. Soll ein weiteres Programm automatisch beim Start des Betriebssystems ebenfalls gestartet werden, muss man dies im Init-System eintragen. Systemd ist mächtig, dadurch auch relativ komplex. Doch es gibt mehrere Alternativen über Programme, die wiederum per Systemd bereites gestartet werden und die Möglichkeit bieten, dort selbst eigene Software zu starten.
Muss ich das überhaupt händisch machen?
Einsteiger sollten wissen, dass APT-Pakete üblicherweise bereits alle nötigen Dateien bei der Installation mitbringen, um sich als Systemd-Dienst zu registrieren. Dieser heißt meist identisch wie die Software. Während der Installation wird teilweise angezeigt, welchen Dienst das Paket anlegt. Folgendes Beispiel zeigt die Ausgabe beim installieren des Webservers Nginx, dieser legt nginx.service
an:
Ansonsten kann man sich mit systemctl --all
sämtlich registrierten Units genannten Einheiten anzeigen und diese mit grep
durchsuchen:
u-labs@pi4:~ $ systemctl --all | grep nginx
nginx.service loaded active running A high performance web server and a reverse proxy server
Oft legen APT-Pakete nicht nur einen passenden Dienst an, sondern aktivieren & starten diesen automatisch nach der Installation.
u-labs@pi4:~ $ systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2024-01-09 12:06:11 CET; 3min 16s ago
Ansonsten lässt sich der Dienst wie folgt in den Autostart legen und starten, sodass er ohne Neustart des Systems anschließend genutzt werden kann:
sudo systemctl enable --now nginx
Anders sieht es aus, wenn man Programme manuell (an der Paketverwaltung vorbei) installiert. Oder eigene Skripte/Programme geschrieben hat. In diesen Fällen existiert natürlich kein vorbereiteter Systemd-Dienst. Man müsste entweder sich mit Systemd befassen und einen eigenen Entwickeln, oder eine der folgenden Alternativen nutzen. Hierfür schauen wir uns im folgenden mehrere Möglichkeiten an – sortiert nach der Reihenfolge, wie ich diese empfehle.
Beispiel-Szenario
Zur Demonstration habe ich in Python 3 einen kleinen Webserver geschrieben, der jede Anfrage mit einer Willkommensnachricht beantwortet. Dies läuft im Vordergrund, d.H. wenn wir es mit python webserver.py
starten, wird die Konsole blockiert. Ziel ist es, dieses Skript automatisiert im Hintergrund zu starten, sodass der Webserver erreichbar ist.
from http.server import BaseHTTPRequestHandler, HTTPServer
import signal
import sys, os
hostName = "0.0.0.0"
serverPort = 8080
webServer = None
class MyServer(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(bytes("Hallo vom Python Webserver", "utf-8"))
def terminate(signal,frame):
webServer.server_close()
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, terminate)
webServer = HTTPServer((hostName, serverPort), MyServer)
print("Server gestartet: http://%s:%s mit pid %i" % (hostName, serverPort, os.getpid()))
webServer.serve_forever()
#1 Einsatz von (Docker)-Containern
Der Docker-Daemon wird per Systemd gestartet und sorgt wiederum dafür, dass alle Container laufen, bei denen das per Policy festgelegt wurde. Darüber hinaus erlaubt es Docker, ein Programm mit allen Abhängigkeiten zu isolieren. Somit können beispielsweise mehrere Python/PHP Versionen für verschiedene Skripte parallel genutzt werden – das ist in den meisten Fällen die insgesamt beste Methode. Lediglich auf besonders ressourcenarmen Systemen für bestimmte Zwecke macht es Sinn, darauf zu verzichten.
Zur Einrichtung von Docker auf dem Raspberry Pi (und Debian GNU/Linux) habe ich bereits mehrere Beiträge gemacht, in denen sowohl das Konzept hinter Containern, als auch die Installation ausführlich gezeigt wird:
- Vorteile von Containertechnologien wie Docker
- Docker auf dem Raspberry Pi OS (und Debian) installieren
- So schreibst du dein erstes Dockerfile für eigene Images
Für unser Beispielszenario genügt ein einfaches Dockerfile, in dem das zuvor angelegte webserver.py Skript (liegt im gleichen Ordner) in einer kompakten Python3 Umgebung geladen wird:
FROM python:3-alpine
WORKDIR /app
COPY webserver.py .
ENV PYTHONUNBUFFERED true
CMD ["python", "./webserver.py"]
docker-compose.yml:
services:
python-web:
build: .
mem_limit: 128M
restart: always
ports:
- 8080:8080
Wichtig ist hierbei die zuvor erwähnte Restart-Policy: Durch restart: always
wird dieser Container automatisch beim Systemstart gestartet, nachdem wir ihn einmalig erstellen & mit -d
im Hintergrund starten. Der einzige Nachteil dieser Variante: Man kann Abhängigkeiten nur innerhalb der docker-compose.yml angeben. Darüber hinaus ist es nicht möglich, Container in komplexeren Szenarien aufeinander aufbauen zu lassen. In den meisten Fällen wird das ausreichen. Im Einzelfall müsste man ggf. über Startskripte die benötigten Abhängigkeiten (etwa ein MySQL Server) abfragen und damit den Start der gewünschten Anwendung verzögern.
docker compose up -d
Nach dem Neustart des Systems:
u-labs@pi5:~ $ uptime
15:14:44 up 1 min, 3 users, load average: 0.24, 0.18, 0.07
u-labs@pi5:~ $ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a09133f29f95 docker-demo-python-web "python ./webserver.…" 4 minutes ago Up About a minute 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp docker-demo-python-web-
#2 Per Crontab
Der frühere Windows-Nutzer in mir würde sie wohl als Aufgabenverwaltung für GNU/Linux bezeichnen: Eben so wie diese ermöglicht es Cron, bestimmte Programme zu festlegten Uhrzeiten auszuführen – täglich um 02:00 Uhr, jede volle Stunde, alle 10 Minuten usw.3 Mit crontab
können solche Aufgaben angelegt & verändert werden.4 Das für Einsteiger etwas komplexe, aber mächtige Syntax kann teilweise durch Nicknames sprechender gestaltet werden: @hourly
für stündlich, @daily
für täglich usw. Als Alternative dazu bietet Systemd übrigens Timer.5
Relativ unbekannt ist @reboot
: Entgegen des Namens führt es Programme nicht nur beim Neustart aus. Sondern auch beim Kaltstart. Im Gegensatz zu den zeitgesteuerten Aufgaben erfolgt dies nur einmal, d.H. es findet keine periodische Wiederholung statt. Ein paar Dinge sind bei der Verwendung von Cron grundsätzlich zu beachten:
- Jeder Benutzer hat seinen eigenen Crontab. Alle darin enthaltenen Skripte werden in seinem Nutzerkontext ausgeführt.
- Cron verwendet SH als Shell und
$PATH
steht nicht zur Verfügung. Für alle Pfade (auch von Binärdateien wie Python, PHP usw) muss der vollständige Pfad angeben! Für Binärdateien könnt ihr dafürtype <Befehl>
(z.B.type python
) verwenden.
Ruft ihr crontab -e
zum ersten Mal mit einem Nutzer auf und habt keinen Standard Text-Editor mit select-editor
ausgewählt, müsst ihr zuerst einen festlegen. Vim ist am mächtigsten, für Einsteiger ist nano
eine weniger mächtige, allerdings dafür einfach zu bedienende Alternative. Dafür gebt ihr die Ziffer „1“ ein.
u-labs@pi5:~ $ select-editor
Select an editor. To change later, run 'select-editor'.
1. /bin/nano <---- easiest
2. /usr/bin/vim.basic
3. /usr/bin/vim.tiny
4. /bin/ed
Choose 1-4 [1]: n
Mit dem gewählten Editor öffnet Crontab eine temporäre Datei, die ihr wie jede andere Textdatei auch damit bearbeiten könnt. Zum starten des Python-Skriptes wird folgende Zeile hinzugefügt:
@reboot /usr/bin/python /home/u-labs/docker-demo/webserver.py &
Nach dem Neustart läuft der Webserver daher automatisch ohne Zutun auf Port 8080:
u-labs@pi5:~ $ ps -fC python
UID PID PPID C STIME TTY TIME CMD
u-labs 820 1 0 15:32 ? 00:00:00 /usr/bin/python /home/u-labs/docker-demo/webserver.py
u-labs@pi5:~ $ curl http://127.0.0.1:8080
Hallo vom Python Webserver
Nachvollziehen kann man das Ausführen der Cronjobs auch über das Syslog, welches ab Debian (und damit auch Raspberry Pi OS) 12 durch journalctl
ersetzt wurde:6
u-labs@pi5:~ $ journalctl -u cron
- Boot eecb17a7cf8e4eed84ed6b38b3e26a98 --
Jan 09 15:32:22 pi5 systemd[1]: Started cron.service - Regular background program processing daemon.
Jan 09 15:32:22 pi5 cron[700]: (CRON) INFO (pidfile fd = 3)
Jan 09 15:32:22 pi5 cron[700]: (CRON) INFO (Running @reboot jobs)
Jan 09 15:32:22 pi5 CRON[728]: pam_unix(cron:session): session opened for user u-labs(uid=1000) by (uid=0)
Jan 09 15:32:22 pi5 CRON[818]: (u-labs) CMD (/usr/bin/python /home/u-labs/docker-demo/webserver.py &)
Per grep
die Datei /var/log/syslog
zu durchsuchen, ist seit Version 12 daher nicht mehr möglich.
#3 Über die Desktopumgebung
Durch die XDG Autostart Spezifikation7 kann eine .desktop
Datei angelegt werden, wenn ihr die Desktop Edition des Raspberry Pi OS installiert habt. Dies macht vor allem für grafische Programme Sinn. Der Name (alles hinter der Endung) ist frei wählbar, ich nenne sie hier im Beispiel browser.desktop
:
mkdir ~/.config/autostart
nano ~/.config/autostart/browser.desktop
Im Ini-Format lässt sich unter Exec
der gewünschte Befehl angeben. In diesem Beispiel starten wir den Firefox-Browser (seit Raspberry Pi OS 12 vorhanden) und öffnen die U-Labs Startseite.
[Desktop Entry]
Type=Application
Name=Browser starten
Exec=/usr/bin/firefox "https://u-labs.de"
Terminal=false
Sollte dies bei z.B. eigenen Skripten/Programmen nicht funktionieren, sehen wir keine Ausgabe möglicher Fehlermeldungen. Um das zu ändern, installieren wir das grafische Terminal xterm mit sudo apt install xterm
und starten es darin. So öffnet sich beim Start ein Konsolenfenster mit allen Ausgaben:
Exec=xterm -hold -e '/usr/bin/python /home/u-labs/script.py'
So besser nicht: Das /etc/rc.local Skript
Vereinzelt wird noch immer empfohlen, /etc/rc.local
zu verwenden – obwohl sie bereits seit Jahrzehnten veraltet ist.8 In einigen Distributionen ist sie daher nicht vorhanden und wird beim Start nicht aufgerufen. Bei manchen Distributionen funktioniert das dagegen bis heute, weil Debian und Raspberry Pi OS die Abwärtskompatibilität mit einem Systemd-Dienst:
u-labs@pi5:~ $ systemctl status rc-local
● rc-local.service - /etc/rc.local Compatibility
Loaded: loaded (/lib/systemd/system/rc-local.service; enabled-runtime; preset: enabled)
Drop-In: /usr/lib/systemd/system/rc-local.service.d
└─debian.conf
/etc/systemd/system/rc-local.service.d
└─ttyoutput.conf
Active: active (exited) since Wed 2024-01-09 16:40:04 CET; 19min ago
Docs: man:systemd-rc-local-generator(8)
Process: 1300 ExecStart=/etc/rc.local start (code=exited, status=0/SUCCESS)
Theoretisch wäre es hier möglich, einfach im Skript /etc/rc.local
über exit 0
die gewünschten Programme/Skripte einzubinden, damit sie beim Systemstart automatisch aufgerufen werden. Da das Skript bereits seit Jahrzehnten als veraltet markiert ist, steht es unter einigen Distributionen nicht mehr zur Verfügung. Ubuntu hat es beispielsweise bereits 2018 in 18.04 LTS entfernt.9
Wer die vorherigen Alternativen nicht nutzen möchte, dem empfehle ich eine Einarbeitung in Systemd. Einen Anhaltspunkt kann dafür ein Blick in den Dienst liefern, der als Übergangslösung eingerichtet wurde, um rc.local
unter Debian/Raspberry Pi OS nachzurüsten:10
u-labs@pi5:~ $ sudo systemctl cat rc-local.service
# /lib/systemd/system/rc-local.service
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
# This unit gets pulled automatically into multi-user.target by
# systemd-rc-local-generator if /etc/rc.local is executable.
[Unit]
Description=/etc/rc.local Compatibility
Documentation=man:systemd-rc-local-generator(8)
ConditionFileIsExecutable=/etc/rc.local
After=network.target
[Service]
Type=forking
ExecStart=/etc/rc.local start
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no
# /usr/lib/systemd/system/rc-local.service.d/debian.conf
[Unit]
# not specified by LSB, but has been behaving that way in Debian under SysV
# init and upstart
After=network-online.target
# Often contains status messages which users expect to see on the console
# during boot
[Service]
StandardOutput=journal+console
StandardError=journal+console
# /etc/systemd/system/rc-local.service.d/ttyoutput.conf
[Service]
StandardOutput=tty
Quellen
- https://wiki.gentoo.org/wiki/Init_system ↩︎
- https://wiki.gentoo.org/wiki/Comparison_of_init_systems ↩︎
- https://linux.die.net/man/8/cron ↩︎
- https://linux.die.net/man/5/crontab ↩︎
- https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html ↩︎
- https://www.thomas-krenn.com/de/wiki/Abl%C3%B6sung_von_/var/log/syslog_durch_journalctl_in_Debian_12 ↩︎
- https://wiki.archlinux.org/title/XDG_Autostart ↩︎
- https://unix.stackexchange.com/a/471871/214989 ↩︎
- https://wiki.ubuntuusers.de/Archiv/rc.local/ ↩︎
- https://unix.stackexchange.com/a/479766/214989 ↩︎