openGym : son suivi de musculation auto-hébergé avec Docker
Installer openGym avec Docker Compose ou docker run, le passer en HTTPS pour les passkeys, l'ouvrir à plusieurs personnes et le sauvegarder sans rien perdre.

Trois ans de séances dans une application de musculation, et un matin l'écran affiche : « Passez à Premium pour voir votre historique complet. » Les données sont là, sur le serveur de quelqu'un d'autre, et l'export coûte un abonnement. C'est exactement le scénario qu'openGym veut rendre impossible.
openGym est une application open source de suivi de musculation et de poids corporel, à héberger sur son propre serveur. On y planifie ses routines, on suit des séances guidées avec minuteur de repos, on enregistre chaque série, et l'application calcule la progression, les records et les muscles travaillés. Tout tient dans deux conteneurs Docker et un dossier de fichiers JSON.
Le projet
Le dépôt DuarteSantos8/openGym a été créé en juillet 2026 et compte déjà plus de 12 000 étoiles et 1 500 forks en octobre. Il est sous licence AGPL-3.0, écrit en JavaScript : React 19 et Vite pour l'interface, un serveur Node.js sans framework pour l'API. La version 1.4.1 est sortie le 10 octobre, au lendemain de la 1.4.0, la plus grosse mise à jour du projet.
Le mainteneur assume un point qui fait débat : une grande partie du code est rédigée avec Claude Code, l'agent de programmation d'Anthropic. Le dépôt contient un CLAUDE.md, les tests couvrent la logique d'entraînement, et chaque version est testée sur de vrais téléphones avant publication. L'application elle-même n'appelle aucune IA dans une installation par défaut.
Ce que fait l'application
La liste des fonctions est longue. L'essentiel :
| Domaine | Fonctions |
|---|---|
| Planification | Une routine par jour de la semaine ou en rotation (A, B, C…), 5 632 exercices avec animations, quatre programmes de départ (Push/Pull/Legs, Upper/Lower, Full Body, 5×5) |
| Séance | Charges préremplies depuis la dernière fois, minuteur de repos, supersets, séries d'échauffement, dégressives, rest-pause, RIR ou RPE, calcul des disques sur la barre |
| Progression | Règles linéaires, double ou triple progression, Greyskull LP, deload automatique en cas de stagnation, 1RM estimé, records |
| Suivi | Carte des muscles travaillés, en récupération ou délaissés, courbe de poids, photos de progression |
| Données | Import depuis FitNotes, Strong, Hevy et Apple Health, export JSON complet |
| Comptes | Passkeys, synchronisation entre appareils, tableau d'administration, 19 langues dont le français |
Une démo tourne dans le navigateur sur le site du projet, avec des données d'exemple. Elle permet de juger l'interface avant d'installer quoi que ce soit.
L'architecture
openGym est volontairement simple :
Le téléphone ou le navigateur parle en HTTPS au conteneur web, un nginx qui sert l'interface React et les animations d'exercices. Ce conteneur relaie tout ce qui commence par /api vers le conteneur api, en Node.js, qui gère les passkeys, la synchronisation et les notifications. L'API écrit tout dans le dossier ./data, en fichiers JSON.
Pas de base de données. Chaque profil est un fichier state-<id>.json, les comptes et les passkeys sont dans db.json. Le conteneur web sert l'interface et relaie /api vers l'API, pour que tout soit servi sur la même origine, condition imposée par les passkeys.
Installer avec Docker Compose
C'est la méthode officielle. Il faut Docker avec le plugin Compose, rien d'autre : pas de Node.js sur la machine.
git clone https://github.com/DuarteSantos8/openGym
cd openGym
cp .env.example .env
docker compose pull
docker compose up -ddocker compose pull récupère les images précompilées sur ghcr.io, pour amd64 et arm64, donc aussi pour un Raspberry Pi récent. Pour compiler depuis les sources, on saute le pull et on lance docker compose up -d --build.
L'application répond sur http://localhost:8080. On vérifie que l'API est en vie :
docker compose ps
curl http://localhost:8080/api/healthSi les bases de Compose vous manquent, le guide Docker Compose explique les services, volumes et réseaux utilisés ici.
Un compose minimal, avec des versions figées
Le docker-compose.yml du dépôt contient aussi les instructions de build et le volume du coach IA. Pour un serveur où l'on veut seulement faire tourner l'application, je préfère un fichier court, sans cloner le dépôt, avec des versions explicites plutôt que latest :
name: opengym
services:
api:
image: ghcr.io/duartesantos8/opengym-api:1.4.1
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.1
restart: unless-stopped
depends_on:
- api
ports:
- "127.0.0.1:8080:80"
environment:
- BACKEND=api
- PORT=3000RP_ID=localhost
ORIGIN=http://localhost:8080
RP_NAME=openGym
FIRST_USER_ADMIN=1Le port est publié sur 127.0.0.1 seulement : l'application sera exposée par un reverse proxy, pas directement. TRUST_PROXY=1 est sans risque ici, parce que l'API n'est joignable qu'à travers le conteneur web, qui écrase les en-têtes d'adresse du client.
Les images sont publiées avec des étiquettes 1.4.1, 1.4 et edge. Figer la version évite une mise à jour surprise ; on la change à la main après avoir lu le changelog.
Sans Compose : docker run
Compose n'est pas obligatoire. Les deux conteneurs ont seulement besoin de partager un réseau Docker pour que web trouve l'API par son nom :
docker network create opengym
docker run -d --name opengym-api \
--network opengym \
--restart unless-stopped \
-e PORT=3000 \
-e DATA_DIR=/data \
-e TRUST_PROXY=1 \
-e RP_ID=localhost \
-e ORIGIN=http://localhost:8080 \
-e FIRST_USER_ADMIN=1 \
-v "$PWD/data:/data" \
ghcr.io/duartesantos8/opengym-api:1.4.1
docker run -d --name opengym-web \
--network opengym \
--restart unless-stopped \
-e BACKEND=opengym-api \
-e PORT=3000 \
-p 127.0.0.1:8080:80 \
ghcr.io/duartesantos8/opengym-web:1.4.1La seule différence avec Compose est BACKEND=opengym-api : nginx relaie /api vers le conteneur qui porte ce nom. Le réseau doit être un réseau créé par l'utilisateur, pas le réseau bridge par défaut, sinon la résolution des noms ne fonctionne pas.
Ça marche, mais la configuration vit alors dans l'historique du shell. Pour un service qu'on garde des années, un fichier Compose versionné reste plus simple à relire et à mettre à jour.
Le point qui bloque tout le monde : les passkeys
openGym connecte les utilisateurs par passkey (Face ID, empreinte, gestionnaire de mots de passe). Les navigateurs imposent deux règles :
- une passkey est liée à un nom d'hôte exact,
RP_ID; - elle ne fonctionne qu'en HTTPS, avec une exception :
http://localhost.
Conséquence : http://localhost:8080 marche sur la machine qui fait tourner Docker, mais http://192.168.1.20:8080 depuis le téléphone n'affichera jamais l'invite de passkey. Pour utiliser l'application à la salle, il faut un vrai domaine en HTTPS. C'est la même contrainte que pour Vaultwarden, qui refuse aussi le HTTP simple.
Deux solutions de repli existent : le mode invité, qui garde les données dans le navigateur seulement, et la connexion par mot de passe, désactivée par défaut (PASSWORD_LOGIN=1).
Passer en HTTPS
N'importe quel reverse proxy convient. Avec Caddy, le certificat Let's Encrypt est automatique :
gym.mondomaine.fr {
reverse_proxy localhost:8080
}Puis on déclare le domaine dans .env :
RP_ID=gym.mondomaine.fr
ORIGIN=https://gym.mondomaine.fr
RP_NAME=openGym
FIRST_USER_ADMIN=1docker compose up -dAttention, docker compose restart ne relit pas le .env. Il faut docker compose up -d, qui recrée les conteneurs. Pour vérifier les valeurs réellement chargées, l'API les affiche au démarrage :
docker compose logs api | grep 'gym-api on'RP_ID contient le nom d'hôte seul : pas de https://, pas de port, pas de barre finale. ORIGIN contient l'URL complète. Une erreur sur l'un des deux donne un « verification failed » à la connexion, la question la plus fréquente sur le dépôt.
Choisissez le domaine avant d'inviter qui que ce soit : changer RP_ID plus tard invalide toutes les passkeys existantes.
Pour un accès depuis l'extérieur sans ouvrir de port sur la box, Cloudflare Tunnel ou Tailscale fonctionnent aussi : il suffit de router le domaine vers http://<hôte>:8080. Derrière Cloudflare, ajoutez CF_CONNECTING_IP=$$http_cf_connecting_ip à l'environnement du conteneur web pour que le journal d'activité enregistre la vraie adresse des visiteurs.
Si votre proxy limite la taille des requêtes, comme nginx avec 1 Mo par défaut, autorisez au moins 5 Mo sur /api/ : l'application synchronise tout l'historique en une requête. Les vidéos d'exercices personnalisés demandent plus, jusqu'à 48 Mo par défaut.
Ouvrir l'instance à plusieurs personnes
Par défaut, l'inscription est ouverte à quiconque connaît l'adresse. Pour une instance familiale ou entre amis, trois réglages suffisent :
FIRST_USER_ADMIN=1 # le premier profil créé devient administrateur
INVITE_ONLY=1 # inscription sur code d'invitation uniquement
ALLOW_GUEST=0 # pas de bouton « continuer sans compte »L'ordre compte : créez votre profil avant de partager l'adresse, sinon le premier visiteur devient administrateur. Ensuite, le tableau d'administration génère les codes d'invitation, désactive des comptes et affiche le journal des connexions. Les adresses IP n'y sont pas enregistrées, sauf si on le demande avec AUDIT_IP.
Sauvegarder et mettre à jour
Tout est dans ./data. La sauvegarde officielle tient en une ligne :
tar czf opengym-$(date +%F).tar.gz data/L'archive contient les profils, les passkeys publiques, les historiques et le fichier secret qui signe les sessions. Perdre secret, c'est déconnecter tout le monde. Les photos et vidéos d'exercices personnalisés sont dans data/uploads/ et forment l'essentiel du volume. Pour une copie chiffrée hors du serveur, j'enverrais ce dossier avec Restic et ses sauvegardes chiffrées plutôt que de laisser traîner des archives en clair.
La mise à jour, avec le fichier minimal ci-dessus :
# changer 1.4.1 par la nouvelle version dans docker-compose.yml, puis
docker compose pull
docker compose up -dLe dossier data n'est pas touché, et les animations d'exercices arrivent avec la nouvelle image. Avec le dépôt cloné, on ajoute un git pull avant, pour récupérer les changements de configuration.
Les options à activer, ou pas
Deux extensions sont désactivées par défaut :
- Le coach IA propose une semaine de routines puis des ajustements selon ce que vous avez enregistré. Il tourne sur votre serveur avec votre propre clé : Anthropic, OpenAI, Gemini, ou un serveur compatible OpenAI comme Ollama. Chaque modification doit être validée.
- Un serveur MCP permet à un assistant comme Claude Desktop d'interroger l'historique d'entraînement, en lecture seule. Il ne fait pas partie de l'image Docker.
Pour un usage strictement personnel, je laisse les deux désactivés. L'application n'en a pas besoin pour faire son travail.
Pour aller plus loin, le dépôt fournit aussi des manifestes Kubernetes et un chart Helm, et une application Android signée, distribuée hors du Play Store, qui peut fonctionner seule ou se connecter à votre serveur.
Ce qu'il faut savoir sur la licence
Le code est sous AGPL-3.0 : libre à l'usage, à la modification et à l'hébergement. Si vous proposez une version modifiée comme service en ligne, vous devez publier ses sources. Les animations d'exercices, en revanche, ne sont pas couvertes : elles appartiennent à Gym visual et sont licenciées pour openGym uniquement, en 180 px sur les serveurs auto-hébergés. Un fork non commercial peut les garder telles quelles avec la mention ; un projet commercial doit acheter sa propre licence.
openGym coche ce que j'attends d'une application auto-hébergée : deux conteneurs, aucune base de données à administrer, un dossier à sauvegarder, et une interface qui n'a rien à envier aux applications payantes. La seule vraie marche est le HTTPS, imposé par les passkeys. Une fois le domaine en place, c'est un service qu'on oublie, jusqu'au jour où l'on veut retrouver ses charges d'il y a trois ans et qu'elles sont toujours là, chez soi.