3 Wege +1 verbotener, um Programme auf dem Raspberry Pi OS automatisch zu starten

Als Video ansehen
Bereitgestellt über YouTube

3 Wege +1 verbotener, um Programme auf dem Raspberry Pi OS automatisch zu starten

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:

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:

  1. Jeder Benutzer hat seinen eigenen Crontab. Alle darin enthaltenen Skripte werden in seinem Nutzerkontext ausgeführt.
  2. 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ür type <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

  1. https://wiki.gentoo.org/wiki/Init_system ↩︎
  2. https://wiki.gentoo.org/wiki/Comparison_of_init_systems ↩︎
  3. https://linux.die.net/man/8/cron ↩︎
  4. https://linux.die.net/man/5/crontab ↩︎
  5. https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html ↩︎
  6. https://www.thomas-krenn.com/de/wiki/Abl%C3%B6sung_von_/var/log/syslog_durch_journalctl_in_Debian_12 ↩︎
  7. https://wiki.archlinux.org/title/XDG_Autostart ↩︎
  8. https://unix.stackexchange.com/a/471871/214989 ↩︎
  9. https://wiki.ubuntuusers.de/Archiv/rc.local/ ↩︎
  10. https://unix.stackexchange.com/a/479766/214989 ↩︎

Leave a Reply