SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-21

Come installare openGym su un VPS con Docker Compose

Guida al self-hosting di openGym su VPS: usa un tag git fissato, configura TLS prima della prima passkey, individua i dati JSON e attiva il server MCP in sola lettura.

Cosa si ottiene con il self-hosting di openGym

Per eseguire openGym in self-hosting, clonate il repository, modificate due righe in .env ed eseguite docker compose up -d --build dietro un reverse proxy che termina TLS (Transport Layer Security). openGym è un tracker per palestra e peso corporeo: piani settimanali, allenamenti guidati, registrazione di ogni serie e andamento del peso nel tempo. Il software è distribuito con licenza AGPL-3.0 e salva tutti i dati in file JSON non cifrati sul disco, quindi non è necessario eseguire un server database.

Lo stack comprende due container a esecuzione prolungata: un container nginx che pubblica la build React e un container Node che ospita l'API. Include inoltre un job eseguito una sola volta, che al primo avvio scarica circa 140 MB di immagini e GIF degli esercizi.

Il README del progetto lascia intendere due aspetti senza esplicitarli per chi esegue il deployment su un server pubblico. L'accesso con passkey è associato a un hostname, quindi il dominio e il relativo certificato devono essere disponibili prima del primo accesso, non dopo. Inoltre, il server MCP opzionale è di sola lettura ed esegue sulla macchina in cui è in esecuzione il client AI, non all'interno dello stack. Questo modifica le operazioni necessarie quando i dati risiedono su un VPS.

openGym è un progetto recente. La prima release contrassegnata, v1.0.0, è datata 20 July 2026, mentre v1.2.7 è stata pubblicata il 18 August 2026. Tredici tag in circa un mese indicano che l'applicazione è ancora soggetta a modifiche frequenti. Eseguite quindi il checkout di un tag di release invece di compilare il codice presente nel ramo predefinito.

Pianificare il dominio prima del primo accesso

Le passkey sono il metodo usato per accedere a openGym. Una passkey è associata a un relying party ID (RP ID), cioè al dominio sul quale è stata creata la credenziale, e i browser creano passkey soltanto tramite HTTPS. L'unica eccezione è localhost.

Questo comporta un problema che si verifica spesso sugli smartphone. Aprite http://203.0.113.10:8080 da un altro dispositivo e non viene visualizzata alcuna richiesta di creare una passkey, perché il browser rifiuta di creare una credenziale su un'origine HTTP semplice o su un indirizzo IP senza dominio. Le note di troubleshooting del progetto confermano lo stesso comportamento: se la richiesta non viene visualizzata, state usando http:// oppure un indirizzo IP.

Il problema è ancora più grave perché l'RP ID è incorporato in ogni credenziale già registrata dagli utenti. Se in seguito cambiate RP_ID, le passkey memorizzate sui loro dispositivi non corrispondono più e nessuno può accedere. Decidete prima il nome host, configurate il DNS affinché punti al VPS e rendete operativo il certificato prima che qualcuno tocchi Create profile.

Distribuire openGym con Docker Compose

Il file Compose monta ./data e ./media come bind mount relativi alla propria posizione. La directory in cui clonate il progetto è quindi il database dell'applicazione. Sceglietene una persistente.

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

Il README contiene ancora un URL di clone github.com. Questo indirizzo non è più raggiungibile. Il repository Gitea indicato sopra è la sede attiva del progetto.

Modificate .env. Su un VPS sono importanti tre righe.

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

RP_ID è il nome host senza schema, mentre ORIGIN è l'URL completo, incluso lo schema. Devono corrispondere esattamente a quanto compare nella barra degli indirizzi. In caso contrario, l'accesso non riesce con verification failed. Il valore WEB_PORT è spiegato nella sezione dedicata a mantenere privata la porta 8080.

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

docker compose ps dovrebbe mostrare web e api in esecuzione e media terminato con codice 0. Questa terminazione è corretta: il job multimediale ha restart: "no" perché il suo lavoro consiste in un download eseguito una sola volta. Il relativo log termina con una riga che inizia con ✓ Exercise media ready e ls media/img | wc -l dovrebbe restituire alcune centinaia, non 0. Una directory vuota indica che il download non è riuscito. In questo caso l'applicazione visualizza le schede degli esercizi senza immagini.

Il flag --build è obbligatorio in questo caso. Il file Compose specifica immagini precompilate su ghcr.io che non sono più pubblicate. Di conseguenza docker compose pull non riesce con denied o manifest unknown e i due servizi vengono compilati dal codice sorgente appena clonato. Entrambi includono una sezione build proprio per questo motivo. Se Compose è una novità per voi, iniziate da Docker Compose su un VPS e poi tornate qui.

Blocca la versione, perché questo progetto è recente

Poiché quello spazio dei nomi del registry non è più disponibile, non esiste più alcun tag dell'immagine da bloccare. Devi invece bloccare il checkout presente sul disco, perché determina quale versione dell'applicazione finirà nel container.

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

git status ora segnala un HEAD scollegato su quel tag, che è il comportamento desiderato su un server. Nulla cambia finché non esegui il checkout di un altro tag.

Poi configura Compose in modo che non contatti più il registry. Inserisci questo contenuto in docker-compose.override.yml, che Compose carica automaticamente e applica sopra al file tracciato. Le chiavi scalari vengono sostituite dall'override, quindi non devi modificare nulla in git e git pull resta pulito. Consulta come Compose applica l'unione di un file di override per le regole complete di unione.

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

Con questa configurazione, un successivo docker compose up -d esegue la build a partire dal codice sorgente disponibile, invece di fallire durante il pull. Verifica che l'unione sia stata applicata, quindi esegui di nuovo la build sul tag.

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

Terminare TLS con un reverse proxy

I container comunicano tramite HTTP non cifrato. Un componente a monte deve gestire il certificato. Caddy è la soluzione più rapida, perché richiede e rinnova automaticamente il certificato tramite Let's Encrypt.

gym.example.com {
    reverse_proxy 127.0.0.1:8080
}

nginx, Traefik e Nginx Proxy Manager funzionano nello stesso modo. Lo stesso vale per Cloudflare Tunnel, documentato dal progetto, che non richiede l'apertura di alcuna porta in ingresso.

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

Il comando dovrebbe restituire HTTP/2 200 senza avvisi relativi al certificato. Ora apri il sito in un browser e seleziona Create profile. Se viene visualizzata la richiesta della passkey e il login restituisce verification failed, RP_ID oppure ORIGIN non corrisponde all'URL nella barra degli indirizzi. Correggi .env ed esegui di nuovo docker compose up -d: in questo modo i container vengono ricreati e leggono i nuovi valori. Un docker compose restart non ricarica .env.

Mantieni la porta 8080 fuori da Internet pubblico

Per impostazione predefinita, il servizio web pubblica 8080 su tutte le interfacce. L'applicazione è quindi raggiungibile tramite HTTP non cifrato sull'IP pubblico, mentre il proxy fornisce HTTPS sullo stesso server. Una regola del firewall non risolve il problema. Docker pubblica una porta con una regola DNAT nella tabella nat. Il traffico viene quindi gestito nella catena FORWARD, dove le regole di Docker lo accettano, mentre le regole di ufw si trovano nel percorso INPUT. sudo ufw deny 8080/tcp non blocca quindi nulla.

La soluzione consiste nel pubblicare la porta soltanto sull'indirizzo di loopback. Il file compose esegue il mapping di "${WEB_PORT:-8080}:${NGINX_PORT:-80}". Qualunque valore si imposti in WEB_PORT viene sostituito a sinistra di quel mapping, e la sintassi abbreviata di Docker accetta in quel punto una coppia ip:port. Per questo WEB_PORT=127.0.0.1:8080 funziona.

docker compose config
sudo ss -ltnp | grep 8080

Nella configurazione risultante, sotto ports del servizio web, deve comparire host_ip: 127.0.0.1. ss deve mostrare 127.0.0.1:8080 e non 0.0.0.0:8080. Da un altro computer, curl http://<your-vps-ip>:8080 dovrebbe ora essere rifiutato o andare in timeout, mentre il nome host HTTPS continua a funzionare.

Chiudere le registrazioni dopo aver creato il proprio profilo

Le registrazioni sono abilitate per impostazione predefinita e la modalità ospite è attiva. Su un hostname pubblico, chiunque trovi l'URL può creare un profilo sul server. Registrate prima il vostro profilo, quindi individuate il vostro ID utente: ls data/ elenca un file denominato state-<uid>.json per ogni utente e quel <uid> è il valore necessario.

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

Eseguite di nuovo docker compose up -d. In Settings è ora disponibile una dashboard Admin, dalla quale potete generare e revocare i codici di invito. In questo modo le persone con cui vi allenate possono registrarsi, mentre tutti gli altri vengono esclusi. openGym non supporta i provider di identità esterni, quindi questi codici di invito controllano soltanto questa applicazione e nient'altro sul server. Se invece preferite assegnare a ogni persona un unico account valido per tutti i servizi gestiti, configurare Authentik come proxy forward auth applica il controllo sull'hostname prima ancora che venga caricata la procedura di accesso con passkey di openGym.

Dove risiedono i dati e quale backup li protegge

Tutto si trova nella directory ./data, montata nel container API in /data. Esistono quattro tipi di file: db.json contiene i profili e le credenziali pubbliche delle passkey, state-<uid>.json contiene le routine, gli allenamenti e il peso corporeo di un utente, secret è la chiave del cookie di sessione e vapid.json contiene le chiavi per le notifiche push generate al primo avvio.

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

Arrestare prima l'API, perché tar copia i file mentre l'API potrebbe scriverne uno. Un file JSON copiato solo parzialmente viene ripristinato come file JSON danneggiato. L'arresto e il riavvio richiedono circa due secondi. Copiare quindi l'archivio fuori dal server, perché un archivio che si trova sul VPS non sopravvive alla perdita del VPS. Escludere media/ dal backup: contiene 140 MB di immagini degli esercizi che il processo multimediale scarica di nuovo gratuitamente.

Il ripristino consiste nell'estrarre l'archivio nello stesso percorso su un host che pubblica lo stesso dominio. Una passkey memorizzata sul telefono è associata al RP ID con cui è stata creata. Il ripristino su un nuovo hostname produce quindi un database funzionante, ma nessuno può effettuare l'accesso. Mantenere il dominio oppure pianificare una nuova registrazione di tutte le passkey. La stessa disciplina si applica a tutto ciò che si esegue; la guida backup e aggiornamento di uno stack Docker Compose descrive la procedura generale.

Il server MCP è di sola lettura e viene eseguito sul tuo computer

MCP (model context protocol) è il protocollo con cui un client come Claude Desktop o Cursor comunica con un server locale degli strumenti. openGym ne include uno in mcp/. Non fa parte del file compose, non è un container e non è in ascolto su alcuna porta. Il client lo avvia come processo figlio e comunica con lui tramite stdio. Per questo il README specifica che non lascia mai il tuo computer.

Installalo nel sistema in cui viene eseguito il client, non sul server:

cd openGym/mcp
npm install

Quindi aggiungilo a 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 è facoltativo in un'installazione per un singolo utente, perché il server rileva l'unico profilo disponibile. Espone otto strumenti: list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm e muscle_balance. Tutti eseguono operazioni di lettura. Nessuno modifica dati. Un assistente può quindi indicare cosa hai registrato la settimana scorsa, ma non può registrare una serie, modificare una routine o eliminare dati.

Un utente VPS deve risolvere il seguente problema. OPENGYM_DATA è un percorso del filesystem, ma i dati si trovano sul VPS mentre il client AI è sul laptop. Esistono due opzioni che riflettono correttamente questa situazione.

  1. Copia i dati sul laptop e configura il server per usare la copia: rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/, quindi imposta OPENGYM_DATA su ~/opengym-data. Il server esegue solo operazioni di lettura, quindi la copia non comporta alcuna perdita di dati. Esegui nuovamente rsync quando vuoi aggiornare i valori.
  2. Esegui il server tramite ssh, impostando command su ssh e args su ["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]. Node deve essere installato sul VPS e l'accesso deve non stampare nulla su stdout, perché stdout è il canale del protocollo.

Se cat data/db.json restituisce Permission denied, il container API ha creato quei file come root e il tuo account non può leggerli. Copiali con sudo oppure modifica il proprietario sull'host. Per i server che devono essere in ascolto sulla rete anziché tramite stdio, consulta eseguire server MCP su un VPS.

openGym o wger: quale dovresti usare?

wger è la soluzione consolidata in questo settore ed è un software molto più grande. Il suo stack Compose esegue gunicorn per servire un'applicazione Django, PostgreSQL, Redis e un worker Celery dietro nginx. In cambio offre il monitoraggio dell'alimentazione e degli ingredienti, un'API REST documentata, un ampio database di esercizi e funzionalità per i personal trainer che gestiscono i piani di altre persone.

openGym è composto da due container, una directory di file JSON e nessun account da amministrare oltre alle passkey. Questa è l'intera differenza.

Usa wger se vuoi monitorare l'alimentazione insieme all'allenamento o se ti serve un'API su cui sviluppare. Usa openGym se vuoi uno stack abbastanza piccolo da poter essere letto interamente in un pomeriggio e un accesso senza password da sottrarre. Il costo di questa scelta è la maturità: al 19 agosto 2026 la prima release di openGym risale a un mese fa, mentre wger ha alle spalle anni di release. Fissa la versione, conserva i backup e leggi le note di rilascio prima di ogni aggiornamento.

Se stai ancora decidendo cosa merita spazio sul server, cosa vale la pena usare in self-hosting nel 2026 illustra i compromessi; questa applicazione si affianca senza problemi a Mealie per le ricette o a Actual Budget per la gestione del denaro sullo stesso VPS di piccole dimensioni.

Aggiornare senza perdere dati

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

Seleziona la release desiderata con git checkout v<new>, quindi esegui docker compose up -d --build per ricreare i container a partire da quel tag. Il backup viene sempre prima, perché il ripristino dei file JSON presenti su disco richiede un solo comando tar e richiede pochi secondi.

FAQ

Perché openGym non mostra mai una richiesta di passkey sul telefono?

Il browser rifiuta di creare una credenziale perché stai usando http:// oppure un indirizzo IP semplice, come http://192.168.1.20:8080. I browser consentono le passkey soltanto su origini HTTPS, con localhost come unica eccezione. Pubblica openGym dietro un reverse proxy con un certificato valido per un hostname reale, imposta RP_ID=gym.example.com e ORIGIN=https://gym.example.com in .env, quindi esegui docker compose up -d affinché i container acquisiscano i nuovi valori. Se la richiesta viene visualizzata ma l'accesso restituisce verification failed, questi due valori non corrispondono esattamente all'URL mostrato nella barra degli indirizzi.

Dove archivia openGym i miei dati e come posso eseguirne il backup?

Nella directory ./data accanto al file compose, montata nel container API come /data. Contiene db.json per i profili e le credenziali pubbliche delle passkey, un file state-<uid>.json per ogni utente con gli allenamenti e il peso corporeo, secret per la chiave dei cookie di sessione e vapid.json per le chiavi delle notifiche push. Esegui il backup con docker compose stop api, poi tar czf ~/opengym-$(date +%F).tar.gz data/, quindi docker compose start api e copia l'archivio fuori dal server. Escludi media/, che contiene 140 MB di immagini degli esercizi; il job multimediale le scarica nuovamente in autonomia.

Claude può leggere la cronologia degli allenamenti di openGym?

Sì, tramite il server MCP opzionale nella directory mcp/, e soltanto in lettura. Espone otto strumenti per gestire routine, piani settimanali, allenamenti registrati, peso corporeo, stima del massimale per una ripetizione e bilanciamento muscolare; nessuno di questi modifica i dati. Non è un container e non apre alcuna porta: il client lo avvia tramite stdio e legge direttamente i file JSON in OPENGYM_DATA. Poiché si tratta di un percorso del filesystem, se openGym è eseguito su un VPS devi sincronizzare una copia di data/ sul computer che esegue il client oppure richiamare il server tramite ssh dalla configurazione del client.

Devo eseguire openGym in modalità self-hosted oppure wger?

Scegli wger se vuoi gestire alimentazione e nutrizione insieme al registro degli allenamenti oppure se ti serve una REST API documentata su cui sviluppare. Utilizza uno stack più ampio: Django tramite gunicorn, PostgreSQL, Redis e un worker Celery dietro nginx. Scegli openGym se vuoi due container, file JSON leggibili con cat e un accesso tramite passkey senza dover gestire password. Al 19 agosto 2026, la prima release contrassegnata di openGym ha un mese di vita; quindi esegui il checkout di un git tag ed esegui il backup di data/ prima di ogni aggiornamento.