Auto-héberger openGym avec Docker Compose
Déployez openGym sur un VPS avec Docker Compose : tag Git figé, TLS avant la première passkey, emplacement des fichiers JSON et serveur MCP en lecture seule.
Ce que vous obtenez en auto-hébergeant openGym
Vous auto-hébergez openGym en clonant le dépôt, en modifiant deux lignes dans .env et en exécutant docker compose up -d --build derrière un reverse proxy qui assure la terminaison TLS (transport layer security). openGym est un outil de suivi de la pratique sportive et du poids corporel : plans hebdomadaires, séances guidées, journalisation de chaque série et évolution du poids. Il est distribué sous licence AGPL-3.0 et stocke toutes les données dans des fichiers JSON en clair sur votre disque. Aucun serveur de base de données n’est donc nécessaire.
La stack comprend deux conteneurs persistants : un conteneur nginx qui sert le build React et un conteneur Node qui héberge l’API. Elle comprend aussi une tâche exécutée une seule fois, qui télécharge environ 140 MB d’images et de GIF d’exercices lors du premier démarrage.
Le README du projet laisse entendre deux points sans les expliquer à quelqu’un qui déploie l’application sur un serveur public. La connexion par passkey est liée à un nom d’hôte. Le domaine et son certificat doivent donc exister avant la première connexion, et non après. Le serveur MCP facultatif est en lecture seule et s’exécute sur la machine où votre client IA est lancé, pas dans la stack. Cela modifie les opérations à effectuer lorsque les données se trouvent sur un VPS.
openGym est encore récent. La première release taguée, v1.0.0, est datée du 20 juillet 2026, et v1.2.7 est sortie le 18 août 2026. Treize tags en environ un mois indiquent que l’application évolue encore rapidement. Utilisez donc un release tag plutôt que de construire la version présente par défaut sur la branche par défaut.
Planifiez le domaine avant la première connexion
Les passkeys servent à vous connecter à openGym. Une passkey est liée à un relying party ID (RP ID), c’est-à-dire au domaine sur lequel l’identifiant a été créé. Les navigateurs ne créent des passkeys qu’en HTTPS. La seule exception est localhost.
Cela entraîne un problème que l’on rencontre sur téléphone. Ouvrez http://203.0.113.10:8080 depuis un autre appareil : aucune invite de création de passkey n’apparaît, car le navigateur refuse de créer un identifiant sur une origine HTTP simple ou sur une adresse IP seule. Les notes de dépannage du projet indiquent la même chose : si aucune invite n’apparaît, vous utilisez http:// ou une adresse IP.
Le RP ID est en outre intégré à chaque identifiant que vos utilisateurs ont déjà enregistré. Si vous modifiez RP_ID par la suite, les passkeys enregistrées sur leurs appareils ne correspondent plus et personne ne peut se connecter. Définissez d’abord le nom d’hôte, faites pointer le DNS vers le VPS et configurez le certificat avant que quiconque sélectionne Create profile.
Déployer openGym avec Docker Compose
Le fichier Compose monte ./data et ./media en bind mount, relativement à son propre emplacement. Le répertoire dans lequel vous clonez le projet constitue donc votre base de données. Placez-le à un emplacement durable.
sudo install -d -o "$USER" -g "$USER" /opt/opengym
git clone https://gitea.com/DuarteSantos/openGym /opt/opengym
cd /opt/opengym
cp .env.example .envLe README affiche encore une URL de clonage github.com. Cette adresse ne répond plus. Le dépôt Gitea indiqué plus haut est désormais l’emplacement actif du projet.
Modifiez .env. Trois lignes sont importantes sur un VPS.
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080RP_ID correspond au nom d’hôte seul. ORIGIN correspond à l’URL complète, schéma inclus. Les deux valeurs doivent correspondre exactement à ce qui apparaît dans la barre d’adresse. Sinon, la connexion échoue avec verification failed. La valeur WEB_PORT est expliquée dans la section consacrée au maintien du port 8080 privé.
docker compose up -d --build
docker compose ps
docker compose logs mediadocker compose ps doit afficher web et api à l’état running, et media à l’état exited avec le code 0. Cet arrêt est normal : le job media a restart: "no", car son travail consiste en un téléchargement unique. Son journal se termine par une ligne commençant par ✓ Exercise media ready, et ls media/img | wc -l doit afficher quelques centaines plutôt que 0. Un répertoire vide indique que le téléchargement a échoué. L’application affiche alors des cartes d’exercices sans images.
L’option --build est obligatoire ici. Le fichier Compose référence des images préconstruites sur ghcr.io, qui ne sont plus publiées. docker compose pull échoue donc avec denied ou manifest unknown. Les deux services sont alors construits à partir du code source que vous venez de cloner. Ils contiennent tous les deux une section build prévue à cet effet. Si Compose est nouveau pour vous, commencez par Docker Compose sur un VPS, puis revenez ici.
Figez la version, car ce projet est encore récent
Comme cet espace de noms du registre a disparu, il ne reste plus de tag d’image à figer. Vous devez donc figer le checkout présent sur le disque, car c’est lui qui détermine la version de l’application placée dans le conteneur.
cd /opt/opengym
git fetch --tags
git checkout v1.2.7git status indique maintenant un HEAD détaché sur ce tag, ce qui convient sur un serveur. Rien ne change tant que vous ne sélectionnez pas un autre tag.
Indiquez ensuite à Compose de ne plus contacter le registre. Placez ceci dans docker-compose.override.yml, que Compose charge automatiquement et fusionne par-dessus le fichier suivi. Les clés scalaires sont remplacées par celles du fichier d’override. Vous n’avez donc pas besoin de modifier les fichiers dans git et git pull reste propre. Consultez la fusion d’un fichier d’override par Compose pour connaître toutes les règles de fusion.
services:
api:
pull_policy: build
web:
pull_policy: buildUne fois cette configuration en place, un docker compose up -d ultérieur construit l’image à partir des sources présentes au lieu d’échouer lors du pull. Vérifiez que la fusion a bien été appliquée, puis reconstruisez l’image sur le tag.
docker compose config | grep pull_policy
docker compose up -d --buildTerminer TLS avec un reverse proxy
Les conteneurs utilisent HTTP en clair. Un composant placé en amont doit gérer le certificat. Caddy est la solution la plus rapide, car il demande et renouvelle lui-même le certificat auprès de Let's Encrypt.
gym.example.com {
reverse_proxy 127.0.0.1:8080
}nginx, Traefik et Nginx Proxy Manager fonctionnent de la même manière. Il en va de même pour un Cloudflare Tunnel, documenté par le projet, qui ne nécessite l’ouverture d’aucun port entrant.
curl -sI https://gym.example.com | head -1Cette commande doit renvoyer HTTP/2 200 sans avertissement de certificat. Ouvrez ensuite le site dans un navigateur et cliquez sur Create profile. Si l’invite de clé d’accès apparaît, puis que la connexion signale verification failed, RP_ID ou ORIGIN ne correspond pas à l’URL affichée dans la barre d’adresse. Corrigez .env, puis exécutez de nouveau docker compose up -d pour recréer les conteneurs afin qu’ils lisent les nouvelles valeurs. Un docker compose restart ne recharge pas .env.
N’exposez pas le port 8080 sur Internet
Par défaut, le service web publie 8080 sur toutes les interfaces. L’application est donc accessible en HTTP sur votre IP publique, tandis que le proxy fournit HTTPS sur le même serveur. Une règle de pare-feu ne corrige pas ce problème. Docker publie un port avec une règle DNAT dans la table nat. Le trafic est ensuite traité dans la chaîne FORWARD, où les propres règles de Docker l’acceptent, tandis que les règles d’ufw se trouvent sur le chemin INPUT. sudo ufw deny 8080/tcp ne bloque donc rien.
La solution consiste à publier le port uniquement sur l’adresse loopback. Le fichier compose mappe "${WEB_PORT:-8080}:${NGINX_PORT:-80}". La valeur définie dans WEB_PORT est donc substituée à gauche de ce mapping, et la syntaxe courte de Docker accepte une paire ip:port à cet endroit. C’est pourquoi WEB_PORT=127.0.0.1:8080 fonctionne.
docker compose config
sudo ss -ltnp | grep 8080Dans la configuration fusionnée, sous ports du service web, vous devez voir host_ip: 127.0.0.1. ss doit afficher 127.0.0.1:8080 et non 0.0.0.0:8080. Depuis une autre machine, curl http://<your-vps-ip>:8080 doit maintenant être refusé ou expirer, tandis que le hostname HTTPS continue de fonctionner.
Fermer les inscriptions une fois votre profil créé
Les inscriptions sont ouvertes par défaut et le mode invité est activé. Sur un nom d’hôte public, toute personne qui trouve l’URL peut donc créer un profil sur votre serveur. Commencez par enregistrer votre propre profil, puis trouvez votre ID utilisateur : ls data/ affiche un fichier nommé state-<uid>.json pour chaque utilisateur, et cette <uid> correspond à la valeur dont vous avez besoin.
ADMIN_UIDS=<your-uid>
INVITE_ONLY=1
ALLOW_GUEST=0Exécutez à nouveau docker compose up -d. Settings affiche désormais un tableau de bord d’administration permettant de générer et de révoquer des codes d’invitation. Les personnes avec lesquelles vous vous entraînez peuvent ainsi s’inscrire, et personne d’autre. openGym ne connaît pas les fournisseurs d’identité externes. Ces codes d’invitation contrôlent donc uniquement cette application, et rien d’autre sur le serveur. Si vous préférez attribuer un compte unique à chaque personne pour l’ensemble des services que vous exécutez, placez Authentik devant avec un proxy d’authentification forward auth afin de contrôler l’accès au nom d’hôte avant même que la connexion par passkey d’openGym ne soit chargée.
Où résident les données et quelle sauvegarde les protège
Tout se trouve dans le répertoire ./data, monté dans le conteneur API à l’emplacement /data. Il existe quatre types de fichiers : db.json contient les profils et les identifiants passkey publics, state-<uid>.json contient les routines, les entraînements et le poids d’un utilisateur, secret est la clé du cookie de session, et vapid.json contient les clés de notifications push générées au premier démarrage.
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start apiArrêtez d’abord l’API, car tar copie les fichiers pendant que l’API peut être en train d’en modifier un. Un fichier JSON copié partiellement est restauré comme un fichier JSON corrompu. L’arrêt et le redémarrage prennent environ deux secondes. Copiez ensuite l’archive hors du serveur, car une archive stockée sur le VPS ne résiste pas à la perte du VPS. Excluez media/ de la sauvegarde : ce répertoire contient 140 MB d’images d’exercices que le job média télécharge à nouveau gratuitement.
La restauration consiste à extraire l’archive dans le même chemin sur un hôte qui sert le même domaine. Une passkey stockée sur votre téléphone est associée au RP ID sur lequel elle a été créée. Une restauration sur un nouveau nom d’hôte vous donne donc une base de données fonctionnelle, mais personne ne peut s’y connecter. Conservez le domaine ou prévoyez de réenregistrer chaque passkey. La même méthode s’applique à tout le reste de ce que vous exécutez, et la sauvegarde et la mise à niveau d’une stack Docker Compose décrit la procédure générale.
Le serveur MCP est en lecture seule et s’exécute sur votre machine
MCP (model context protocol) permet à un client comme Claude Desktop ou Cursor de communiquer avec un serveur d’outils local. openGym en fournit un dans mcp/. Il ne fait pas partie du fichier compose, ce n’est pas un conteneur et il n’écoute sur aucun port. Le client le démarre comme processus enfant et communique avec lui via stdio. C’est pourquoi le README indique qu’il ne quitte jamais votre machine.
Installez-le là où le client s’exécute, et non sur le serveur :
cd openGym/mcp
npm installAjoutez-le ensuite à claude_desktop_config.json :
{
"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 est facultatif pour une installation mono-utilisateur, lorsque le serveur détecte le seul profil trouvé. Il expose huit outils : list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm et muscle_balance. Ils effectuent tous des lectures. Aucun n’écrit de données. Un assistant peut donc répondre à la question de savoir ce que vous avez enregistré la semaine dernière, mais il ne peut pas enregistrer une série, modifier une routine ni supprimer quoi que ce soit.
Voici le point qu’un utilisateur de VPS doit résoudre. OPENGYM_DATA est un chemin de système de fichiers, et vos données se trouvent sur le VPS alors que votre client d’IA se trouve sur votre ordinateur portable. Deux options permettent de gérer cette situation correctement.
- Copiez les données localement et indiquez cette copie au serveur :
rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/, puis définissezOPENGYM_DATAsur~/opengym-data. Le serveur effectue uniquement des lectures, donc une copie n’entraîne aucune perte. Relancez rsync lorsque vous voulez obtenir des chiffres à jour. - Exécutez le serveur via SSH, avec
commanddéfini sursshetargsdéfini sur["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]. Node doit être installé sur le VPS et la commande de connexion ne doit rien afficher sur stdout, car stdout transporte le protocole.
Si cat data/db.json renvoie Permission denied, le conteneur d’API a créé ces fichiers en tant que root et votre compte ne peut pas les lire. Copiez-les avec sudo, ou modifiez leur propriétaire sur l’hôte. Pour les serveurs qui doivent écouter sur le réseau plutôt que via stdio, consultez exécuter des serveurs MCP sur un VPS.
openGym ou wger : lequel déployer ?
wger est l’option établie dans ce domaine et représente un logiciel beaucoup plus volumineux. Sa stack Compose exécute gunicorn pour servir une application Django, PostgreSQL, Redis et un worker Celery derrière nginx. En contrepartie, vous bénéficiez du suivi de la nutrition et des ingrédients, d’une API REST documentée, d’une vaste base de données d’exercices et de fonctions destinées aux coachs qui gèrent les plans d’autres personnes.
openGym se compose de 2 conteneurs, d’un dossier de fichiers JSON et d’aucun compte à administrer en dehors des passkeys. C’est toute la différence.
Déployez wger si vous voulez suivre votre alimentation en plus de votre entraînement ou si vous avez besoin d’une API sur laquelle développer. Déployez openGym si vous voulez une stack suffisamment petite pour être lue de bout en bout en un après-midi, avec une connexion sans mot de passe à compromettre. Le coût de ce choix est la maturité : au 19 August 2026, la première release d’openGym date d’un mois, tandis que wger possède des années de releases à son actif. Épinglez votre version, conservez les sauvegardes et lisez les release notes avant chaque mise à jour.
Si vous cherchez encore ce qui mérite une place sur le serveur, ce qui mérite d’être auto-hébergé en 2026 présente les compromis. Cette application s’intègre facilement aux côtés de Mealie pour les recettes ou d’Actual Budget pour les finances sur le même petit VPS.
Mise à jour sans rien perdre
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api
git fetch --tagsRécupérez la release voulue avec git checkout v<new>, puis exécutez docker compose up -d --build afin de reconstruire les conteneurs à partir de ce tag. La sauvegarde passe toujours en premier, car la restauration des fichiers JSON présents sur le disque se fait avec une seule commande tar et ne prend que quelques secondes.
FAQ
Pourquoi openGym n’affiche-t-il jamais de demande de passkey sur mon téléphone ?
Le navigateur refuse de créer un credential parce que vous utilisez http:// ou une adresse IP seule, comme http://192.168.1.20:8080. Les navigateurs n’autorisent les passkeys que sur des origines HTTPS, avec localhost comme seule exception. Placez openGym derrière un reverse proxy qui utilise un vrai certificat pour un vrai nom d’hôte, définissez RP_ID=gym.example.com et ORIGIN=https://gym.example.com dans .env, puis exécutez docker compose up -d pour que les conteneurs prennent en compte les nouvelles valeurs. Si la demande apparaît, mais que la connexion renvoie verification failed, ces deux valeurs ne correspondent pas exactement à l’URL affichée dans la barre d’adresse.
Où openGym stocke-t-il mes données, et comment les sauvegarder ?
Dans le répertoire ./data situé à côté du fichier compose, monté dans le conteneur API sous /data. Il contient db.json pour les profils et les credentials passkey publics, un fichier state-<uid>.json par utilisateur pour les entraînements et le poids, secret pour la clé du cookie de session et vapid.json pour les clés de notifications push. Sauvegardez-le avec docker compose stop api, puis tar czf ~/opengym-$(date +%F).tar.gz data/, puis docker compose start api, et copiez l’archive hors du serveur. Ignorez media/, qui contient 140 MB d’images d’exercices que le job media télécharge à nouveau automatiquement.
Claude peut-il lire l’historique de mes entraînements openGym ?
Oui, via le serveur MCP facultatif situé dans le répertoire mcp/, et uniquement en lecture. Il expose huit outils couvrant les routines, les programmes hebdomadaires, les entraînements enregistrés, le poids, l’estimation du one-rep max et l’équilibre musculaire. Aucun de ces outils n’écrit de données. Ce n’est pas un conteneur et il n’ouvre aucun port : votre client le démarre via stdio et lit directement les fichiers JSON à l’emplacement OPENGYM_DATA. Comme il s’agit d’un chemin de système de fichiers, exécuter openGym sur un VPS signifie soit synchroniser une copie de data/ vers la machine qui exécute le client, soit invoquer le serveur via ssh dans la configuration du client.
Dois-je auto-héberger openGym ou wger ?
Choisissez wger si vous voulez suivre l’alimentation et la nutrition en plus de vos entraînements, ou disposer d’une API REST documentée sur laquelle vous appuyer. Il utilise une stack plus importante : Django sous gunicorn, PostgreSQL, Redis et un worker Celery derrière nginx. Choisissez openGym si vous voulez deux conteneurs, des fichiers JSON lisibles avec cat et une connexion par passkey sans mot de passe à gérer. Au 19 août 2026, la première release taguée d’openGym date d’un mois. Consultez donc un tag git et sauvegardez data/ avant chaque mise à jour.