sphings-dev.de

Marstek Offline Endpoint mit Docker auf Linux einrichten

Den Marstek Offline Endpoint auf einem Linux-Server mit Docker Compose starten und per DNS dafür sorgen, dass die Batterie ihn statt der Cloud anspricht. Danach bleiben die regelmäßigen Netzwerk-Resets aus.

Der Marstek Offline Endpoint ist ein kleiner Container, der den Telemetrie-Upload eines Marstek Venus (E, D, E v3) im Heimnetz beantwortet. Ohne Antwort aus der Cloud setzt die Batterie auf der Control-Firmware v150 alle 30 Minuten ihren Netzwerkchip zurück; Modbus-Verbindungen brechen dabei ab. Firmware v151 startet stattdessen die Steuerung neu, wenn vier Stunden lang mindestens fünf Uploads unversendet bleiben, und schaltet dabei den Wechselrichterausgang ab. Der Container bestätigt jeden Upload, damit beides gar nicht erst eintritt.

So funktioniert die Umleitung

Die Batterie spricht zwei Server an: api-eu.marstekcloud.com für den Upload (HTTPS, Port 443) und eu.hamedata.com für die Uhrzeit (HTTP, Port 80). Du änderst nichts an der Batterie. Stattdessen sorgst du im Heimnetz dafür, dass beide Namen auf den Rechner zeigen, auf dem der Container läuft. Dazu legst du zwei DNS-Einträge für die ganzen Domains marstekcloud.com und hamedata.com an, damit auch eine andere Region als eu im Namen funktioniert. Die Ports 80 und 443 lassen sich nicht ändern, die Batterie verbindet sich fest dorthin.

Zwei Dinge gehören ausdrücklich nicht dazu: Den MQTT-Broker der Batterie leitest du nicht um (er prüft Zertifikate und lässt sich nicht nachbilden), und der Container gehört nicht ins Internet. Öffne also keine Ports im Router dorthin.

Vorbereitung

Du brauchst einen Linux-Server, der dauerhaft läuft, mit Docker und Docker Compose. Die Befehle gelten für Debian und Ubuntu. Alle Befehle gibst du in einem Terminal auf dem Server ein, direkt am Gerät oder per SSH. Jeder Befehl wird mit der Eingabetaste abgeschickt. Bei sudo fragt das System nach deinem Passwort; beim Tippen erscheinen keine Zeichen, das ist normal.

Feste Adresse für den Server

Die DNS-Einträge zeigen später auf diesen Server. Seine Adresse darf sich deshalb nicht ändern. Der einfachste Weg ist eine feste Zuordnung im Router: Suche den Server in der Geräteliste des Routers und aktiviere dort eine Option wie „immer dieselbe Adresse zuweisen“ (DHCP-Reservierung). Wie der Punkt heißt, hängt vom Router ab. Schreibe dir die Adresse auf; im Folgenden steht dafür das Beispiel 192.168.1.60.

Prüfen, ob die Ports frei sind

Der Container braucht auf dem Server die Ports 80 und 443. Läuft dort bereits ein Webserver wie Apache oder nginx, sind sie belegt.

  1. Prüfe, ob einer der beiden Ports belegt ist.
sudo ss -ltnp | grep -E ':(80|443) '

Bleibt die Ausgabe leer, sind beide Ports frei. Steht dort eine Zeile, belegt ein anderes Programm den Port; stoppe es oder nimm einen anderen Rechner.

Docker installieren

Wenn docker compose version bei dir schon eine Versionsnummer zeigt, überspringe diesen Abschnitt. Sonst installierst du Docker aus dem offiziellen Paket-Repository von Docker. Der Befehl erkennt selbst, ob du Debian oder Ubuntu hast. Ableitungen wie Linux Mint unterstützt Docker offiziell nicht.

  1. Entferne mögliche Konfliktpakete. Die Meldung, dass keines davon installiert ist, ist in Ordnung.
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)
  1. Installiere die Hilfspakete und lege den Schlüsselordner an.
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
  1. Lade den Schlüssel von Docker herunter und mache ihn lesbar.
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
  1. Trage das Repository ein. Kopiere dazu den ganzen Block bis einschließlich der Zeile EOF.
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
  1. Lies die Paketlisten neu ein.
sudo apt update
  1. Installiere Docker samt Compose-Plugin.
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
  1. Nimm deinen Benutzer in die Gruppe docker auf, damit du nicht jedes Mal sudo brauchst.
sudo usermod -aG docker $USER
  1. Melde dich ab und wieder an (bei SSH: Verbindung trennen und neu verbinden), damit die Gruppe gilt. Prüfe dann, dass Compose antwortet.
docker compose version

Die Ausgabe nennt eine Version. Der Befehl heißt docker compose mit Leerzeichen, nicht docker-compose. Unter Debian und Ubuntu startet der Docker-Dienst bei jedem Systemstart automatisch; das ist wichtig, damit der Container nach einem Neustart zurückkommt.

Container einrichten

  1. Lege einen Ordner für das Projekt an und wechsle hinein.
mkdir -p ~/marstek-offline-endpoint && cd ~/marstek-offline-endpoint
  1. Öffne einen Editor für die Compose-Datei.
nano compose.yaml
  1. Füge den folgenden Inhalt ein. Ändere TZ auf deine Zeitzone, falls du nicht in Deutschland bist.
# Marstek Offline Endpoint: beantwortet die Cloud-Anfragen deiner Marstek Venus im Heimnetz
services:
  marstek-offline-endpoint:
    image: ghcr.io/sphings79/marstek-offline-endpoint:latest   # fertiges Image für PC, NAS und Raspberry Pi
    container_name: marstek-offline-endpoint
    restart: unless-stopped        # startet nach einem Neustart automatisch wieder, das ist wichtig
    ports:
      - "443:443"                  # HTTPS: Telemetrie-Upload der Batterie (Port nicht ändern)
      - "80:80"                    # HTTP: Zeitabfrage der Batterie (Port nicht ändern)
    volumes:
      - ./data:/data               # empfangene Messdaten, eine Datei pro Tag
      - ./certs:/certs             # selbst erzeugtes Zertifikat, wird beim ersten Start angelegt
    environment:
      - TZ=Europe/Berlin           # deine Zeitzone, sonst stellt der Container die Uhr der Batterie auf UTC
  1. Speichere mit Strg+O und der Eingabetaste und beende den Editor mit Strg+X.

Die Zeitzone TZ ist wichtig: Der Container stellt die Uhr der Batterie, und der echte Server antwortet in Ortszeit. Ohne TZ bekommt die Batterie UTC. Den MQTT-Port 8883 veröffentlichen wir bewusst nicht.

  1. Starte den Container im Hintergrund.
docker compose up -d

Docker lädt das Image und startet den Container. Zum Schluss steht bei marstek-offline-endpoint die Meldung „Started“. Beim ersten Start erzeugt der Container ein selbstsigniertes Zertifikat im Ordner certs; die Batterie prüft es nicht.

  1. Sieh dir die Startmeldungen an.
docker compose logs

Du willst drei „listening“-Zeilen sehen und keine Fehler:

https listening on :443
http  listening on :80
mqtt probe listening on :8883

Neustartfest machen

Fällt der Container länger aus, füllt sich der Puffer der Batterie, und die Resets beginnen von vorn. Er muss deshalb nach jedem Neustart des Servers wieder laufen. restart: unless-stopped in der Compose-Datei und der automatisch startende Docker-Dienst sorgen dafür. Teste es einmal.

  1. Starte den Server neu.
sudo reboot
  1. Melde dich nach etwa einer Minute wieder an und prüfe, dass der Container läuft.
docker ps

In der Liste steht marstek-offline-endpoint mit einem Status wie „Up 40 seconds“. Fehlt er, aktiviere den Dienst mit sudo systemctl enable docker und starte noch einmal neu.

DNS-Einträge anlegen

Jetzt kommt der Schritt, der die Batterie tatsächlich zu deinem Container umleitet. Du brauchst Zugriff auf den DNS-Server, den dein Heimnetz benutzt. Das ist meist ein Pi-hole oder AdGuard Home, manchmal der Router selbst, wenn er lokale DNS-Einträge für ganze Domains erlaubt. Als Ziel trägst du die feste Adresse des Servers ein; in dieser Anleitung ist das als Beispiel 192.168.1.60. Setze deine eigene Adresse ein.

So findest du heraus, wo du die Einträge anlegen musst: Schau in die DHCP-Einstellungen deines Routers. Der dort verteilte DNS-Server ist der, den auch die Batterie bekommt, und genau dort musst du die Einträge anlegen.

AdGuard Home

  1. Öffne die Weboberfläche von AdGuard Home.
  2. Wechsle zu „Filter › DNS-Umschreibungen“.
  3. Klicke auf „DNS-Umschreibung hinzufügen“.
  4. Trage als Domain marstekcloud.com und als IP-Adresse 192.168.1.60 ein und speichere.
  5. Lege ebenso *.marstekcloud.com mit 192.168.1.60 an.
  6. Lege hamedata.com mit 192.168.1.60 an.
  7. Lege *.hamedata.com mit 192.168.1.60 an.

AdGuard Home übernimmt Umschreibungen sofort. Es sind vier Einträge, jeweils die Domain ohne und mit Platzhalter.

Pi-hole

Die Maske für lokale DNS-Einträge in Pi-hole nimmt nur ganze Hostnamen. Einfacher ist eine kleine Konfigurationsdatei für den eingebauten dnsmasq-Dienst. Du legst sie auf dem Gerät an, auf dem Pi-hole läuft.

  1. Öffne ein Terminal auf dem Pi-hole-Rechner und gib den folgenden Befehl ein. Ersetze zuvor die Adresse durch deine eigene.
sudo tee /etc/dnsmasq.d/99-marstek.conf >/dev/null <<'EOF'
address=/marstekcloud.com/192.168.1.60
address=/hamedata.com/192.168.1.60
EOF
  1. Starte den DNS-Dienst von Pi-hole neu.
pihole restartdns

Wenn du statt der Domains nur die beiden genauen Namen eintragen willst, geht das in der Pi-hole-Oberfläche als lokaler DNS-Eintrag für api-eu.marstekcloud.com und für eu.hamedata.com. Das gilt dann nur für die Region eu, die bei der Batterie im Test beobachtet wurde. Die Domain-Variante ist die sicherere.

Router oder anderer DNS-Server

Wenn dein Router oder Firewall-System einen lokalen DNS-Eintrag oder „Host Override“ für eine ganze Domain anbietet, gelten dieselben zwei Domains mit derselben Zieladresse. Bei OPNsense liegt das unter „Services › Unbound DNS › Overrides › Domain Overrides“. Ob dein Router das kann, steht in dessen Handbuch; nicht jeder Router erlaubt Einträge für ganze Domains. Wenn nicht, brauchst du einen eigenen DNS-Server wie Pi-hole oder AdGuard Home.

Prüfen, ob die Einträge greifen

  1. Öffne auf einem PC oder Mac ein Terminal (unter Windows die Eingabeaufforderung oder PowerShell) und frage den ersten Namen ab.
nslookup api-eu.marstekcloud.com
  1. Frage auch den zweiten Namen ab.
nslookup eu.hamedata.com

In beiden Antworten muss unter „Name“ der jeweilige Hostname stehen und unter „Address“ die Adresse 192.168.1.60, also deine Container-Adresse, keine öffentliche. Steht dort eine fremde Adresse, benutzt dein PC einen anderen DNS-Server. Frage dann den richtigen direkt ab, indem du dessen Adresse anhängst: nslookup api-eu.marstekcloud.com DNS-SERVER-IP.

Die Batterie muss denselben DNS-Server benutzen wie dieser Test. Manche Geräte ignorieren den per DHCP verteilten DNS-Server und benutzen einen fest eingebauten. Dann hilft statt DNS eine Firewall-Regel (DNAT) im Router: Verkehr von der IP der Batterie auf die Ports 443 und 80 wird auf den Container umgebogen. Wenn die Batterie in einem eigenen VLAN hängt, muss sie den Container außerdem erreichen dürfen.

Kontrolle

  1. Verfolge die Meldungen des Containers live. Beenden kannst du die Anzeige mit Strg+C.
cd ~/marstek-offline-endpoint && docker compose logs -f

Innerhalb von etwa fünf Minuten erscheinen Zeilen wie diese (Uhrzeit und Anhänge sind bei dir anders):

2026-08-26 08:17:38 TIME   http GET eu.hamedata.com/app/neng/getDateInfoeu.php?uid=… (0 B)
2026-08-26 08:19:07 ACCEPT https POST api-eu.marstekcloud.com/data-upload/v1/venus/… (1204 B)

TIME ist die Batterie, die nach der Uhrzeit fragt. ACCEPT ist der Telemetrie-Upload, den der Container bestätigt. Beides zusammen heißt: Die Umleitung funktioniert. Die Zeitabfrage kommt etwa alle zehn Minuten, der Upload etwa alle fünf Minuten.

Kommen vier TIME-Zeilen im Abstand von 20 Sekunden hintereinander, lehnt die Batterie die Antwort ab; dann läuft ein veraltetes Image, lade es neu. Kommen Uploads exakt alle 86 Sekunden, ist noch ein Rückstau in der Batterie abzubauen. Das ist für ein bis zwei Stunden normal.

  1. Prüfe, dass der Container die Messdaten mitschreibt.
ls ~/marstek-offline-endpoint/data

Dort liegt eine Datei requests-JJJJ-MM-TT.jsonl mit allem, was die Batterie gesendet hat. Die Daten bleiben bei dir zu Hause.

Um zu sehen, dass die Abbrüche weg sind, kannst du von einem Mac oder Linux-Rechner, der durchläuft, die Batterie anpingen. Das folgende Skript meldet sich nur, wenn ein Ping ausfällt; so sieht ein Netzwerk-Reset der Batterie von außen aus. Ersetze 192.168.1.50 durch die IP deiner Batterie (in der Geräteliste deines Routers, meist als „Marstek“, „VenusE“ oder ähnlich geführt).

IP=192.168.1.50; F=0
while true; do
  if ping -c1 -W1 "$IP" >/dev/null 2>&1; then
    [ $F -gt 0 ] && echo "$(date '+%F %T')  WIEDER DA nach ${F}s"; F=0
  else
    [ $F -eq 0 ] && echo "$(date '+%F %T')  WEG"; F=$((F+1))
  fi
  sleep 1
done

Bleibt die Ausgabe über zwei halbe Stunden leer, ist es geschafft. Vorher stand dort alle 30 Minuten eine Meldung. Beim gemessenen Gerät dauerte es nach dem ersten bestätigten Upload etwa 45 Minuten, bis der Rückstau abgebaut war und die Abbrüche aufhörten.

Ein Rest bleibt, den der Container nicht beheben kann: Bei jedem Upload rechnet die Batterie etwa vier bis fünf Sekunden am TLS-Schlüsselaustausch und beantwortet in der Zeit kein Modbus. Stelle deshalb in deinem Modbus-Client ein Antwort-Timeout von mindestens 6 Sekunden ein.

Später aktualisieren

  1. Wechsle in den Ordner mit der Compose-Datei und lade das neue Image.
cd ~/marstek-offline-endpoint && docker compose pull
  1. Ersetze den laufenden Container durch den neuen.
docker compose up -d

Daten und Zertifikat liegen in den Ordnern data und certs und bleiben erhalten.

Wenn etwas nicht klappt

Nach 15 Minuten steht nichts im Log.

Die Batterie landet nicht beim Container. Wiederhole die nslookup-Prüfung. Sieht sie richtig aus, hat die Batterie womöglich eine alte Antwort zwischengespeichert: Trenne sie kurz vom Strom, schalte sie wieder ein und warte fünf Minuten. Kommt immer noch nichts, benutzt sie einen fest eingebauten DNS-Server, dann hilft die DNAT-Regel im Router.

TIME-Zeilen kommen, aber kein ACCEPT.

Nur hamedata.com ist umgeleitet, marstekcloud.com nicht. Prüfe den zweiten DNS-Eintrag samt Platzhalter-Variante.

port is already allocated oder „address already in use“.

Auf dem Server belegt schon ein Programm Port 80 oder 443. Stoppe es oder nimm einen anderen Rechner. Die Ports sind nicht verhandelbar.

Nach einem Neustart ist der Container weg.

Docker startet nicht beim Booten. Führe sudo systemctl enable docker aus und starte den Server neu.

Ich will den Container neu aufsetzen.

Im Ordner ~/marstek-offline-endpoint entfernst du den Container mit docker compose down und startest ihn mit docker compose up -d neu. Daten und Zertifikat bleiben erhalten.

Die Einträge sollen wieder weg.

Entferne die DNS-Einträge (bei AdGuard die Umschreibungen löschen, bei Pi-hole die Datei /etc/dnsmasq.d/99-marstek.conf löschen und pihole restartdns ausführen) und stoppe den Container mit docker compose down. Die Batterie spricht dann wieder mit der Marstek-Cloud; ist die nicht erreichbar, beginnen die Resets von vorn.

Voraussetzungen

  • Ein Linux-Server (Debian oder Ubuntu, 64 Bit), der dauerhaft läuft, mit Terminal-Zugang (direkt oder per SSH)
  • Root-Rechte per sudo auf dem Server
  • Die Ports 80 und 443 sind auf dem Server frei
  • Eine feste IP-Adresse für den Server (DHCP-Reservierung im Router)
  • Zugriff auf den DNS-Server im Heimnetz (z. B. Pi-hole oder AdGuard Home)
  • Ein Marstek Venus (E, D oder E v3) im selben Netz
  • Internetverbindung zum Herunterladen von Docker und des Images

Andere Systeme

Unterstützen

Diese Tools entstehen in meiner Freizeit und bleiben kostenlos, offen und cloud-frei. Wenn dir eins davon einen Nachmittag gespart hat, freue ich mich über einen Kaffee oder ein Stück Pizza.

Kaffee oder Pizza