Como hospedar o openGym com Docker Compose
Instale o openGym em um VPS com Docker Compose: use uma tag Git fixa, configure TLS antes da primeira passkey, localize os dados e ative o MCP somente leitura.
O que você obtém ao alojar o openGym por conta própria
Você aloja o openGym por conta própria clonando o repositório, editando duas linhas em .env e executando docker compose up -d --build atrás de um reverse proxy que termina o TLS (segurança da camada de transporte). O openGym é um gestor de treinos e peso corporal: planos semanais, treinos guiados, registo de todas as séries e evolução do peso ao longo do tempo. É licenciado sob AGPL-3.0 e armazena tudo em ficheiros JSON simples no disco, portanto não é necessário executar um servidor de base de dados.
A stack é composta por dois contentores de execução contínua: um contentor nginx que serve a build React e um contentor Node que aloja a API. Existe também um job executado uma vez, que descarrega cerca de 140 MB de imagens e GIFs de exercícios na primeira vez que o inicia.
O README do projeto dá a entender duas coisas, mas não as explica para quem vai implementar o serviço num servidor público. O início de sessão com passkey está associado a um hostname, portanto o domínio e o respetivo certificado têm de existir antes do primeiro início de sessão, e não depois. Além disso, o servidor MCP opcional é apenas de leitura e é executado na máquina onde o seu cliente de IA é executado, e não dentro da stack. Isto altera o que tem de fazer quando os dados estão num VPS.
O openGym é um projeto recente. A primeira release identificada, v1.0.0, está datada de 20 July 2026, e a v1.2.7 foi disponibilizada em 18 August 2026. Treze tags em cerca de um mês indicam que a aplicação ainda está a sofrer alterações. Por isso, use uma release tag em vez de criar a aplicação a partir do que estiver na branch predefinida.
Planeje o domínio antes do primeiro login
As passkeys são usadas para iniciar sessão no openGym. Uma passkey está vinculada a um relying party ID (RP ID), que é o domínio em que a credencial foi criada, e os navegadores só criam passkeys através de HTTPS. A única exceção é localhost.
Isto tem uma consequência que os utilizadores encontram no telemóvel. Abra http://203.0.113.10:8080 noutro dispositivo e não aparece qualquer pedido para criar uma passkey, porque o navegador recusa criar uma credencial numa origem HTTP simples ou num endereço IP sem nome de domínio. As próprias notas de resolução de problemas do projeto dizem o mesmo: se não aparece nenhum pedido, está a usar http:// ou um IP.
Pior ainda, o RP ID fica incorporado em todas as credenciais que os utilizadores já registaram. Se alterar RP_ID mais tarde, as passkeys armazenadas nos dispositivos deixam de corresponder, e ninguém consegue iniciar sessão. Decida primeiro o nome do host, aponte o DNS para o VPS e faça o certificado funcionar antes de alguém tocar em Create profile.
Implantar o openGym com Docker Compose
O ficheiro Compose faz bind mount de ./data e ./media relativamente a si próprio. Por isso, o diretório onde fizer o clone é a sua base de dados. Coloque-o num local 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 .envO README ainda apresenta um URL de clone github.com. Esse endereço já não resolve, e o repositório Gitea acima é a localização atual do projeto.
Edite .env. Num VPS, três linhas são importantes.
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080RP_ID é o nome de host simples e ORIGIN é o URL completo, incluindo o esquema. Têm de corresponder exatamente ao que aparece na barra de endereços. Caso contrário, o início de sessão falha com verification failed. O valor WEB_PORT é explicado na secção sobre como manter a porta 8080 privada.
docker compose up -d --build
docker compose ps
docker compose logs mediadocker compose ps deve mostrar web e api como ativos, e media como terminado com o código 0. Essa terminação está correta: o trabalho de media tem restart: "no" porque a tarefa é uma transferência única. O log termina com uma linha que começa por ✓ Exercise media ready, e ls media/img | wc -l deve apresentar algumas centenas, não 0. Um diretório vazio significa que a transferência falhou. Nesse caso, a aplicação apresenta cartões de exercícios com imagens vazias.
A flag --build não é opcional neste caso. O ficheiro Compose referencia imagens pré-construídas em ghcr.io que já não são publicadas. Por isso, docker compose pull falha com denied ou manifest unknown, e os dois serviços são compilados a partir do código-fonte que acabou de clonar. Ambos incluem uma secção build precisamente para esse fim. Se o Compose ainda é novo para si, comece por Docker Compose num VPS e depois volte aqui.
Fixe a versão, porque este projeto ainda é recente
Como esse namespace do registry deixou de existir, já não há uma tag de imagem para fixar. Em vez disso, fixe o checkout no disco, porque é ele que determina qual versão da aplicação fica no contentor.
cd /opt/opengym
git fetch --tags
git checkout v1.2.7git status agora indica um HEAD separado nessa tag, que é o comportamento pretendido num servidor. Nada muda até fazer checkout de outra versão.
Depois, diga ao Compose para deixar de tentar aceder ao registry. Coloque isto em docker-compose.override.yml, que o Compose carrega automaticamente e combina com o ficheiro controlado pelo git. As chaves escalares são substituídas pelo override, por isso não é necessário editar nada no git e git pull permanece limpo. Consulte como o Compose combina um ficheiro de override para conhecer todas as regras de combinação.
services:
api:
pull_policy: build
web:
pull_policy: buildCom essa configuração, um docker compose up -d posterior cria a partir do código-fonte disponível, em vez de falhar ao tentar fazer pull. Confirme que a combinação foi aplicada e, em seguida, volte a criar a imagem nessa tag.
docker compose config | grep pull_policy
docker compose up -d --buildTerminar o TLS com um reverse proxy
Os contentores comunicam por HTTP simples. Um componente à frente deles tem de gerir o certificado. O Caddy é o caminho mais curto, porque solicita e renova o certificado do Let's Encrypt automaticamente.
gym.example.com {
reverse_proxy 127.0.0.1:8080
}nginx, Traefik e Nginx Proxy Manager funcionam da mesma forma. O mesmo acontece com um Cloudflare Tunnel, que é documentado pelo projeto e não requer nenhuma porta de entrada aberta.
curl -sI https://gym.example.com | head -1Isto deve devolver HTTP/2 200 sem aviso de certificado. Agora abra o site num navegador e selecione Create profile. Se o pedido da passkey aparecer e, depois, o início de sessão devolver verification failed, RP_ID ou ORIGIN não corresponde ao URL na barra de endereço. Corrija .env e execute novamente docker compose up -d, que recria os contentores para que leiam os novos valores. Um docker compose restart não recarrega .env.
Mantenha a porta 8080 fora da Internet pública
Por padrão, o serviço web publica 8080 em todas as interfaces. Assim, a aplicação fica acessível por HTTP simples no seu IP público, enquanto o proxy serve HTTPS no mesmo servidor. Uma regra de firewall não corrige isto. O Docker publica uma porta com uma regra DNAT na tabela nat, e esse tráfego é depois processado na cadeia FORWARD, onde as próprias regras do Docker o aceitam, enquanto as regras do ufw ficam no caminho INPUT. Por isso, sudo ufw deny 8080/tcp não bloqueia nada.
A correção é publicar apenas no endereço de loopback. O ficheiro compose mapeia "${WEB_PORT:-8080}:${NGINX_PORT:-80}". Assim, o valor definido em WEB_PORT é substituído no lado esquerdo desse mapeamento, e a sintaxe curta do Docker aceita aí um par ip:port. É por isso que WEB_PORT=127.0.0.1:8080 funciona.
docker compose config
sudo ss -ltnp | grep 8080Na configuração resultante, em ports do serviço web, deve aparecer host_ip: 127.0.0.1. ss deve mostrar 127.0.0.1:8080, e não 0.0.0.0:8080. A partir de outra máquina, curl http://<your-vps-ip>:8080 deve ser recusado ou atingir o tempo limite, enquanto o hostname HTTPS continua a funcionar.
Feche os registos depois de criar o seu perfil
O registo está aberto por predefinição e o modo de convidado está ativo. Num nome de host público, isso significa que qualquer pessoa que encontre o URL pode criar um perfil no seu servidor. Registe primeiro o seu próprio perfil e, em seguida, encontre o seu ID de utilizador: ls data/ lista um ficheiro chamado state-<uid>.json para cada utilizador, e esse <uid> é o valor de que precisa.
ADMIN_UIDS=<your-uid>
INVITE_ONLY=1
ALLOW_GUEST=0Execute docker compose up -d novamente. A área Settings passa a mostrar um painel Admin onde pode gerar e revogar códigos de convite. Assim, as pessoas com quem treina podem registar-se, e mais ninguém. O openGym não tem conhecimento de fornecedores de identidade externos. Por isso, esses códigos de convite controlam apenas esta aplicação e nada mais no servidor. Se preferir atribuir uma única conta a cada pessoa para todos os serviços que executa, coloque o Authentik à frente como um proxy de autenticação forward auth. Assim, o nome de host é controlado antes de o login com passkey do openGym ser carregado.
Onde os dados ficam e o backup que os protege
Tudo fica no diretório ./data, montado no contentor da API em /data. Existem quatro tipos de ficheiro: db.json contém perfis e credenciais públicas de passkeys, state-<uid>.json contém as rotinas, os treinos e o peso corporal de um utilizador, secret é a chave do cookie de sessão e vapid.json contém as chaves de notificações push geradas na primeira execução.
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start apiPare primeiro a API, porque tar copia ficheiros enquanto a API pode estar a escrever num deles. Um ficheiro JSON copiado apenas parcialmente é restaurado como um ficheiro JSON inválido. Parar e iniciar demora cerca de dois segundos. Depois, copie o arquivo para fora do servidor, porque um arquivo armazenado na VPS não sobrevive à própria VPS. Exclua media/ do backup: são 140 MB de imagens de exercícios que a tarefa de media volta a descarregar gratuitamente.
Restaurar significa extrair o arquivo para o mesmo caminho num host que disponibiliza o mesmo domínio. Uma passkey armazenada no telemóvel está associada ao RP ID em que foi criada. Por isso, restaurar os dados num novo hostname produz uma base de dados funcional, mas ninguém consegue iniciar sessão. Mantenha o domínio ou planeie voltar a registar todas as passkeys. A mesma disciplina aplica-se a tudo o resto que executar, e fazer backup e atualizar uma stack Docker Compose explica o procedimento geral.
O servidor MCP é somente leitura e é executado na sua máquina
MCP (model context protocol) é a forma como um cliente, como Claude Desktop ou Cursor, comunica com um servidor de ferramentas local. O openGym inclui um servidor em mcp/. Ele não faz parte do ficheiro compose, não é um contentor e não escuta em nenhuma porta. O cliente inicia-o como um processo filho e comunica com ele através de stdio. Por isso, o README diz que ele nunca sai da sua máquina.
Instale-o onde o cliente é executado, não no servidor:
cd openGym/mcp
npm installDepois, adicione-o 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 é opcional numa instalação de um único utilizador, na qual o servidor deteta o único perfil encontrado. Ele expõe oito ferramentas: list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm e muscle_balance. Todas fazem apenas leituras. Nenhuma escreve dados. Assim, um assistente pode responder ao que registou na semana passada, mas não pode registar uma série, editar uma rotina nem apagar dados.
Esta é a parte que um utilizador de VPS tem de resolver. OPENGYM_DATA é um caminho do sistema de ficheiros, mas os seus dados estão no VPS enquanto o cliente de IA está no seu portátil. Há duas opções que representam corretamente essa situação.
- Copie os dados para o portátil e indique ao servidor a cópia:
rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/. Depois, definaOPENGYM_DATAcomo~/opengym-data. O servidor apenas lê os dados, portanto a cópia não perde informação. Execute novamente o rsync quando quiser obter números atualizados. - Execute o servidor através de ssh, com
commanddefinido comossheargsdefinido como["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]. É necessário ter o Node instalado no VPS e usar um login que não escreva nada em stdout, porque stdout é o canal do protocolo.
Se cat data/db.json devolver Permission denied, o contentor da API escreveu esses ficheiros como root e o seu login não consegue lê-los. Copie-os com sudo ou altere o proprietário no host. Para servidores que devam escutar na rede em vez de usar stdio, consulte executar servidores MCP num VPS.
openGym ou wger: qual deve executar?
wger é a opção consolidada neste nicho e é uma aplicação muito maior. A sua stack Compose executa o gunicorn para servir uma aplicação Django, PostgreSQL, Redis e um worker Celery atrás do nginx. Em troca, oferece acompanhamento de nutrição e ingredientes, uma API REST documentada, uma grande base de dados de exercícios e funcionalidades para treinadores gerirem os planos de outras pessoas.
openGym são dois contentores, uma pasta com ficheiros JSON e nenhuma conta para administrar além de passkeys. Essa é toda a diferença.
Execute wger se quiser acompanhar a alimentação juntamente com o treino ou se precisar de uma API para desenvolver integrações. Execute openGym se quiser uma stack suficientemente pequena para ler de ponta a ponta numa tarde e um início de sessão sem uma palavra-passe que possa ser exposta. O custo dessa escolha é a maturidade: em 19 August 2026, a primeira release do openGym tem um mês, enquanto o wger tem anos de releases acumuladas. Fixe a versão, mantenha as cópias de segurança e leia as release notes antes de cada atualização.
Se ainda estiver a decidir o que merece espaço no servidor, o que vale a pena alojar autonomamente em 2026 explica os compromissos, e esta aplicação combina bem com Mealie para receitas ou Actual Budget para finanças no mesmo VPS pequeno.
Atualizar sem perder nada
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api
git fetch --tagsMude para a versão pretendida com git checkout v<new> e execute docker compose up -d --build para reconstruir os contentores a partir dessa tag. Faça sempre a cópia de segurança primeiro, porque o processo de restauro dos ficheiros JSON em disco consiste num único comando tar e demora apenas alguns segundos.
FAQ
Por que o openGym nunca mostra um pedido de passkey no meu telemóvel?
O navegador recusa criar uma credencial porque está em http:// ou num endereço IP simples, como http://192.168.1.20:8080. Os navegadores só permitem passkeys em origens HTTPS, com localhost como única exceção. Coloque o openGym atrás de um reverse proxy com um certificado válido para um hostname real, defina RP_ID=gym.example.com e ORIGIN=https://gym.example.com em .env e execute docker compose up -d para que os contentores recebam os novos valores. Se o pedido aparecer, mas o início de sessão indicar verification failed, esses dois valores não correspondem exatamente ao URL na barra de endereços.
Onde o openGym armazena os meus dados e como faço uma cópia de segurança?
No diretório ./data junto ao ficheiro compose, montado no contentor da API como /data. Contém db.json, com os perfis e as credenciais públicas de passkey, um state-<uid>.json por utilizador, com os treinos e o peso corporal, secret, com a chave dos cookies de sessão, e vapid.json, com as chaves das notificações push. Faça a cópia de segurança com docker compose stop api, depois tar czf ~/opengym-$(date +%F).tar.gz data/, depois docker compose start api, e copie o arquivo para fora do servidor. Ignore media/, que tem 140 MB de imagens de exercícios que o job de media descarrega novamente por conta própria.
O Claude pode ler o meu histórico de treinos do openGym?
Sim, através do servidor MCP opcional no diretório mcp/, e apenas para leitura. Expõe oito ferramentas para consultar rotinas, planos semanais, treinos registados, peso corporal, uma repetição máxima estimada e equilíbrio muscular; nenhuma delas escreve dados. Não é um contentor e não abre nenhuma porta: o seu cliente inicia-o através de stdio e lê diretamente os ficheiros JSON em OPENGYM_DATA. Como esse valor é um caminho do sistema de ficheiros, executar o openGym num VPS significa sincronizar uma cópia de data/ para a máquina que executa o cliente ou invocar o servidor através de ssh na configuração do cliente.
Devo alojar o openGym ou o wger no meu próprio servidor?
Escolha o wger se quiser acompanhar a alimentação e a nutrição juntamente com o registo de treinos ou se precisar de uma API REST documentada para desenvolver sobre ela. O wger executa uma stack maior: Django com gunicorn, PostgreSQL, Redis e um worker Celery atrás do nginx. Escolha o openGym se quiser dois contentores, ficheiros JSON que possa ler com cat e início de sessão com passkey, sem palavras-passe para gerir. Em 19 August 2026, a primeira release etiquetada do openGym tinha um mês, por isso consulte uma tag do git e faça uma cópia de segurança de data/ antes de cada atualização.