SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor · Aktualisiert 2026-08-27

openGym selbst hosten: Docker-Setup und Passkeys

openGym auf einem VPS mit Docker Compose bereitstellen: Git-Tag festlegen, TLS vor dem ersten Passkey einrichten, JSON-Daten finden und den schreibgeschützten MCP-Server nutzen.

Was Sie beim Self-Hosting von openGym erhalten

Sie hosten openGym selbst, indem Sie das Repository klonen, in .env zwei Zeilen bearbeiten und docker compose up -d --build hinter einem Reverse Proxy ausführen, der TLS (Transport Layer Security) terminiert. openGym ist ein Trainings- und Körpergewichtstracker: Wochenpläne, geführte Workouts, protokollierte Sätze und die Gewichtsentwicklung im Zeitverlauf. Die Software ist unter AGPL-3.0 lizenziert und speichert alle Daten als einfache JSON-Dateien auf Ihrer Festplatte. Daher müssen Sie keinen Datenbankserver betreiben.

Der Stack besteht aus zwei dauerhaft laufenden Containern: einem nginx-Container, der den React-Build bereitstellt, und einem Node-Container, der die API enthält. Zusätzlich gibt es einen einmalig ausgeführten Job, der beim ersten Start etwa 140 MB an Trainingsbildern und GIFs herunterlädt.

Zwei Punkte deutet die README des Projekts an, führt sie für die Bereitstellung auf einem öffentlich erreichbaren Server aber nicht aus. Die Passkey-Anmeldung ist an einen Hostnamen gebunden. Daher müssen die Domain und ihr Zertifikat vor der ersten Anmeldung vorhanden sein und nicht erst danach. Der optionale MCP-Server ist schreibgeschützt und läuft auf dem Rechner, auf dem Ihr AI-Client ausgeführt wird, nicht innerhalb des Stacks. Das ändert die erforderlichen Schritte, wenn die Daten auf einem VPS liegen.

openGym ist noch jung. Das erste getaggte Release, v1.0.0, ist auf den 20. Juli 2026 datiert, und v1.2.7 wurde am 18. August 2026 veröffentlicht. Dreizehn Tags in etwa einem Monat zeigen, dass sich die Anwendung noch schnell weiterentwickelt. Checken Sie daher ein Release-Tag aus, statt den aktuellen Stand des Default-Branches zu bauen.

Planen Sie die Domain vor der ersten Anmeldung

Passkeys dienen zur Anmeldung bei openGym. Ein Passkey ist an eine Relying-Party-ID (RP ID) gebunden. Dabei handelt es sich um die Domain, unter der das Zugangsmittel erstellt wurde. Browser erstellen Passkeys nur über HTTPS. Die einzige Ausnahme ist localhost.

Das macht sich besonders auf dem Smartphone bemerkbar. Öffnen Sie http://203.0.113.10:8080 von einem anderen Gerät. Es erscheint überhaupt keine Aufforderung zum Erstellen eines Passkeys, weil der Browser die Erstellung eines Zugangsmittels über eine reine HTTP-Quelle oder eine nicht näher bezeichnete IP-Adresse verweigert. In den eigenen Hinweisen zur Fehlerbehebung des Projekts steht dasselbe: Wenn keine Aufforderung erscheint, verwenden Sie http:// oder eine IP-Adresse.

Noch problematischer ist, dass die RP ID in jedem Zugangsmittel hinterlegt ist, das Ihre Benutzer bereits registriert haben. Wenn Sie RP_ID später ändern, stimmen die auf ihren Geräten gespeicherten Passkeys nicht mehr überein. Dann kann sich niemand mehr anmelden. Legen Sie den Hostnamen zuerst fest, verweisen Sie DNS auf den VPS und richten Sie das Zertifikat ein, bevor jemand auf Create profile tippt.

openGym mit Docker Compose bereitstellen

Die Compose-Datei bind-mountet ./data und ./media relativ zu sich selbst. Das Verzeichnis, in das Sie das Repository klonen, ist daher Ihre Datenbank. Wählen Sie dafür einen dauerhaften Speicherort.

sudo install -d -o "$USER" -g "$USER" /opt/opengym
git clone https://gitea.com/DuarteSantos/openGym /opt/opengym
cd /opt/opengym
cp .env.example .env

Die README-Datei enthält weiterhin eine github.com-Clone-URL. Diese Adresse ist nicht mehr erreichbar. Das oben genannte Gitea-Repository ist der aktuelle Projektstandort.

Bearbeiten Sie .env. Auf einem VPS sind drei Zeilen relevant.

RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080

RP_ID ist der reine Hostname. ORIGIN ist die vollständige URL einschließlich Schema. Beide müssen exakt mit der Adresse in der Browserzeile übereinstimmen. Andernfalls schlägt die Anmeldung mit verification failed fehl. Der Wert WEB_PORT wird im Abschnitt zum privaten Halten von Port 8080 erklärt.

docker compose up -d --build
docker compose ps
docker compose logs media

docker compose ps sollte web und api als laufend anzeigen. media sollte mit Exit-Code 0 beendet sein. Dieses Ende ist korrekt. Der Medienjob hat restart: "no", weil seine Aufgabe aus einem einmaligen Download besteht. Sein Log endet mit einer Zeile, die mit ✓ Exercise media ready beginnt. ls media/img | wc -l sollte einige hundert Einträge statt 0 ausgeben. Ein leeres Verzeichnis bedeutet, dass der Download fehlgeschlagen ist. Die Anwendung rendert dann Übungskarten mit leeren Bildern.

Das Flag --build ist hier erforderlich. Die Compose-Datei verwendet auf ghcr.io vorgefertigte Images, die nicht mehr veröffentlicht werden. Daher schlägt docker compose pull mit denied oder manifest unknown fehl. Die beiden Dienste werden stattdessen aus dem gerade geklonten Quellcode gebaut. Beide enthalten dafür einen Abschnitt build. Wenn Docker Compose für Sie neu ist, beginnen Sie mit Docker Compose auf einem VPS und kehren Sie anschließend hierher zurück.

Version festschreiben, weil dieses Projekt noch jung ist

Da dieser Registry-Namespace nicht mehr vorhanden ist, gibt es kein Image-Tag mehr, das Sie festschreiben können. Stattdessen legen Sie den Checkout auf dem Datenträger fest. Er bestimmt, welche Version der Anwendung im Container landet.

cd /opt/opengym
git fetch --tags
git checkout v1.2.7

git status meldet jetzt einen detached HEAD bei diesem Tag. Das ist auf einem Server der gewünschte Zustand. Der Checkout ändert sich nicht, bis Sie einen anderen auswählen.

Sorgen Sie anschließend dafür, dass Compose die Registry überhaupt nicht mehr kontaktiert. Legen Sie Folgendes in docker-compose.override.yml ab. Compose lädt diese Datei automatisch und führt sie mit der versionierten Datei zusammen. Skalare Schlüssel werden durch den Override ersetzt. Daher muss nichts in git geändert werden, und git pull bleibt unverändert. Unter Zusammenführung einer Override-Datei durch Compose finden Sie die vollständigen Regeln für das Zusammenführen.

services:
  api:
    pull_policy: build
  web:
    pull_policy: build

Damit baut ein späteres docker compose up -d aus dem vorhandenen Quellcode, statt beim Pull-Vorgang fehlzuschlagen. Prüfen Sie, ob der Merge übernommen wurde, und erstellen Sie das Image anschließend beim festgelegten Tag neu.

docker compose config | grep pull_policy
docker compose up -d --build

TLS-Terminierung mit einem Reverse Proxy

Die Container verwenden unverschlüsseltes HTTP. Davor muss ein Dienst das Zertifikat verwalten. Caddy ist der kürzeste Weg, weil es das Zertifikat selbstständig bei Let's Encrypt anfordert und erneuert.

gym.example.com {
    reverse_proxy 127.0.0.1:8080
}

nginx, Traefik und Nginx Proxy Manager funktionieren auf dieselbe Weise. Das gilt auch für einen Cloudflare Tunnel, den das Projekt dokumentiert und der überhaupt keinen offenen eingehenden Port benötigt.

curl -sI https://gym.example.com | head -1

Das sollte HTTP/2 200 ohne Zertifikatswarnung zurückgeben. Öffnen Sie die Website nun in einem Browser und wählen Sie Create profile. Wenn die Passkey-Abfrage erscheint und die Anmeldung anschließend meldet, dass verification failed, RP_ID oder ORIGIN nicht mit der URL in der Adressleiste übereinstimmt, korrigieren Sie .env und führen Sie docker compose up -d erneut aus. Dadurch werden die Container neu erstellt, damit sie die neuen Werte einlesen. Ein docker compose restart lädt .env nicht neu.

Port 8080 vom öffentlichen Internet fernhalten

Standardmäßig veröffentlicht der Webdienst 8080 auf allen Schnittstellen. Dadurch ist die Anwendung unter Ihrer öffentlichen IP-Adresse über unverschlüsseltes HTTP erreichbar, während der Proxy auf demselben Host HTTPS bereitstellt. Eine Firewall-Regel behebt das nicht. Docker veröffentlicht einen Port mit einer DNAT-Regel in der nat-Tabelle. Dieser Datenverkehr wird anschließend in der FORWARD-Kette verarbeitet, in der die eigenen Regeln von Docker ihn akzeptieren. Die Regeln von ufw greifen dagegen auf dem INPUT-Pfad. sudo ufw deny 8080/tcp blockiert daher nichts.

Die Lösung besteht darin, den Port nur an die Loopback-Adresse zu binden. Die Compose-Datei verwendet das Mapping "${WEB_PORT:-8080}:${NGINX_PORT:-80}". Der in WEB_PORT gesetzte Wert wird daher auf der linken Seite dieses Mappings eingesetzt. Die Kurzsyntax von Docker akzeptiert dort ein ip:port-Paar. Deshalb funktioniert WEB_PORT=127.0.0.1:8080.

docker compose config
sudo ss -ltnp | grep 8080

In der zusammengeführten Konfiguration soll unter ports des Webdienstes host_ip: 127.0.0.1 angezeigt werden. ss sollte 127.0.0.1:8080 und nicht 0.0.0.0:8080 anzeigen. Von einem anderen Rechner aus sollte curl http://<your-vps-ip>:8080 nun abgewiesen werden oder in einen Timeout laufen. Der HTTPS-Hostname muss weiterhin funktionieren.

Registrierung schließen, sobald Ihr Profil vorhanden ist

Die Registrierung ist standardmäßig geöffnet, und der Gastmodus ist aktiviert. Bei einem öffentlich erreichbaren Hostnamen bedeutet das, dass jeder, der die URL findet, ein Profil auf Ihrem Server anlegen kann. Registrieren Sie zuerst Ihr eigenes Profil und ermitteln Sie anschließend Ihre Benutzer-ID: ls data/ listet für jeden Benutzer eine Datei namens state-<uid>.json auf, und der Wert in <uid> ist der benötigte Wert.

ADMIN_UIDS=<your-uid>
INVITE_ONLY=1
ALLOW_GUEST=0

Führen Sie docker compose up -d erneut aus. Unter Settings wird nun ein Admin-Dashboard angezeigt. Dort können Sie Einladungscodes erstellen und widerrufen. Dadurch können sich die Personen, mit denen Sie trainieren, registrieren, während alle anderen ausgeschlossen bleiben. openGym unterstützt keine externen Identity Provider. Die Einladungscodes gelten daher nur für diese Anwendung und für nichts anderes auf dem Server. Wenn Sie stattdessen für jede Person ein einzelnes Konto für alle von Ihnen betriebenen Dienste bereitstellen möchten, setzen Sie Authentik als Forward-Auth-Proxy davor. Dadurch wird der Hostname geschützt, bevor der eigene Passkey-Login von openGym überhaupt geladen wird.

Wo die Daten liegen und welches Backup sie schützt

Alles liegt im Verzeichnis ./data, das im API-Container unter /data eingebunden ist. Es gibt vier Dateitypen: db.json enthält Profile und öffentliche Passkey-Anmeldedaten, state-<uid>.json enthält die Routinen, Workouts und das Körpergewicht eines Benutzers, secret ist der Schlüssel für das Session-Cookie und vapid.json enthält die Schlüssel für Push-Benachrichtigungen, die beim ersten Start erzeugt werden.

cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api

Stoppen Sie die API zuerst, weil tar Dateien kopiert, während die API möglicherweise in eine Datei schreibt. Eine nur teilweise kopierte JSON-Datei wird als beschädigte JSON-Datei wiederhergestellt. Das Stoppen und Starten dauert etwa zwei Sekunden. Kopieren Sie das Archiv anschließend vom Server herunter, weil ein Archiv auf dem VPS den Ausfall des VPS nicht übersteht. Lassen Sie media/ beim Backup aus: Es handelt sich um 140 MB Übungsbilder, die der Media-Job erneut und kostenlos herunterlädt.

Für die Wiederherstellung müssen Sie das Archiv auf einem Host, der dieselbe Domain bereitstellt, in denselben Pfad entpacken. Ein auf Ihrem Telefon gespeicherter Passkey ist an die RP ID gebunden, unter der er erstellt wurde. Bei einer Wiederherstellung unter einem neuen Hostnamen erhalten Sie daher eine funktionierende Datenbank, bei der sich niemand anmelden kann. Behalten Sie die Domain bei, oder planen Sie, jeden Passkey neu zu registrieren. Für alles andere, was Sie betreiben, gilt dieselbe Vorgehensweise. Das Backup und Upgrade eines Docker-Compose-Stacks beschreibt den allgemeinen Ablauf.

Der MCP-Server ist schreibgeschützt und läuft auf Ihrer Maschine

MCP (Model Context Protocol) ist die Schnittstelle, über die ein Client wie Claude Desktop oder Cursor mit einem lokalen Tool-Server kommuniziert. openGym liefert einen solchen Server in mcp/ aus. Er ist nicht Teil der Compose-Datei, läuft nicht in einem Container und lauscht an keinem Port. Der Client startet ihn als untergeordneten Prozess und kommuniziert über stdio mit ihm. Deshalb steht in der README, dass er Ihre Maschine nie verlässt.

Installieren Sie ihn dort, wo der Client läuft, nicht auf dem Server:

cd openGym/mcp
npm install

Fügen Sie ihn anschließend zu claude_desktop_config.json hinzu:

{
  "mcpServers": {
    "opengym": {
      "command": "node",
      "args": ["/absolute/path/to/openGym/mcp/src/index.js"],
      "env": {
        "OPENGYM_DATA": "/absolute/path/to/openGym/data",
        "OPENGYM_UID": "<your-uid>"
      }
    }
  }
}

OPENGYM_UID ist bei einer Einzelbenutzerinstallation optional, wenn der Server das einzige gefundene Profil automatisch erkennt. Der Server stellt acht Tools bereit: list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm und muscle_balance. Jedes dieser Tools liest Daten. Keines schreibt Daten. Ein Assistent kann daher beantworten, was Sie in der vergangenen Woche trainiert haben, aber keinen Satz protokollieren, keine Routine bearbeiten und nichts löschen. Diese Liste ist ein kompaktes Beispiel für eine grundlegende Entscheidung beim Agent-Design: Die bereitgestellten Tools bestimmen vollständig, was ein Modell tun kann. Wenn Sie den Ablauf selbst schreiben, lernen Sie, wie Agenten funktionieren ist der schnellste Weg zu verstehen, warum ein schreibgeschützter Werkzeugsatz eine Designentscheidung und keine Einschränkung ist.

Hier liegt die Aufgabe, die Sie bei einem VPS lösen müssen. OPENGYM_DATA ist ein Dateisystempfad. Ihre Daten liegen auf dem VPS, während Ihr KI-Client auf Ihrem Laptop läuft. Dafür gibt es zwei praktikable Möglichkeiten.

  1. Kopieren Sie die Daten auf den Client und verweisen Sie den Server auf die Kopie: rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/. Setzen Sie anschließend OPENGYM_DATA auf ~/opengym-data. Der Server liest die Daten nur, daher geht durch die Kopie nichts verloren. Führen Sie rsync erneut aus, wenn Sie aktuelle Werte benötigen.
  2. Führen Sie den Server über ssh aus. Setzen Sie command auf ssh und args auf ["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]. Dafür muss Node auf dem VPS installiert sein. Außerdem muss die Anmeldung auf stdout nichts ausgeben, weil stdout als Protokollkanal dient.

Beide Optionen setzen voraus, dass der Agent selbst auf Ihrem Laptop läuft. Wenn er stattdessen auf demselben Server wie die Daten laufen soll, stellt OneCLI jeder Person einen isolierten Agenten auf dem Server bereit, sodass der stdio-Hop zurück zu data/ wieder lokal erfolgt.

Wenn cat data/db.json Permission denied zurückgibt, hat der API-Container diese Dateien als root erstellt und Ihr Benutzer kann sie nicht lesen. Kopieren Sie die Dateien mit sudo, oder ändern Sie den Eigentümer auf dem Host. Wenn der Server über das Netzwerk statt über stdio lauschen soll, lesen Sie MCP-Server auf einem VPS ausführen.

openGym oder wger: Was sollten Sie betreiben?

wger ist die etablierte Option in diesem Bereich und eine deutlich größere Software. Sein Compose-Stack führt gunicorn für eine Django-Anwendung, PostgreSQL, Redis und einen Celery-Worker hinter nginx aus. Dafür erhalten Sie die Erfassung von Ernährung und Zutaten, eine dokumentierte REST-API, eine große Datenbank mit Übungen sowie Funktionen für Trainer, die die Trainingspläne anderer Personen verwalten.

openGym besteht aus zwei Containern, einem Ordner mit JSON-Dateien und keinen zu verwaltenden Konten außer Passkeys. Das ist der gesamte Unterschied. Wenn Sie bereits einmal eine Chatwoot-Installation betrieben haben, bei der ein Backup aus einem Postgres-Dump zusammen mit dem Upload-Verzeichnis besteht und jedes Versionsupdate Datenbankmigrationen ausführt, wissen Sie bereits, welchen Wartungsaufwand die Struktur von wger mit sich bringt.

Betreiben Sie wger, wenn Sie neben dem Training auch Lebensmittel erfassen möchten oder eine API benötigen, auf der Sie aufbauen können. Betreiben Sie openGym, wenn Sie einen Stack möchten, den Sie an einem Nachmittag vollständig nachvollziehen können, und eine Anmeldung ohne Passwort, das abhandenkommen kann. Der Nachteil dieser Entscheidung ist die geringere Reife: Am 19. August 2026 ist die erste openGym-Version erst einen Monat alt, während wger auf mehrere Jahre an Releases zurückblickt. Legen Sie Ihre Version fest, halten Sie die Backups aktuell und lesen Sie vor jedem Update die Release Notes.

Wenn Sie noch entscheiden, was auf dem Server Platz verdient, behandelt was sich 2026 für das Self-Hosting lohnt die verschiedenen Vor- und Nachteile. Diese Anwendung passt auf demselben kleinen VPS gut neben Mealie für Rezepte oder Actual Budget für Finanzen.

Aktualisieren, ohne Daten zu verlieren

cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api
git fetch --tags

Prüfen Sie den gewünschten Release mit git checkout v<new> aus. Führen Sie anschließend docker compose up -d --build aus, damit die Container aus diesem Tag neu erstellt werden. Das Backup wird immer zuerst erstellt, weil der Wiederherstellungsweg für JSON-Dateien auf dem Datenträger aus einem tar-Befehl besteht und nur wenige Sekunden dauert.

FAQ

Warum zeigt openGym auf meinem Telefon nie eine Aufforderung für einen Passkey an?

Der Browser verweigert das Erstellen einer Anmeldedaten, weil Sie sich unter http:// oder unter einer reinen IP-Adresse wie http://192.168.1.20:8080 befinden. Browser erlauben Passkeys nur für HTTPS-Ursprünge, mit localhost als einziger Ausnahme. Betreiben Sie openGym hinter einem Reverse Proxy mit einem echten Zertifikat für einen echten Hostnamen, setzen Sie RP_ID=gym.example.com und ORIGIN=https://gym.example.com in .env und führen Sie docker compose up -d aus, damit die Container die neuen Werte übernehmen. Wenn die Aufforderung erscheint, die Anmeldung aber verification failed meldet, stimmen diese beiden Werte nicht exakt mit der URL in der Adressleiste überein.

Wo speichert openGym meine Daten, und wie sichere ich sie?

Im Verzeichnis ./data neben der Compose-Datei, das als /data in den API-Container eingebunden wird. Es enthält db.json für Profile und öffentliche Passkey-Anmeldedaten, jeweils eine Datei state-<uid>.json pro Benutzer für Workouts und Körpergewicht, secret für den Schlüssel des Sitzungscookies und vapid.json für Push-Benachrichtigungsschlüssel. Sichern Sie die Daten mit docker compose stop api, anschließend mit tar czf ~/opengym-$(date +%F).tar.gz data/ und dann mit docker compose start api. Kopieren Sie das Archiv vom Server. Lassen Sie media/ aus. Dieses Verzeichnis enthält 140 MB an Trainingsbildern, die der Medien-Job selbstständig erneut herunterlädt.

Kann Claude meinen openGym-Trainingsverlauf lesen?

Ja, über den optionalen MCP-Server im Verzeichnis mcp/, und nur lesend. Er stellt acht Tools für Routinen, Wochenpläne, protokollierte Workouts, Körpergewicht, geschätztes One-Rep-Maximum und Muskelbalance bereit. Keines dieser Tools schreibt Daten zurück. Der Server ist kein Container und öffnet keinen Port. Ihr Client startet ihn über stdio, und er liest die JSON-Dateien unter OPENGYM_DATA direkt. Da es sich um einen Dateisystempfad handelt, müssen Sie bei openGym auf einem VPS entweder eine Kopie von data/ auf den Rechner synchronisieren, auf dem der Client läuft, oder den Server über ssh aus der Client-Konfiguration aufrufen.

Sollte ich openGym oder wger selbst hosten?

Wählen Sie wger, wenn Sie zusätzlich zu Ihrem Trainingsprotokoll Lebensmittel und Ernährung erfassen möchten oder eine dokumentierte REST API benötigen, auf der Sie aufbauen können. wger verwendet einen größeren Stack: Django unter gunicorn, PostgreSQL, Redis und einen Celery-Worker hinter nginx. Wählen Sie openGym, wenn Sie zwei Container, mit cat lesbare JSON-Dateien und eine Passkey-Anmeldung ohne zu verwaltende Passwörter möchten. Am 19. August 2026 ist das erste getaggte Release von openGym erst einen Monat alt. Prüfen Sie daher einen git-Tag aus und sichern Sie data/ vor jedem Update.