Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 24.10.2026 · 10 min Lesezeit

openGym selbst hosten: Fitness-Tracker mit Passkey-Login per Docker

openGym ist ein quelloffener Trainingstracker mit Passkey-Anmeldung, der gerade auf GitHub stark wächst. Die Anleitung zeigt die Installation mit Docker Compose und gepinnten Images, die richtige Einstellung von RP_ID und ORIGIN hinter einem Reverse Proxy, die Sicherung des Datenordners samt getestetem Restore und den Update-Weg von 1.4.1 auf 1.4.2.

Geprüft am 24.10.2026 · für openGym 1.4.2

Mit KI erstellt – redaktionelle Prüfung ausstehend

WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Titelbild zur Anleitung: openGym selbst hosten mit Passkey-Login, Backup und Docker Compose

openGym ist ein quelloffener Trainings- und Körpergewichtstracker, den Sie auf dem eigenen Server betreiben. Sie planen Routinen pro Wochentag, protokollieren Sätze und Gewichte und synchronisieren alles zwischen Smartphone und Rechner. Angemeldet wird per Passkey statt Passwort. Das Projekt steht unter AGPL-3.0 und hat in den letzten 30 Tagen rund 14.000 Sterne auf GitHub gesammelt. Diese Anleitung zeigt die Installation mit Docker Compose, die richtige HTTPS-Konfiguration für Passkeys, Backup und Wiederherstellung des Datenordners sowie den Update-Weg. Alle Schritte haben wir mit den Versionen 1.4.1 und 1.4.2 im Labor durchgespielt.

Voraussetzungen

openGym ist sehr genügsam. Im Test belegte der API-Container rund 28 MiB RAM, der Webcontainer rund 13 MiB. Mehr Platz brauchen die Images: Das Web-Image ist entpackt etwa 540 MB groß, weil es die Übungsbilder und Animationen enthält, das API-Image etwa 250 MB. Die Images gibt es für amd64 und arm64.

  • Linux-Server oder Mini-PC mit 1 bis 2 CPU-Kernen, 1 GB freiem RAM und 2 GB freiem Speicher, x86 oder ARM
  • Docker Engine mit Docker Compose Plugin (Einrichtung siehe Docker-Compose-Grundlage für Linux)
  • eine Domain oder Subdomain wie gym.example.de, deren DNS auf den Server zeigt
  • ein Reverse Proxy mit gültigem Zertifikat, zum Beispiel Caddy, Traefik oder Nginx Proxy Manager
  • ein Browser oder Smartphone mit Passkey-Unterstützung

Schritt 1: Projektordner und Compose-Datei anlegen

Das Repository enthält eine docker-compose.yml, die für jeden Dienst sowohl image: mit dem Tag latest als auch build: mit Verweis auf die Quellordner setzt. Das ist für Entwickler praktisch, für den Betrieb aber unglücklich: Wer nur die Datei ohne Quellcode herunterlädt, landet beim Bauen bei lstat ... /web: no such file or directory, und latest wechselt bei jedem Release ohne Vorwarnung. Wir verwenden deshalb eine reine Image-Variante mit fest gepinnten Versionen. Das Volume ./coach-auth fehlt bewusst, es wird nur für den optionalen KI-Coach mit Codex-Anbindung gebraucht, der ein eigenes Build-Target voraussetzt.

mkdir -p /opt/opengym && cd /opt/opengym

Legen Sie dort die Datei compose.yaml an:

services:
  api:
    image: ghcr.io/duartesantos8/opengym-api:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      - PORT=3000
      - DATA_DIR=/data
      - TRUST_PROXY=1
    volumes:
      - ./data:/data

  web:
    image: ghcr.io/duartesantos8/opengym-web:1.4.2
    restart: unless-stopped
    depends_on:
      - api
    ports:
      - "127.0.0.1:8080:80"

Der Webcontainer liefert die Oberfläche aus und leitet /api intern an den API-Container weiter, nach außen gibt es also nur einen Port. Er ist hier an 127.0.0.1 gebunden, weil der Reverse Proxy auf demselben Host davor sitzt. Läuft der Proxy auf einem anderen Rechner, lassen Sie die Adresse weg und schränken den Zugriff per Firewall ein (siehe Linux-Server mit ufw und Fail2ban absichern). TRUST_PROXY=1 entspricht der Vorgabe des Projekts: Der Webcontainer überschreibt X-Forwarded-For mit der echten Gegenstelle, sodass die API Anmeldeversuche pro Adresse drosseln kann.

EckdatumWert
Imagesghcr.io/duartesantos8/opengym-api:1.4.2, ghcr.io/duartesantos8/opengym-web:1.4.2
Port80 im Webcontainer, im Beispiel lokal auf 8080
Volume./data:/data (Konten, Passkeys, Trainingsdaten, Sitzungsschlüssel, Uploads)
Pflicht-EnvRP_ID, ORIGIN, RP_NAME
Optionale EnvFIRST_USER_ADMIN, INVITE_ONLY, ALLOW_GUEST, PASSWORD_LOGIN, DEFAULT_LANG
Healthcheckim Image enthalten, prüft /api/health bzw. / alle 5 Minuten

Verifizieren: docker compose config --quiet gibt nichts aus und endet ohne Fehler. Dann ist die YAML-Datei gültig.

Schritt 2: .env mit Domain und Passkey-Werten anlegen

Passkeys (WebAuthn) sind an einen exakten Hostnamen gebunden und funktionieren laut Browserregeln nur über HTTPS, einzige Ausnahme ist http://localhost. Ein Aufruf über http://192.168.x.x:8080 aus dem Heimnetz reicht also nicht. Tragen Sie deshalb gleich die spätere öffentliche Adresse ein:

RP_ID=gym.example.de
ORIGIN=https://gym.example.de
RP_NAME=openGym
FIRST_USER_ADMIN=1
DEFAULT_LANG=de

RP_ID ist der nackte Hostname ohne Schema, Port und Schrägstrich. ORIGIN ist die komplette Adresse mit https://, aber ohne abschließenden Schrägstrich. Mit FIRST_USER_ADMIN=1 wird das erste Profil, das auf einer leeren Instanz entsteht, zum Admin. DEFAULT_LANG=de startet Anmeldebildschirm und neue Profile auf Deutsch. Legen Sie die Domain vor dem ersten Profil fest: Ändern Sie RP_ID später, sind alle vorhandenen Passkeys ungültig und jeder muss sich neu registrieren.

Verifizieren: grep -E '^(RP_ID|ORIGIN)=' .env zeigt genau die Adresse, die später in der Adresszeile des Browsers steht.

Schritt 3: Container starten und Gesundheit prüfen

docker compose up -d
docker compose ps
docker compose logs api | grep 'gym-api on'
curl -s http://127.0.0.1:8080/api/health

Im Test meldete der API-Container beim Start genau die geladenen Werte. Diese Zeile ist die wichtigste Kontrolle, denn sie zeigt, was der Server wirklich verwendet:

api-1  | gym-api on :3000 (rpID=gym.example.de, origin=https://gym.example.de)
{"ok":true,"users":0,"writable":true}

writable: true bestätigt, dass die API in ./data schreiben kann. Direkt nach dem ersten Start liegen dort nur secret (Schlüssel für die Sitzungscookies) und vapid.json (Schlüssel für Push-Benachrichtigungen), beide mit Rechten 600 und Eigentümer root.

Verifizieren: Beide Dienste stehen in docker compose ps auf Up, /api/health liefert "ok":true, und die Logzeile nennt Ihre Domain. healthy erscheint wegen des 5-Minuten-Intervalls erst nach einigen Minuten.

Schritt 4: HTTPS per Reverse Proxy vorschalten

Der Proxy terminiert TLS und leitet alles an den Webcontainer weiter. Mit Caddy genügen zwei Zeilen, das Zertifikat holt Caddy selbst (Details in der Caddy-Anleitung):

gym.example.de {
    reverse_proxy 127.0.0.1:8080
}

Zwei Punkte aus der Projektdokumentation sollten Sie beachten. Setzen Sie keine eigene CORS-Middleware vor /api/, openGym beantwortet die Preflight-Anfragen der Smartphone-App selbst. Begrenzt Ihr Proxy die Größe von Anfragen (nginx erlaubt standardmäßig 1 MiB), geben Sie /api/ mindestens 5 MiB und /api/media/ mehr als die 40 MB für Übungsvideos.

Verifizieren: curl -s https://gym.example.de/api/health liefert von außen dieselbe JSON-Antwort wie lokal.

Schritt 5: Profil mit Passkey anlegen und erste Routine erstellen

Öffnen Sie https://gym.example.de, wählen Sie Neues Profil erstellen, geben Sie einen Namen ein und bestätigen Sie mit Passkey erstellen. Der Browser fragt nach Fingerabdruck, Gesicht oder PIN, danach landen Sie auf der Startseite mit Wochenansicht und Körpergewicht.

openGym-Startseite nach der Anmeldung mit Wochenkalender, Check-in und Körpergewicht
Startseite eines frisch angelegten Profils

Unter Plan und Routinen legen Sie mit Neue Routine einen Trainingstag an, benennen ihn und fügen über Übung hinzufügen Übungen aus der Bibliothek hinzu. Die Suche findet deutsche Namen wie „Kniebeuge“. Jede Übung bekommt Sätze, Wiederholungen und Gewicht, die App zeigt dazu die trainierten Muskelgruppen.

Routine in openGym mit einer Kniebeuge-Übung, drei mal zehn Wiederholungen und Muskelkarte
Eine Routine mit Übung, Satzschema und Muskelübersicht

Die App synchronisiert sofort. Auf dem Server entstehen dabei db.json mit Konto und Passkey-Schlüssel, eine Sicherheitskopie db.json.bak, audit.log und je Profil eine Datei state-<ID>.json mit Routinen und Trainings. In db.json steht beim ersten Profil "admin": true, im Aktivitätsprotokoll der Eintrag admin.first-user.

Über Einstellungen, Konto und Admin-Dashboard sehen Sie Nutzerzahlen, Einladungscodes und das Aktivitätsprotokoll. Das Dashboard war im Test trotz DEFAULT_LANG=de noch überwiegend englisch.

Admin-Dashboard von openGym mit Nutzerzahlen, KI-Coach-Hinweis und Einladungscodes
Das Admin-Dashboard erscheint nur für Admin-Profile

Wollen Sie nach dem eigenen Profil niemanden mehr frei registrieren lassen, setzen Sie laut Dokumentation INVITE_ONLY=1 und erzeugen Einladungscodes im Dashboard. ALLOW_GUEST=0 entfernt zusätzlich den Gastmodus, der Daten nur im Browser speichert. Beide Werte wirken nach docker compose up -d.

Verifizieren: curl -s https://gym.example.de/api/health meldet jetzt "users":1, und ls data/ zeigt eine Datei state-*.json.

Schritt 6: Backup anlegen und Wiederherstellung testen

Alles, was die Instanz ausmacht, liegt in ./data. Die Dokumentation empfiehlt ein tar-Archiv. Halten Sie die Container dafür kurz an, damit keine Datei mitten im Schreiben gesichert wird:

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

Das Archiv enthielt im Test secret, vapid.json, audit.log, db.json, db.json.bak und die state-Datei. Weil secret und die Passkey-Schlüssel darin stecken, gehört das Archiv verschlüsselt auf ein zweites System, etwa mit Autorestic und Restic. Fotos und Videos eigener Übungen landen in data/uploads/.

Den Ernstfall haben wir mit einem Totalverlust geprobt: Container entfernen, data/ löschen, neu starten. Die Instanz meldete danach "users":0, die Routine war weg, und die bestehende Browsersitzung bekam auf /api/me nur noch 401 {"error":"not signed in"}. So stellen Sie wieder her:

docker compose down
rm -rf data
tar xzf opengym-backup-2026-10-24.tar.gz
docker compose up -d
curl -s http://127.0.0.1:8080/api/health

Nach dem Entpacken meldete /api/health wieder "users":1, die Test-Routine war vollständig zurück, und weil secret mitgesichert war, blieb sogar die alte Browsersitzung gültig. Auch die Anmeldung per Passkey funktionierte danach ohne neue Registrierung.

Verifizieren: Nach dem Restore liefert /api/health die alte Nutzerzahl, und Ihre Routinen erscheinen in der App ohne erneute Anmeldung.

Schritt 7: Auf eine neue Version aktualisieren

Wir haben openGym zunächst mit 1.4.1 installiert, Profil und Routine angelegt und dann auf 1.4.2 gehoben. Ändern Sie dafür nur die beiden Tags in der compose.yaml und ziehen Sie die neuen Images:

sed -i 's/:1.4.1/:1.4.2/' compose.yaml
docker compose pull
docker compose up -d
docker inspect -f '{{index .Config.Labels "org.opencontainers.image.version"}}' \
  $(docker compose ps -q)

Beide Container meldeten danach 1.4.2, die Routine war unverändert vorhanden, und die Sitzung blieb erhalten. Die Oberfläche zeigte unter Einstellungen zunächst noch v1.4.1, weil der Service Worker die alte App-Hülle zwischenspeichert. Nach einem Neuladen stand dort openGym v1.4.2. Achten Sie beim Pinnen auf die Schreibweise: Die GitHub-Releases heißen v1.4.2, in der Registry fehlt das v. Ein Pull von opengym-api:v1.4.2 scheitert:

Error response from daemon: failed to resolve reference "ghcr.io/duartesantos8/opengym-api:v1.4.2": ghcr.io/duartesantos8/opengym-api:v1.4.2: not found

Für Hinweise auf neue Versionen ohne automatisches Aktualisieren eignet sich Diun (siehe Container-Updates mit Diun und WUD). Sichern Sie vor jedem Update data/ wie in Schritt 6.

Verifizieren: Das Label org.opencontainers.image.version zeigt bei beiden Containern die neue Version, und Einstellungen, Info & Updates nennt nach einem Neuladen dieselbe Nummer.

Troubleshooting

Die häufigste Fehlerquelle sind RP_ID und ORIGIN. Beide Varianten haben wir gezielt provoziert.

SymptomUrsache und Lösung
Beim Erstellen des Passkeys meldet der Browser: The relying party ID is not a registrable domain suffix of, nor equal to the current domain.RP_ID passt nicht zur aufgerufenen Domain, im Test stand noch der Standardwert localhost drin, während die Seite über HTTPS und eine echte Domain lief. RP_ID auf den Hostnamen setzen und docker compose up -d ausführen.
Passkey wird im Browser erzeugt, das Profil entsteht aber nicht. Die API antwortet mit 400: Unexpected registration response origin "https://gym.example.de", expected "http://localhost:8080"RP_ID stimmt, ORIGIN nicht. Die Meldung nennt die geladenen Werte gleich mit. Im Test zeigte der Dialog keinen sichtbaren Hinweis, im audit.log stand auth.register.fail mit verify-error. ORIGIN korrigieren.
Änderung an der .env wirkt nichtdocker compose restart liest die .env nicht neu. Verwenden Sie docker compose up -d und prüfen Sie die Logzeile gym-api on.
Kein Passkey-Dialog auf dem SmartphoneAufruf über http:// oder eine IP-Adresse. Passkeys brauchen HTTPS, siehe Schritt 4.
App meldet, der Server akzeptiere diesen Browser nicht mehrDie Sitzung ist ungültig, etwa nach Verlust von data/secret oder nach „Überall abmelden“. Neu anmelden, die lokalen Änderungen werden danach zusammengeführt.

Häufige Fragen

Brauche ich eine Datenbank?

Nein. openGym speichert Konten in db.json und die Trainingsdaten je Profil in einer eigenen JSON-Datei. Manche Projektbeschreibungen im Netz nennen MongoDB, das trifft auf den aktuellen Stack nicht zu.

Geht es auch ohne Passkeys?

Ja. Mit PASSWORD_LOGIN=1 erlaubt openGym zusätzlich eine Anmeldung mit Profilname und Passwort, etwa für Browser im LAN ohne HTTPS. Die Funktion ist standardmäßig aus, Passkeys bleiben die empfohlene Variante.

Können mehrere Personen eine Instanz nutzen?

Ja. Jedes Profil sieht nur die eigenen Daten. Mit INVITE_ONLY=1 braucht jedes neue Profil einen Einladungscode aus dem Admin-Dashboard.

Was passiert, wenn ich data/secret verliere?

Alle Sitzungen und gekoppelten Smartphones werden ungültig, die Daten bleiben aber erhalten. Im Restore-Test blieb die alte Sitzung nur deshalb gültig, weil secret im Archiv lag.

Sendet openGym Daten nach außen?

Laut FAQ des Projekts gibt es keine Telemetrie. Der optionale KI-Coach ist aus, bis ein Admin ihn einrichtet, und überträgt dann Trainingsdaten an den gewählten Anbieter.

Fazit

openGym ist schnell installiert und braucht kaum Ressourcen. Die eigentliche Arbeit steckt in der HTTPS-Strecke: Mit passender Domain, korrektem RP_ID und ORIGIN funktioniert die Anmeldung per Passkey ohne Umwege, mit einem Tippfehler scheitert sie, teils ohne sichtbaren Hinweis. Die Datenhaltung in einem einzigen Ordner macht Backup und Umzug angenehm einfach, im Test kam nach einem Totalverlust alles samt Sitzung zurück. Für Vereine, kleine Teams oder die Familie ist das eine schlanke Alternative zu Cloud-Fitness-Apps. Pinnen Sie die Versionen, sichern Sie data/ regelmäßig und legen Sie die Domain vor dem ersten Profil fest.

Weiterführende Anleitungen und Quellen

openGymDocker ComposePasskeysSelfhostingFitness-TrackerWebAuthnBackup