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:
Wants/After=network-online.targetverzögert den Start, bis das Netzwerk konfiguriert ist – nicht nur, bis der Netzwerkdienst gestartet wurde.time-sync.targetwartet auf die Uhrzeit (siehe den TLS-Hinweis unten).StartLimitIntervalSec=0deaktiviert die Startratenbegrenzung, sodass systemd endlos weiter versucht, statt nach einigen schnellen Fehlschlägen aufzugeben.Restart=alwaysundRestartSec=5starten das Skript fünf Sekunden nach jedem Beenden neu, sowohl nach Abstürzen als auch nach sauberem Beenden.PYTHONUNBUFFERED=1sorgt dafür, dassprint()-Ausgaben sofort im Journal erscheinen.- 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:
status=203/EXEC: systemd kannExecStartnicht 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 mitls -l.ModuleNotFoundError: paho: Die Unit startet das System-Python statt der venv. Lassen SieExecStartauf/opt/mqtt-client/.venv/bin/pythonzeigen, nicht aufpython3.KeyError: 'MQTT_CLIENT_ID': In der Umgebungsdatei fehlt eine Variable oder sie enthält einen Tippfehler. Beachten Sie, dassEnvironmentFile-Zeilen einfachesKEY=valueohneexportsind.PermissionErrorbei einer Datei oder einem Gerät:ProtectSystem=stricthat den Pfad schreibgeschützt gemacht, oder dem Dienstbenutzer fehlt eine Gruppe. Tragen Sie das Verzeichnis inReadWritePaths=ein oder fügen Sie den Benutzer der richtigen Gruppe hinzu.- 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.
- Endlose Neustarts alle paar Sekunden: Das Skript beendet sich sofort. Führen Sie den exakten
ExecStart-Befehl als Dienstbenutzer mitsudo -u mqttclientvon 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.



