„No subscription, no account required, and no telemetry. Not now, not later.“ So steht es auf der Startseite von openGym, einem Trainingstagebuch, das du auf deinem eigenen Server laufen lässt. Der zweite Satz ist der, den ich mir merke: „Nicht jetzt, nicht später“ ist genau der Satz, den man bei Fitness-Apps spätestens beim ersten Abo-Hinweis vermisst. Ob openGym es hält, weiß heute niemand, das Projekt ist 87 Tage alt. Was man heute prüfen kann: der Code (AGPL), das Tempo (fünf Releases in drei Wochen) und ob es bei dir läuft. Das habe ich getan, und die Installation dauert zehn Minuten.

Kurz gesagt: openGym ist ein selbst gehosteter Trainingsplaner und -logger: Routinen anlegen, Sätze und Gewichte eintragen, Körpergewicht verfolgen, Daten aus Hevy, Strong oder FitNotes importieren. Zwei Container, keine Datenbank, alles liegt als JSON-Dateien in einem Ordner. Am Ende läuft es auf Port 8080 in deinem Heimnetz, mit einem Admin-Profil und Passwort-Anmeldung. Dauer: etwa 10 Minuten. Stufe: Standard (Docker und Kommandozeile im Groben bekannt). Stand: openGym 1.4.1, geprüft am 13-10-2026.
Schaubild: openGym in drei Schritten, Compose-Datei anlegen, Container starten und anklopfen, erstes Profil anlegen
Der ganze Weg auf einen Blick: Compose-Datei, Start mit Gesundheitscheck, erstes Profil als Admin.

Was openGym ist und welches Problem es löst

Wer im Fitnessstudio trainiert, schreibt irgendwo auf, was er gehoben hat: Übung, Gewicht, Wiederholungen, und beim nächsten Mal ein bisschen mehr. Früher war das ein Notizbuch, heute eine App. Die Apps sind gut geworden, und viele von ihnen sind zum Abo geworden, während dein Trainingsverlauf, teils über Jahre, auf ihren Servern liegt. Wer kündigt, exportiert eine CSV-Datei und hat dann eine Tabelle, aber keine App mehr.

openGym ist die Antwort darauf, und zwar eine, die auf dem Handy benutzbar ist. Du legst Routinen an (Push/Pull/Legs, Ganzkörper, 5x5, vier Vorlagen sind dabei), startest ein Training, und die App füllt die Gewichte vom letzten Mal vor, startet den Pausentimer und merkt sich Bestleistungen. Dazu kommen Supersätze, Aufwärmsätze, Körpergewicht mit Zielkurve, eine Jahres-Heatmap und seit Version 1.4.0 eine Übungsbibliothek mit 5.632 Übungen samt Animationen. Der Import aus Hevy, Strong und FitNotes ist eingebaut, als CSV oder bei Hevy auch per API-Schlüssel. Du verlierst beim Umzug also nicht deine Historie.

Technisch ist es angenehm klein: ein Container web mit nginx, der die App ausliefert, und ein Container api mit Node.js, der deine Daten verwaltet. Keine Datenbank. Jedes Profil ist eine JSON-Datei in einem Ordner auf deinem Server, und ein Backup ist ein tar dieses Ordners. Für ein Homelab ist das der Idealfall, weil es nichts gibt, was man kaputt-migrieren kann.

Handy oder Browser→ Port 8080 →web (nginx)→ /api →api (Node.js)→ schreibt →./data (JSON)

Warum gerade alle darüber reden

Das Repository ist am 18-07-2026 entstanden und hat heute 10.746 Sterne auf GitHub, zuletzt mehrere hundert am Tag. Der Entwickler heißt Duarte Santos, und sein Tempo ist das eines Projekts, das gerade seinen Moment hat: 1.3.8 am 20-09-2026, 1.3.9 am 28-09, 1.3.10 am 07-10, 1.4.0 am 09-10 und 1.4.1 am 10-10. Die 1.4.0 hat die Übungsbibliothek von 1.324 auf 5.632 Einträge vervierfacht, mit lizenzierten Animationen von Gym Visual. Es freut mich, wenn jemand in drei Monaten etwas hinstellt, wofür andere Firmen Jahre und ein Abo-Modell brauchen. Und dasselbe Tempo macht mich vorsichtig: Ein Projekt, das zwei Releases in zwei Tagen schiebt, hat 132 offene Issues, und das ist bei dieser Geschwindigkeit normal.

Zwei Dinge sollte man wissen, bevor man sich verliebt. Erstens steht in der FAQ, dass openGym mit Claude Code gebaut wird und der Entwickler die Releases selbst durchsieht und testet. Ich halte das für ehrlich und nehme es als das, was es ist: KI als Werkzeug, der Beweis ist das Ergebnis, nicht die Herkunft. Zweitens hat der Entwickler am 12-10-2026 in Issue #554 eine Contributor-Vereinbarung (CLA) eingeführt, weil er eine iOS-App in den App Store bringen will und Apples Bedingungen sich mit reinem AGPL-Code schlecht vertragen. Der bisherige Code bleibt AGPL, das steht ausdrücklich drin. Wer nicht unterschreibt, dessen Beitrag wird neu geschrieben statt erzwungen. Das ist sauber gelöst, und ich sage trotzdem, dass eine CLA immer die Tür ist, durch die später eine andere Lizenz kommen kann. Für dich als Nutzer ändert das heute nichts; für den Stein im Brett ist es eine Fußnote.

Die Passkey-Hürde im Heimnetz

Bevor du installierst, der Punkt, an dem laut Handbuch die meisten Support-Anfragen entstehen. openGym meldet dich standardmäßig mit Passkeys an.

Passkey? Ein Schlüssel statt eines Passworts: Dein Handy oder Laptop erzeugt ein Schlüsselpaar, der Browser gibt es nur an genau die Adresse heraus, für die es angelegt wurde, und du bestätigst mit Fingerabdruck oder Gesicht. Nichts zum Merken, nichts zum Phishen. Der Haken: Browser erlauben Passkeys nur über HTTPS oder auf localhost.

Im Heimnetz tippst du aber http://192.168.178.50:8080 ein, und das ist weder HTTPS noch localhost. Der Passkey-Dialog erscheint dann gar nicht, ohne Fehlermeldung. Das Handbuch bietet drei Auswege: den Gastmodus, der alles nur im Browser speichert und nie mit dem Server redet; eine Passwort-Anmeldung, die man in der Konfiguration freischalten muss; oder HTTPS über einen Reverse Proxy mit eigener Domain. Ich nehme für diese Anleitung die Passwort-Anmeldung, weil sie ohne Domain funktioniert und die Daten trotzdem auf dem Server landen. HTTPS kommt als Ausbaustufe, dazu unten mehr. Dieser Umweg ist der Preis dafür, dass openGym bei der Anmeldung das Sichere zum Standard macht, und ich finde den Preis in Ordnung.

Was du brauchst

  • Einen Linux-Server mit Docker und Docker Compose. Prüfen mit docker compose version. Fehlt Docker, hilft die Docker-Compose-Anleitung für Einsteiger. Die Images gibt es für amd64 und arm64, ein Raspberry Pi 4 oder 5 ist also dabei.
  • Einen freien Port 8080. Prüfen mit ss -tlnp | grep 8080 || echo "Port frei". Läuft dort schon etwas, nimmst du in der Compose-Datei links vom Doppelpunkt eine andere Nummer und trägst sie auch in ORIGIN ein. Wie du den Belegten findest, steht in Port belegt? So findest du den Schuldigen.
  • Platz für zwei Images; allein die Übungsbilder im Web-Image sind laut Handbuch rund 80 MB. Prüfen mit df -h /.
  • Die IP-Adresse deines Servers, weil openGym sie in der Konfiguration wissen muss. Prüfen mit hostname -I; die erste Adresse ist die richtige.
  • IP-Adresse deines Servers: 192.168.178.50
  • Ordner für die Compose-Datei: ~/opengym
  • Port für die Oberfläche: 8080
  • Name des ersten Profils: sakis
So liest du diese Anleitung: Alles, was du selbst tun sollst, steht in einem farbigen Kasten. [ per klick ] heißt: Das erledigst du mit der Maus in einer Web-Oberfläche, hier der openGym-Oberfläche im Browser. [ über die shell ] heißt: Den Befehl kopieren, ins Terminal einfügen, Enter. Stehen beide Kästen direkt untereinander, führen sie zum selben Ergebnis. Du gehst EINEN davon, nicht beide.

Schritt 1: Ordner und Compose-Datei anlegen

Das Handbuch sieht vor, dass du das Repository klonst und die mitgelieferte Compose-Datei nimmst. Die enthält aber auch Bauanleitungen für den Fall, dass du die Images selbst kompilieren willst, und eine Variante mit KI-Coach. Für ein Homelab reicht der Kern: dieselben zwei Dienste, dieselben Einstellungen, nur ohne den Ballast. Die Werte stammen aus der offiziellen docker-compose.yml und der .env.example des Projekts.

mkdir -p ~/opengym && cd ~/opengym
nano compose.yaml

In den leeren Editor kommt dieser Inhalt. Die Adresse 192.168.178.50 ersetzt du an beiden Stellen durch die IP deines Servers:

name: opengym

services:
  api:
    image: ghcr.io/duartesantos8/opengym-api:1.4.1
    restart: unless-stopped
    environment:
      - PORT=3000
      - DATA_DIR=/data
      - TRUST_PROXY=1
      - RP_ID=192.168.178.50
      - ORIGIN=http://192.168.178.50:8080
      - RP_NAME=openGym
      - PASSWORD_LOGIN=1
      - FIRST_USER_ADMIN=1
    volumes:
      - ./data:/data
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:3000/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  web:
    image: ghcr.io/duartesantos8/opengym-web:1.4.1
    restart: unless-stopped
    depends_on:
      - api
    ports:
      - "8080:80"
    environment:
      - NGINX_PORT=80
      - BACKEND=api
      - PORT=3000
      - RESOLVER=127.0.0.11
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:80/"]
      interval: 30s
      timeout: 5s
      retries: 3

Speichern mit Strg+O, Enter, den Editor schließen mit Strg+X. Was die Zeilen tun, die du kennen solltest:

  • image: …:1.4.1 holt genau diese Version statt latest. Bei fünf Releases in drei Wochen will ich selbst entscheiden, wann ein Update kommt, und vorher die Notes lesen.
  • ORIGIN ist die Adresse, die du später in den Browser tippst, mit http:// und Port. Das Handbuch ist hier streng: Die API vergleicht bei jeder Anmeldung, von welcher Adresse der Browser kommt, und lehnt alles andere ab. Deshalb muss hier exakt das stehen, was in der Adresszeile steht, ohne Schrägstrich am Ende.
  • RP_ID ist der Hostname für Passkeys, ohne http:// und ohne Port. Über HTTP spielt er keine Rolle, aber die beiden Werte gehören zusammen, und so passt er, falls du später auf HTTPS umziehst und beide ohnehin änderst.
  • PASSWORD_LOGIN=1 schaltet die Passwort-Anmeldung frei, aus dem Abschnitt oben. FIRST_USER_ADMIN=1 macht das erste Profil, das auf dieser Installation angelegt wird, zum Admin. Beides ist in der .env.example dokumentiert und standardmäßig aus.
  • ./data:/data ist der Ordner mit allem: Profile, Passkeys, Trainings, der Schlüssel für die Sitzungen. Das Handbuch schreibt daneben in Großbuchstaben „BACK THIS UP“, und ich würde das nicht nur wegen meines Ticks unterschreiben.
  • TRUST_PROXY=1 steht so in der offiziellen Datei: Die API ist nur über den Web-Container erreichbar, der die Absenderadresse weiterreicht, damit die Bremse gegen Passwort-Raten pro Gerät zählt und nicht alle in einen Topf wirft.
  • Die beiden healthcheck-Blöcke sind meine Zugabe. Die Images bringen genau diese Prüfung schon mit, aber mit einem Intervall von fünf Minuten; so lange steht in docker compose ps ein „starting“, ohne dass etwas fehlt. Mit 30 Sekunden siehst du schneller, ob es läuft. Die Prüfbefehle sind dieselben wie im Dockerfile des Projekts.

Ob die Datei fehlerfrei ist, sagt dir Docker, bevor irgendetwas startet. Keine Ausgabe heißt: alles in Ordnung.

docker compose config --quiet

Schritt 2: Starten und anklopfen

docker compose up -d
docker compose ps

Beim ersten Mal lädt Docker die beiden Images, das dauert je nach Leitung eine halbe Minute bis eine Minute. Nach dem Start braucht openGym etwa eine halbe Minute, bis es auf Port 8080 antwortet. Danach zeigt ps zwei Zeilen, opengym-api-1 und opengym-web-1, beide mit Up; nach spätestens einer weiteren halben Minute steht hinter beiden (healthy). Dann der eigentliche Klopftest, die API hat dafür eine Adresse:

curl -s http://127.0.0.1:8080/api/health

Die Antwort ist eine JSON-Zeile:

{"ok":true,"users":0,"writable":true}

Das heißt: Der Web-Container nimmt Anfragen an, reicht /api an den API-Container weiter, und der antwortet. Alle drei Beteiligten aus dem Flussdiagramm reden miteinander. Noch ein Blick, ob die API die richtigen Adressen geladen hat; laut Handbuch ist das die erste Frage bei jedem Anmeldeproblem:

docker compose logs api | grep 'gym-api on'
api-1  | gym-api on :3000 (rpID=192.168.178.50, origin=http://192.168.178.50:8080)

Steht dort noch localhost, obwohl du die Datei geändert hast, läuft der Container mit der alten Umgebung. docker compose restart liest die Datei nicht neu ein, nur docker compose up -d tut das. Das steht so im Handbuch, und ich glaube, diesen Satz hat sich dort jemand hart erarbeitet.

Schritt 3: Das erste Profil anlegen

Jetzt der Browser, am besten gleich der auf dem Handy, weil du dort später trainierst. Die Adresse ist genau die aus ORIGIN; über localhost oder einen anderen Hostnamen würde die Anmeldung abgelehnt.

Stand openGym 1.4.1: http://192.168.178.50:8080 öffnen. Über http:// plus IP steht auf der Startseite über den Knöpfen der Hinweis „Dieser Browser unterstützt keine Passkeys. Melde dich stattdessen mit Name und Passwort an.“ Auf der Startseite Neues Profil erstellen wählen und dort die Variante mit Passwort statt Passkey nehmen; sie erscheint nur, weil PASSWORD_LOGIN=1 gesetzt ist. Nach Neues Profil erstellen öffnet sich direkt das Formular „Erstelle dein Profil“ mit den Feldern Dein Name, E-Mail (optional), Passwort und Passwort wiederholen. Profilname eintragen (im Beispiel sakis) und ein Passwort mit mindestens 10 Zeichen, besser eine lange Passphrase aus dem Passwort-Manager. Danach landest du auf dem Reiter Start; die Einstellungen liegen hinter dem Zahnrad oben rechts.

Die Startseite von openGym mit den drei Knöpfen Mit Passwort anmelden, Neues Profil erstellen und Ohne Konto fortfahren.
Hier klicken
Das Formular Erstelle dein Profil mit dem Namen sakis, zwei ausgefüllten Passwortfeldern und dem Knopf Profil erstellen.
(1) Profilname: sakis; (2) Mindestens 10 Zeichen; (3) Hier klicken
Die Startseite von openGym nach dem Anlegen des Profils sakis, mit Wochenplan, Reiter Start unten und dem Zahnrad oben rechts.
(1) Reiter Start; (2) Einstellungen, Zahnrad

Erfolgskontrolle: Unter Einstellungen und dann Account steht deine Account-ID, und darunter gibt es den Link Admin dashboard. Der Link ist der Beweis, dass FIRST_USER_ADMIN gegriffen hat. Auf dem Server liegt jetzt in ~/opengym/data unter anderem db.json mit dem Profil und die Datei secret, mit der jede Sitzung unterschrieben wird. Das erste eigene Profil auf dem eigenen Server, in Dateien, die du mit cat lesen kannst: kleiner Moment, darf man kurz genießen.

Zwei Handgriffe direkt danach, beide in den Einstellungen:

Stand openGym 1.4.1: Einstellungen, dann Einheiten & Sprache, dort Sprache auf Deutsch stellen; die Oberfläche wechselt sofort. Die Oberfläche folgt beim ersten Aufruf der Sprache des Browsers; steht sie schon auf Deutsch, entfällt dieser Handgriff. Wer Daten mitbringt: unter Einstellungen, dann Daten & Backup, der Knopf Aus anderer App importieren für eine CSV-Datei aus Hevy, Strong oder FitNotes, oder Aus Hevy importieren mit einem Hevy-Pro-API-Schlüssel. Die Datei wählst du direkt im Browser aus, der Import läuft in wenigen Sekunden, und mehrere Trainings am selben Tag bleiben seit 1.4.1 getrennt.

Die Sprachauswahl unter Einheiten & Sprache mit der Liste der Sprachen, darin Deutsch mit dem Hinweis, dass Übungsanleitungen noch auf Englisch bleiben.
Deutsch wählen
Die Seite Daten & Backup mit den Knöpfen Backup exportieren, Backup importieren, Aus anderer App importieren und Aus Hevy importieren.
(1) CSV aus Hevy, Strong oder FitNotes; (2) Mit Hevy-Pro-API-Schlüssel

Danach die App auf den Startbildschirm legen (Android: Menü, „Zum Startbildschirm hinzufügen“; iPhone: Teilen, „Zum Home-Bildschirm“). Für Android gibt es zusätzlich eine eigene App als APK auf der Release-Seite, die sich seit 1.3.11 auch über eine http://-Adresse im Heimnetz mit deinem Server koppelt. Eine iPhone-App gibt es nicht im App Store, dafür ist die CLA von oben gedacht; die Web-App auf dem Home-Bildschirm tut es bis dahin.

Was am Ende eingestellt ist

Benutzer: ein Profil mit Passwort, das Admin ist. Und eine Tür, die noch offen steht: Standardmäßig darf jeder, der die Adresse erreicht, ein eigenes Profil anlegen. Im Heimnetz ist das deine Familie, trotzdem würde ich sie jetzt schließen, wo dein Profil existiert. Dafür in ~/opengym/compose.yaml im Abschnitt environment des Dienstes api diese zwei Zeilen ergänzen (Editor wie in Schritt 1: nano ~/opengym/compose.yaml, Strg+O, Enter, Strg+X):

      - INVITE_ONLY=1
      - ALLOW_GUEST=0

Übernommen wird die Änderung so, und restart würde hier wieder nichts bringen:

cd ~/opengym && docker compose up -d

Neue Profile brauchen dann einen Einladungscode, den du im Admin-Dashboard erzeugst, und der Knopf „Continue without account“ verschwindet. Das Handbuch sagt ausdrücklich: erst das eigene Profil anlegen, dann INVITE_ONLY einschalten, sonst könnte niemand den ersten Code erzeugen. Erfolgskontrolle: Die Startseite im privaten Browserfenster zeigt den Gast-Knopf nicht mehr. Die übrigen Einstellungen:

  • Daten: alles in ~/opengym/data, unverschlüsselt, pro Profil eine JSON-Datei. Backup laut Handbuch: tar czf opengym-backup-$(date +%F).tar.gz data/ im Ordner ~/opengym; zurückspielen heißt entpacken. Verlierst du die Datei secret, sind alle Geräte abgemeldet, sonst passiert nichts. Jeder Benutzer kann sich außerdem unter Einstellungen, „Daten & Backup“, ein eigenes JSON exportieren.
  • Unverschlüsselt: Port 8080 ist im ganzen Heimnetz offen, Docker trägt ihn an deiner Firewall vorbei ein (Docker UFW: Ports trotz Firewall offen), und Passwort wie Sitzungs-Cookie laufen über HTTP im Klartext durchs Netz. Im eigenen WLAN ist das vertretbar, nach außen nie. Von unterwegs kommst du per VPN heran, etwa mit wg-easy. Willst du Passkeys und Push-Benachrichtigungen (Pausentimer, Erinnerung an den Trainingstag), brauchst du HTTPS mit eigenem Hostnamen, zum Beispiel über den Nginx Proxy Manager: Dann zeigen RP_ID und ORIGIN auf diesen Namen (gym.example.com und https://gym.example.com), die Port-Zeile wird "127.0.0.1:8080:80", und das Handbuch warnt zu Recht: Den Hostnamen legt man fest, bevor andere Profile anlegen, weil ein Wechsel alle Passkeys ungültig macht.
  • Updates: keine automatischen. Du änderst die Versionsnummer in beiden image-Zeilen, dann docker compose pull && docker compose up -d. Bei diesem Tempo alle zwei bis drei Wochen, und vorher die Release Notes lesen; die Daten in ./data bleiben dabei unangetastet.
  • KI-Coach: aus, und das bleibt so, bis ein Admin ihn in den Einstellungen anschaltet und einen eigenen API-Schlüssel hinterlegt. Ohne das ist die Installation laut .env.example „byte-for-byte“ dieselbe App wie vor der Funktion. Telemetrie gibt es keine; das verspricht die FAQ, und das Flussdiagramm oben hat keinen Pfeil nach draußen.

Wo es klemmt

  • „verification failed“ oder abgelehnte Anmeldung: Im Handbuch die häufigste Support-Frage, und die Werte sind meist fast richtig. Erst docker compose logs api | grep 'gym-api on' aus Schritt 2, dann vergleichen: RP_ID ohne Schema und Port, ORIGIN mit Schema, ohne Schrägstrich am Ende, und beides exakt wie in der Adresszeile. Geändert wird mit docker compose up -d, nicht mit restart.
  • Kein Passkey-Dialog auf dem Handy: kein Fehler, sondern der Abschnitt oben. Über http:// plus IP gibt es keine Passkeys; Passwort oder HTTPS.
  • Android-App meldet „Failed to fetch“ beim Koppeln: Issue #329, das meistkommentierte Problem der letzten Wochen. Die Ursache war in allen aufgeklärten Fällen ein Reverse Proxy, dessen Middleware die OPTIONS-Anfrage des Handys blockte; ein Betroffener schrieb selbst, der Fehler lag in seiner Traefik-Konfiguration. Seit 1.3.10 sagt die App genauer, was schiefging. Ohne Proxy im Heimnetz tritt es nicht auf.
  • Übungsbilder fehlen: Laut Troubleshooting-Tabelle ein Image älter als 1.4.0. Mit der Compose-Datei von oben kann das nicht passieren; nach einem Downgrade schon.
  • Ein zweiter Reverse Proxy vor dem Web-Container: Dann kommen für die API alle Besucher von derselben Adresse, und die Bremse gegen Passwort-Raten zählt alle zusammen; ein Nachbar mit Tippfehlern kann die Passwort-Anmeldung für alle bis zu 15 Minuten pausieren. Passkeys bleiben davon unberührt. Das Handbuch beschreibt das offen, eine Lösung außerhalb von Cloudflare gibt es noch nicht.

Für wen es sich lohnt

Wenn du heute für Hevy oder Strong zahlst und ein Homelab hast, würde ich openGym ausprobieren, und zwar mit Import deiner Historie, damit der Vergleich fair ist. Der Funktionsumfang reicht für Krafttraining nach Plan, die App fühlt sich auf dem Handy an wie eine App, und was du eintippst, liegt in Dateien, die du lesen kannst. Wer nur Laufen oder Radfahren trackt, ist hier falsch; dafür gibt es FitTrackee und Co., openGym ist für das Studio und die Hantelbank im Keller gemacht.

Ehrlich bleibt der Reifegrad: drei Monate alt, ein Entwickler, 132 offene Issues, Releases im Wochentakt. Dass etwas, das so schnell wächst, auch mal stolpert, gehört dazu; das Backup aus einem tar-Befehl macht den Stolperer verschmerzbar. Und die Lizenzfrage mit der CLA würde ich in einem Jahr noch einmal nachlesen. Bis dahin: läuft, kostet nichts, und „nicht jetzt, nicht später“ ist ein Satz, an dem man ein Projekt messen kann.

Mit welcher App zählst du heute deine Sätze, Hevy, Strong, eine Tabelle oder noch das Notizbuch? Schreib's in die Kommentare, und wenn du openGym schon laufen hast, würde mich interessieren, ob dein Import sauber durchgelaufen ist.

Wenn der Umzug der Fotos von Google zu Immich dich damals angesprochen hat, ist das hier das gleiche Prinzip mit Hanteln; der Beitrag zum Google-Fotos-Umzug zeigt, wie so ein Auszug ohne Datenverlust läuft.

Quellen