Um MQTT in GitHub Actions zu testen, startest du einen Broker im Job, wartest, bis er Verbindungen annimmt, und lässt deine Testsuite dagegen laufen – dann ergänzt du einen zweiten Job, der dieselben Tests gegen einen gehosteten Broker ausführt, mit Zugangsdaten als Repository-Secrets. Der erste Job findet Logikfehler bei jedem Commit; der zweite findet TLS-, Authentifizierungs- und ACL-Probleme, bevor deine Geräte sie bemerken.
Diese Anleitung gibt dir kopierfertige Workflows für beides und erklärt die eine Mosquitto-Voreinstellung, an der die meisten ersten Versuche scheitern.
Die „Local Only Mode"-Falle
Der erste Impuls ist ein Service-Container:
services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883Der Container startet, der Port ist gemappt, und jeder Test schlägt mit Connection refused oder ECONNRESET fehl. Das Broker-Log erklärt, warum:
mosquitto version 2.0.x starting
Config loaded from /mosquitto/config/mosquitto.conf.
Starting in local only mode. Connections will only be possible from clients running on this machine.
Create a configuration file which defines a listener to allow remote access.Seit Version 2.0 bindet sich Mosquitto ohne listener in der Konfiguration nur an das Loopback-Interface innerhalb seines eigenen Containers. Traffic, der über das Port-Mapping ankommt, kommt von außen und wird deshalb abgelehnt. Die Standard-Konfigurationsdatei des Images hat alle Optionen auskommentiert – genau das bekommst du also.
Service-Container machen die Lösung umständlich: Sie starten, bevor irgendein Step läuft, dein Repository (und seine mosquitto.conf) ist also noch nicht ausgecheckt. Am einfachsten und vorhersehbarsten ist es, den Broker selbst in einem Step zu starten. Dasselbe Problem für lokale Setups behandeln Mosquitto in Docker und Mosquitto-Konfiguration wird nicht geladen.
Job 1: Mosquitto im Workflow
Committe eine minimale Testkonfiguration ins Repository:
listener 1883 0.0.0.0
allow_anonymous true
persistence false
log_dest stdoutStrengere Varianten – mit Passwortdatei oder ACLs, um Ablehnungen zu testen – erzeugst du mit dem Mosquitto-Config-Generator. Starte den Broker dann nach dem Checkout und warte, bis er wirklich per MQTT antwortet, nicht nur per TCP:
name: mqtt-tests
on:
push:
pull_request:
jobs:
local-broker:
runs-on: ubuntu-latest
env:
MQTT_HOST: localhost
MQTT_PORT: "1883"
steps:
- uses: actions/checkout@v4
- name: Start Mosquitto
run: |
docker run -d --name mosquitto -p 1883:1883 \
-v "$PWD/test/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" \
eclipse-mosquitto:2
for i in $(seq 1 30); do
if docker exec mosquitto mosquitto_pub -h localhost -t testmqtt/ci/ready -m ok; then
echo "broker ready"; exit 0
fi
sleep 1
done
echo "broker did not start"; docker logs mosquitto; exit 1
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements-dev.txt
- run: pytest -v tests/
- name: Broker logs
if: failure()
run: docker logs mosquittoWarum die Schleife mosquitto_pub ausführt, statt den Port zu prüfen: Dockers Port-Proxy nimmt TCP-Verbindungen auf dem Host an, sobald der Container existiert – noch bevor der Broker darin lauscht. Ein erfolgreicher Publish beweist, dass der Broker läuft. Das eclipse-mosquitto-Image bringt die Kommandozeilen-Clients mit, es muss also nichts zusätzlich installiert werden.
Für ein Node.js-Projekt ersetzt du die Python-Steps durch actions/setup-node@v4, npm ci und npm test; die Broker-Steps bleiben gleich.
Job 2: Tests gegen einen gehosteten Broker
Ein Container in CI ist anonym, unverschlüsselt und bei jedem Lauf frisch konfiguriert – das Gegenteil von Produktion. Ein zweiter Job führt dieselbe Suite über TLS mit echten Zugangsdaten aus. Speichere sie unter Settings → Secrets and variables → Actions als MQTT_USER und MQTT_PASS:
hosted-broker:
runs-on: ubuntu-latest
needs: local-broker
# secrets are not available to pull requests from forks
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
concurrency:
group: mqtt-hosted-${{ github.ref }}
cancel-in-progress: true
env:
MQTT_HOST: broker.connectmq.com
MQTT_PORT: "8883"
MQTT_USER: ${{ secrets.MQTT_USER }}
MQTT_PASS: ${{ secrets.MQTT_PASS }}
MQTT_TOPIC_PREFIX: testmqtt/ci/${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements-dev.txt
- run: pytest -v tests/Was die einzelnen Teile tun:
needs: local-brokerführt den langsameren Job mit Netzwerkzugriff nur aus, wenn der schnelle Job erfolgreich war.- Die
if:-Bedingung überspringt den Job bei Pull Requests aus Forks, die keine Secrets erhalten. Ohne sie scheitert der Job mit einem Authentifizierungsfehler, der wie ein echter Bug aussieht. MQTT_TOPIC_PREFIXenthält Run-ID und Attempt, sodass sich Wiederholungen und parallele Workflows nie Topics teilen. Deine Tests müssen jedes Topic daraus aufbauen – siehe MQTT-Integrationstests für das Fixture-Muster.- GitHub maskiert Secret-Werte in Logs automatisch, trotzdem solltest du Verbindungsoptionen nicht in Debug-Ausgaben schreiben.
Den gehosteten Job schnell und sauber halten
Ein paar Gewohnheiten halten den Netzwerk-Job zuverlässig, während die Suite wächst:
- Nur eine Teilmenge ausführen. Markiere Tests, die TLS, Zugangsdaten oder ACLs brauchen (zum Beispiel
@pytest.mark.hostedoder ein separates Jest-Projekt), und führe nur diese im gehosteten Job aus. Reine Logik deckt der lokale Job bereits ab. - Retained Messages aufräumen. Jede Retained Message, die ein Test erzeugt, überlebt den Lauf. Lösche sie im Teardown mit einem leeren Retained-Publish oder verzichte außerhalb von Retained-Tests auf das Retain-Flag.
- Clean Sessions verwenden. Persistente Sessions mit zufälligen Client-IDs sammeln sich auf dem Broker, bis sie ablaufen. Nutze Clean Sessions (oder eine kurze MQTT-5-Session-Expiry), außer ein Test dreht sich um Sessions.
- Timeouts an das Netzwerk anpassen. Eine Nachricht, die lokal in 20 ms ankommt, kann von einem geteilten Runner über das Internet eine Sekunde brauchen. Nutze in diesem Job Nachrichten-Timeouts von 5–10 Sekunden; die Tests enden trotzdem, sobald die Nachricht da ist.
Mit einer Matrix gegen mehrere Broker testen
Wenn deine Bibliothek oder dein Gateway mit mehr als einem Broker funktionieren muss, führt eine Matrix die Suite gegen jeden aus, ohne YAML zu duplizieren:
compat:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
image: ["eclipse-mosquitto:2", "emqx/emqx:latest", "hivemq/hivemq-ce:latest"]
steps:
- uses: actions/checkout@v4
- name: Start ${{ matrix.image }}
run: docker run -d --name broker -p 1883:1883 ${{ matrix.image }} ${{ startsWith(matrix.image, 'eclipse-mosquitto') && 'mosquitto -c /mosquitto-no-auth.conf' || '' }}
- name: Wait for MQTT
run: |
pip install paho-mqtt
python scripts/wait_for_broker.py localhost 1883 --timeout 60
- run: pip install -r requirements-dev.txt && pytest -v tests/EMQX und HiveMQ CE brauchen länger zum Starten als Mosquitto, und ihre Images enthalten kein mosquitto_pub, deshalb läuft der Bereitschafts-Check vom Runner aus. Pinne in echten Pipelines exakte Image-Tags, damit ein neues Broker-Release deinen Build nicht unangekündigt bricht. Ein kleines wait_for_broker.py muss nur client.connect() wiederholen, bis es klappt oder der Timeout abläuft.
Fehlgeschlagene CI-Läufe debuggen
| Fehler im Job-Log | Ursache | Lösung |
|---|---|---|
Connection refused / ECONNREFUSED direkt nach dem Start | Broker noch nicht bereit oder Local Only Mode | MQTT-Bereitschaftsschleife nutzen; Listener deklarieren |
ECONNRESET bei jedem Connect | Port gemappt, aber Mosquitto an Loopback gebunden | listener 1883 0.0.0.0 in der Konfiguration |
Connection Refused: not authorised im gehosteten Job | Secret fehlt (Fork-PR) oder falscher User | if:-Bedingung und Secret-Namen prüfen |
[SSL: CERTIFICATE_VERIFY_FAILED] | TLS-Port ohne CA oder falscher Hostname | System-CA-Bundle und den echten Hostnamen des Brokers verwenden |
| Tests laufen einzeln, scheitern in der Matrix | Gemeinsame Topics zwischen Jobs | Topic-Präfix mit Run-ID und Matrix-Wert |
Der if: failure()-Step, der docker logs ausgibt, lohnt sich dauerhaft: Das Broker-Log zeigt abgelehnte Verbindungen, ACL-Ablehnungen und Client-IDs. Für Reason Codes in Client-Fehlern nutze die MQTT-Reason-Code-Suche.
Die Pipeline überprüfen
Lass die Pipeline beweisen, dass sie fehlschlagen kann. Pushe einen Branch, der auf das falsche Topic publisht, und bestätige, dass der Job rot wird. Abonniere dann, während der gehostete Job läuft, testmqtt/ci/# im Online-MQTT-Client mit denselben Zugangsdaten: Du siehst das laufspezifische Präfix auftauchen und den Test-Traffic fließen – damit ist bestätigt, dass Secrets, TLS und ACLs wie gewünscht funktionieren.
Eine zuverlässige MQTT-Pipeline braucht zwei Dinge: einen Broker im Job, der so konfiguriert ist, dass er Verbindungen annimmt, und der mit einem echten Publish geprüft wird, sowie eine zweite Stufe, die TLS, Zugangsdaten und ACLs wie in Produktion durchspielt. Mit eindeutigen Topic-Präfixen und ohne Secrets in Fork-Builds laufen beide bei jedem Push, ohne sich in die Quere zu kommen. Wenn du Container lieber direkt aus dem Testcode verwaltest, sieh dir MQTT-Tests mit Testcontainers an.
Häufig gestellte Fragen
Warum kann sich mein GitHub-Actions-Job nicht mit dem Mosquitto-Container verbinden?
Mosquitto 2.x startet im Local Only Mode, wenn kein Listener konfiguriert ist, und akzeptiert dann nur Verbindungen aus dem eigenen Container. Starte ihn mit einer Konfiguration, die listener 1883 und allow_anonymous true (oder echte Zugangsdaten) festlegt, zum Beispiel mit der mitgelieferten /mosquitto-no-auth.conf.
Sind GitHub-Secrets in Pull Requests aus Forks verfügbar?
Nein. Workflows, die durch Pull Requests aus geforkten Repositories ausgelöst werden, erhalten keine Repository-Secrets. Führe Tests gegen den gehosteten Broker nur bei Pushes und Pull Requests aus demselben Repository aus und nutze für alles andere den Job mit lokalem Broker.
Sollte CI einen lokalen Mosquitto oder einen gehosteten Broker verwenden?
Beides. Ein lokaler Container liefert bei jedem Commit schnelles, isoliertes Feedback. Ein Job gegen einen gehosteten Broker prüft TLS, Authentifizierung und ACLs so, wie sie in Produktion konfiguriert sind – das kann ein Wegwerf-Container nicht.



