Raspberry Pi

Python-MQTT-Skript beim Booten starten: systemd-Dienst auf dem Raspberry Pi

Machen Sie aus einem Python-MQTT-Client einen sauberen systemd-Dienst auf dem Raspberry Pi: Start erst nach Netzwerk und Uhrzeit, automatischer Neustart, Zugangsdaten in einer Umgebungsdatei und sauberes Herunterfahren.

Raspberry-Pi-Terminal mit einem systemd-Dienst, der beim Booten einen Python-Client startet – Python-MQTT-Skript Autostart
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Der zuverlässige Weg, ein Python-MQTT-Skript beim Booten zu starten, ist ein systemd-Dienst: Er startet Ihren Client, sobald das Netzwerk steht, startet ihn nach einem Absturz neu, erfasst seine Logs und beendet ihn beim Herunterfahren sauber. In diesem Tutorial machen Sie aus einem paho-mqtt-Skript einen Dienst auf dem Raspberry Pi, legen Zugangsdaten außerhalb des Codes ab, lösen das Uhrzeitproblem beim Booten, das TLS scheitern lässt, und lernen die Befehle für den täglichen Betrieb. Dieselbe Unit-Datei funktioniert auf jeder modernen Linux-Distribution – vom Maker-Projekt bis zum Edge-Gateway in der Fertigungshalle.

Warum systemd und nicht cron oder rc.local

Es gibt mehrere Wege, ein Skript beim Booten zu starten, und sie sind nicht gleichwertig:

Methode Neustart nach Absturz Wartet auf Netzwerk Logs Sauberes Herunterfahren
@reboot in der crontab Nein Nein Nur bei umgeleiteter Ausgabe Nein
/etc/rc.local Nein Unzuverlässig Nein Nein
Desktop-Autostart Nein Nein Nein Nur mit Desktop-Sitzung
systemd-Dienst Ja, konfigurierbar Ja, über Abhängigkeiten journald integriert Ja, SIGTERM, dann SIGKILL

Ein crontab-Eintrag wirkt einfacher, doch das Ergebnis ist ein MQTT-Gateway, das um 3 Uhr nachts still stirbt und nie zurückkommt. systemd läuft unter Raspberry Pi OS bereits, es muss also nichts installiert werden.

Schritt 1: Das Skript vorbereiten

Ein Skript, das als Dienst läuft, sollte seine Konfiguration aus Umgebungsvariablen lesen, sich selbstständig neu verbinden und sich sauber beenden, wenn systemd SIGTERM sendet. Dieses Beispiel publiziert alle 30 Sekunden einen Heartbeat mit der Laufzeit und nutzt einen Last Will, damit Subscriber sehen, wenn das Gerät offline geht.

#!/usr/bin/env python3
import json
import os
import signal
import threading
import time

import paho.mqtt.client as mqtt

HOST = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.environ["MQTT_CLIENT_ID"]            # muss gesetzt und eindeutig sein
PREFIX = os.getenv("TOPIC_PREFIX", "connectmq/demo/s8t4n6/pi1")
INTERVAL = int(os.getenv("PUBLISH_INTERVAL", "30"))

stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *_: stop.set())
signal.signal(signal.SIGINT, lambda *_: stop.set())


def on_connect(client, userdata, flags, reason_code, properties):
    print(f"connected to {HOST}:{PORT} ({reason_code})", flush=True)
    client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)


def on_disconnect(client, userdata, flags, reason_code, properties):
    print(f"disconnected ({reason_code}), will retry", flush=True)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
if USERNAME:
    client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
    client.tls_set()
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.connect_async(HOST, PORT, keepalive=30)
client.loop_start()

start = time.monotonic()
while not stop.wait(INTERVAL):
    payload = {"uptime_s": int(time.monotonic() - start), "ts": int(time.time())}
    client.publish(f"{PREFIX}/heartbeat", json.dumps(payload), qos=1)

print("stopping", flush=True)
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(3)
client.disconnect()
client.loop_stop()

connect_async() ist hier entscheidend. Ein einfaches connect() löst eine Exception aus, wenn der Broker in genau diesem Moment nicht erreichbar ist – in den ersten Sekunden nach dem Booten ist das häufig der Fall. connect_async() übergibt den Verbindungsaufbau an die Netzwerkschleife, die mit dem über reconnect_delay_set() eingestellten Backoff erneut versucht.

Schritt 2: An einem stabilen Ort installieren

Betreiben Sie Dienste nicht aus dem Downloads-Ordner Ihres Home-Verzeichnisses. Geben Sie dem Gateway ein eigenes Verzeichnis, eine eigene virtuelle Umgebung und einen eigenen Systembenutzer:

sudo useradd --system --home /opt/mqtt-client --shell /usr/sbin/nologin mqttclient
sudo mkdir -p /opt/mqtt-client
sudo cp heartbeat.py /opt/mqtt-client/
sudo python3 -m venv /opt/mqtt-client/.venv
sudo /opt/mqtt-client/.venv/bin/pip install "paho-mqtt>=2.0"
sudo chown -R mqttclient:mqttclient /opt/mqtt-client

Nutzt das Skript GPIO, I2C oder serielle Schnittstellen, fügen Sie den Benutzer der passenden Gruppe hinzu, zum Beispiel mit sudo usermod -aG gpio,i2c,dialout mqttclient.

Schritt 3: Zugangsdaten in einer Umgebungsdatei ablegen

sudo mkdir -p /etc/mqtt-client
sudo tee /etc/mqtt-client/env > /dev/null <<'EOF'
MQTT_HOST=broker.connectmq.com
MQTT_PORT=1883
MQTT_CLIENT_ID=pi1-heartbeat-s8t4n6
TOPIC_PREFIX=connectmq/demo/s8t4n6/pi1
PUBLISH_INTERVAL=30
EOF
sudo chmod 600 /etc/mqtt-client/env

Für den ersten Test zeigt dies auf den öffentlichen MQTT-Broker, der keine Zugangsdaten benötigt und für Tests gedacht ist. systemd liest die Datei als root, bevor der Prozess startet; sie kann also ausschließlich für root lesbar bleiben.

Schritt 4: Die Unit-Datei für den Autostart des Python-MQTT-Skripts schreiben

Legen Sie /etc/systemd/system/mqtt-client.service an:

[Unit]
Description=Python MQTT client
Wants=network-online.target time-sync.target
After=network-online.target time-sync.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=mqttclient
Group=mqttclient
WorkingDirectory=/opt/mqtt-client
EnvironmentFile=/etc/mqtt-client/env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mqtt-client/.venv/bin/python /opt/mqtt-client/heartbeat.py
Restart=always
RestartSec=5
TimeoutStopSec=15
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

Was die wichtigsten Zeilen bewirken:

  1. Wants/After=network-online.target verzögert den Start, bis das Netzwerk konfiguriert ist – nicht nur, bis der Netzwerkdienst gestartet wurde.
  2. time-sync.target wartet auf die Uhrzeit (siehe den TLS-Hinweis unten).
  3. StartLimitIntervalSec=0 deaktiviert die Startratenbegrenzung, sodass systemd endlos weiter versucht, statt nach einigen schnellen Fehlschlägen aufzugeben.
  4. Restart=always und RestartSec=5 starten das Skript fünf Sekunden nach jedem Beenden neu, sowohl nach Abstürzen als auch nach sauberem Beenden.
  5. PYTHONUNBUFFERED=1 sorgt dafür, dass print()-Ausgaben sofort im Journal erscheinen.
  6. Die Protect*-Optionen isolieren den Prozess in einer Sandbox; die Checkliste zur Absicherung des Raspberry Pi im IoT erklärt sie genauer.

Aktivieren und starten Sie den Dienst:

sudo systemctl daemon-reload
sudo systemctl enable --now mqtt-client.service
systemctl status mqtt-client.service

Schritt 5: Prüfen und betreiben

Verfolgen Sie die Logs live und abonnieren Sie anschließend von einem anderen Rechner aus, um zu bestätigen, dass Nachrichten ankommen:

journalctl -u mqtt-client.service -f
mosquitto_sub -h broker.connectmq.com -p 1883 -i boot-viewer-s8t4n6 -t "connectmq/demo/s8t4n6/pi1/#" -v

Starten Sie den Pi nun mit sudo reboot neu. Der Subscriber sieht offline (vom sauberen Herunterfahren), dann online und Heartbeats, sobald der Pi wieder da ist. Um die Wiederherstellung nach einem Absturz zu testen, beenden Sie den Prozess mit sudo systemctl kill -s SIGKILL mqtt-client.service und beobachten, wie systemd ihn nach fünf Sekunden neu startet.

Befehle für den Alltag:

Aufgabe Befehl
Neustart nach Änderung von Code oder Umgebung sudo systemctl restart mqtt-client
Vorübergehend stoppen sudo systemctl stop mqtt-client
Autostart deaktivieren sudo systemctl disable mqtt-client
Logs seit dem letzten Booten journalctl -u mqtt-client -b
Wirksame Unit anzeigen systemctl cat mqtt-client

Die Falle mit TLS und Uhrzeit

Die meisten Raspberry-Pi-Modelle haben keine batteriegepufferte Echtzeituhr (der Pi 5 hat eine RTC, braucht aber eine Batterie, um die Zeit im stromlosen Zustand zu halten). Beim Booten startet die Uhr mit der zuletzt gespeicherten Zeit, bis NTP synchronisiert. Weicht sie stark ab, scheitert die Prüfung des TLS-Zertifikats, weil das Zertifikat scheinbar noch nicht gültig oder bereits abgelaufen ist, und Ihr Skript protokolliert wiederholte Verbindungsfehler auf Port 8883.

Die Abhängigkeit von time-sync.target hilft nur, wenn ein Dienst tatsächlich auf die Synchronisierung wartet. Aktivieren Sie den Dienst, der das tut:

sudo systemctl enable systemd-time-wait-sync.service
timedatectl status

Damit startet der MQTT-Client erst, wenn die Uhr synchronisiert ist. Da sich das Skript zudem selbst neu verbindet, erholt es sich auch ohne diesen Dienst – nur langsamer.

Fehlersuche bei einem Dienst, der nicht stabil läuft

Wenn ein Dienst sich falsch verhält, enthalten systemctl status und das Journal fast immer die Antwort. Dies sind die häufigsten Fehler bei Python-MQTT-Clients:

  1. status=203/EXEC: systemd kann ExecStart nicht ausführen. Der Pfad zum Python der venv ist falsch, oder die venv wurde von einem anderen Benutzer angelegt und ist nicht lesbar. Verwenden Sie absolute Pfade und prüfen Sie sie mit ls -l.
  2. ModuleNotFoundError: paho: Die Unit startet das System-Python statt der venv. Lassen Sie ExecStart auf /opt/mqtt-client/.venv/bin/python zeigen, nicht auf python3.
  3. KeyError: 'MQTT_CLIENT_ID': In der Umgebungsdatei fehlt eine Variable oder sie enthält einen Tippfehler. Beachten Sie, dass EnvironmentFile-Zeilen einfaches KEY=value ohne export sind.
  4. PermissionError bei einer Datei oder einem Gerät: ProtectSystem=strict hat den Pfad schreibgeschützt gemacht, oder dem Dienstbenutzer fehlt eine Gruppe. Tragen Sie das Verzeichnis in ReadWritePaths= ein oder fügen Sie den Benutzer der richtigen Gruppe hinzu.
  5. Wiederholt „connection refused“ oder „not authorised“: falscher Port, falsche Zugangsdaten oder eine doppelte Client ID. Zwei Prozesse mit derselben Client ID trennen sich gegenseitig ständig – stellen Sie sicher, dass kein manueller Testlauf mehr aktiv ist.
  6. Endlose Neustarts alle paar Sekunden: Das Skript beendet sich sofort. Führen Sie den exakten ExecStart-Befehl als Dienstbenutzer mit sudo -u mqttclient von Hand aus, um den Fehler im Terminal zu sehen.

Wechsel zu einem privaten Broker

Wenn der Dienst funktioniert, wechseln Sie zu produktiven Zugangsdaten. Legen Sie einen Broker bei ConnectMQ an und bearbeiten Sie dann /etc/mqtt-client/env:

MQTT_HOST=broker.connectmq.com
MQTT_PORT=8883
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
MQTT_CLIENT_ID=YOUR_CLIENT_ID
TOPIC_PREFIX=site1/gateways/pi1

Starten Sie den Dienst neu, und das Skript verbindet sich über TLS. Ein verwalteter MQTT-Broker hält die Serverseite am Laufen, während systemd die Clientseite am Laufen hält – zusammen decken beide die meisten Ausfälle ab. Für Abstürze, die eher wie Hänger aussehen als wie ein Beenden, ergänzen Sie einen Anwendungs-Watchdog, wie in den Tipps zur Zuverlässigkeit des Raspberry Pi beschrieben, und puffern Sie Daten bei Netzwerkausfällen mit MQTT-Offline-Pufferung in Python. Das systemd.service-Handbuch listet alle Optionen auf.

Neu bei MQTT auf dem Pi? Beginnen Sie mit dem Raspberry Pi MQTT-Tutorial und kommen Sie dann hierher zurück, um Ihren Client dauerhaft einzurichten.

Halten Sie Ihren Pi mit ConnectMQ verbunden

Registrieren Sie sich unter app.connectmq.com für den kostenlosen Serverless-Plan, ohne Kreditkarte, und tragen Sie die generierten Zugangsdaten in Ihre Umgebungsdatei ein. Das ConnectMQ-Portal listet Ihre Geräte auf, sodass Sie nach dem nächsten Neustart prüfen können, ob Ihr Dienst wieder online ist, ohne sich am Pi anzumelden.

Häufig gestellte Fragen

Warum startet mein Dienst, bevor das Netzwerk bereit ist?

After=network.target wartet nur auf den Netzwerkdienst, nicht auf eine funktionierende Verbindung. Verwenden Sie Wants= und After=network-online.target und lassen Sie das Skript sich mit connect_async() selbst neu verbinden.

Warum sehe ich keine Ausgabe in journalctl?

Python puffert stdout, wenn es nicht in einem Terminal läuft. Setzen Sie PYTHONUNBUFFERED=1, starten Sie Python mit -u oder übergeben Sie flush=True an print().

Sollte ich einen User-Dienst statt eines Systemdienstes verwenden?

User-Dienste laufen nur, solange der Benutzer eine Sitzung hat, es sei denn, Lingering ist aktiviert. Ein Systemdienst mit eigenem User= ist einfacher und startet beim Booten ohne Anmeldung.

Wie betreibe ich mehrere MQTT-Skripte?

Legen Sie pro Skript eine Unit-Datei an oder nutzen Sie eine Template-Unit wie mqtt-client@.service mit einer eigenen Umgebungsdatei pro Instanz. Jede Instanz benötigt ihre eigene eindeutige Client ID.

Verwandte Artikel

Raspberry Pi RFID MQTT: Zutrittsprotokollierung mit dem RC522-Leser

Schließen Sie einen RC522-RFID-Leser an einen Raspberry Pi an, lesen Sie Tags mit Python, prüfen Sie sie gegen eine lokale Allowlist und veröffentlichen Sie jeden Zutrittsversuch als MQTT-Ereignis, das Logger, Dashboards und Alarmwerkzeuge verarbeiten können.

7 Min. Lesezeit

Pico W vs. Pico 2 W für MQTT: RP2040 vs. RP2350 bei Leistung, Speicher und TLS

Der Pico 2 W behält Formfaktor und WLAN-Chip des Pico W, ersetzt aber den RP2040 durch den schnelleren RP2350. Dieser Vergleich zeigt, was das für MQTT-Geräte bedeutet: CPU, Speicher, TLS, Sicherheitsfunktionen, Softwareunterstützung und welches Board für welches Projekt.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.