SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-27

Como fazer self-host do openGym com Docker Compose

Instale o openGym em um VPS com Docker Compose, fixe uma tag Git, configure TLS antes da primeira Passkey, localize os dados e use o MCP somente leitura.

O que obtém ao fazer self-hosting do openGym

Faça o self-hosting do openGym 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 (transport layer security). O openGym é um gestor de treinos e de peso corporal: planos semanais, treinos guiados, registo de todas as séries e evolução do peso ao longo do tempo. O software é licenciado ao abrigo da AGPL-3.0 e armazena tudo em ficheiros JSON simples no disco, pelo que não é necessário executar um servidor de base de dados.

A stack tem dois contentores de execução contínua: um contentor nginx que serve a compilação React e um contentor Node que aloja a API. Tem também uma tarefa executada uma única vez, que descarrega cerca de 140 MB de imagens e GIFs de exercícios na primeira inicialização.

O README do projeto sugere duas coisas, mas não as explica a quem está a fazer uma implementação num servidor público. O início de sessão com Passkey está associado a um nome de host, pelo que 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 corre o cliente de IA, não dentro da stack. Isto altera o que tem de fazer quando os dados estão num VPS.

O openGym ainda é recente. A primeira versão marcada, v1.0.0, tem a data de 20 July 2026, e a v1.2.7 foi lançada em 18 August 2026. Treze tags em cerca de um mês significam que a aplicação ainda está a mudar. Por isso, faça checkout de uma tag de versão, em vez de compilar o 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á associada a um relying party ID (RP ID), que corresponde ao domínio onde 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 a partir de outro 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 isolado. As próprias notas de resolução de problemas do projeto dizem o mesmo: a ausência do pedido significa que está em http:// ou num IP.

O problema é ainda maior porque o RP ID fica gravado em todas as credenciais que os utilizadores já registaram. Altere RP_ID mais tarde e as passkeys armazenadas nos dispositivos deixam de corresponder, impedindo qualquer pessoa de iniciar sessão. Decida primeiro o nome do host, aponte o DNS para o VPS e configure o certificado antes de alguém tocar em Create profile.

Implementar o openGym com Docker Compose

O ficheiro Compose monta ./data e ./media como bind mounts relativos ao próprio ficheiro, pelo que 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 .env

O 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, há três linhas importantes.

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

RP_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 manter a porta 8080 privada.

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

docker compose ps deve apresentar web e api como em execução, e media como terminado com o código 0. Essa saída é correta: o trabalho de media tem restart: "no" porque a tarefa é uma transferência única. O respetivo 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, pelo que 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 têm uma secção build precisamente para esse fim. Se o Compose ainda for novo para si, comece por Docker Compose num VPS e depois retome 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 container.

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

git status agora indica um HEAD desanexado 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 sobre o ficheiro controlado pelo git. As chaves escalares são substituídas pelo override, portanto 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: build

Com isso, um docker compose up -d posterior compila 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, faça novamente o build na tag.

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

Terminar TLS com um reverse proxy

Os contentores comunicam por HTTP simples. É necessário ter um componente à frente para gerir o certificado. Caddy é o caminho mais curto, porque solicita e renova o certificado da 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 se aplica a um Cloudflare Tunnel, que está documentado pelo projeto e não requer qualquer porta de entrada aberta.

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

Isto deve devolver HTTP/2 200 sem qualquer aviso de certificado. Agora abra o site num browser e selecione Create profile. Se o pedido da passkey aparecer e, em seguida, o início de sessão indicar que verification failed, RP_ID ou ORIGIN não corresponde ao URL na barra de endereço, corrija .env e execute docker compose up -d novamente. Isto recria os contentores para que leiam os novos valores. Um docker compose restart não recarrega .env.

Manter a porta 8080 fora da Internet pública

Por predefiniçã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 disponibiliza HTTPS no mesmo servidor. Uma regra de firewall não corrige este problema. O Docker publica uma porta com uma regra DNAT na tabela nat. Depois, esse tráfego é 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 consiste em 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 8080

Na configuração agregada, em ports do serviço Web, deve ver 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 exceder o tempo limite. O hostname HTTPS deve continuar a funcionar.

Feche o registo depois de criar o seu perfil

O registo está aberto por predefinição, e o modo de convidado está ativo. Num hostname público, qualquer pessoa que encontre o URL pode criar um perfil no seu servidor. Crie 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=0

Execute docker compose up -d novamente. Agora, Settings mostra um painel de administração onde pode gerar e revogar códigos de convite. Assim, as pessoas com quem treina podem registar-se, e mais ninguém pode fazê-lo. O openGym não conhece fornecedores de identidade externos. Por isso, esses códigos de convite controlam apenas esta aplicação, e mais nada no servidor. Se preferir atribuir uma única conta a cada pessoa em tudo o que executa, coloque o Authentik à frente como um proxy de autenticação forward auth. Dessa forma, o hostname é protegido antes de o login por passkey do openGym ser carregado.

Onde os dados ficam e o backup que os protege

Tudo está no diretório ./data, montado no contentor da API em /data. Existem quatro tipos de ficheiro: db.json contém os perfis e as 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 api

Pare primeiro a API, porque tar copia ficheiros enquanto a API pode estar a escrever num deles, e um ficheiro JSON copiado pela metade é restaurado como um ficheiro JSON inválido. Parar e iniciar o serviço demora cerca de dois segundos. Em seguida, copie o arquivo para fora do servidor, porque um arquivo armazenado na VPS não sobrevive à VPS. Exclua media/ do backup: são 140 MB de imagens de exercícios que o processo de media transfere novamente sem custos.

Restaurar significa extrair o tar para o mesmo caminho num host que disponibilize o mesmo domínio. Uma passkey armazenada no telefone está associada ao RP ID onde foi criada, por isso restaurar os dados num novo hostname fornece uma base de dados funcional na qual 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 que executar, e fazer backup e atualizar uma stack Docker Compose descreve a rotina 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 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 que o README indica que ele nunca sai da sua máquina.

Instale-o onde o cliente é executado, não no servidor:

cd openGym/mcp
npm install

Depois, 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 para um único utilizador, na qual o servidor deteta o único perfil encontrado. Expõe oito ferramentas: list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm e muscle_balance. Todas fazem leituras. Nenhuma escreve. Assim, um assistente pode responder ao que treinou na semana passada, mas não pode registar uma série, editar uma rotina nem apagar nada. Esta lista é um exemplo compacto da decisão de design de agentes à qual se regressa frequentemente: as ferramentas expostas definem tudo o que um modelo pode fazer. Aprender como os agentes funcionam escrevendo o ciclo por si próprio é a forma mais rápida de perceber por que motivo um conjunto de ferramentas somente leitura é uma escolha de design, e não uma limitação.

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 na VPS enquanto o cliente de IA está no portátil. Há duas opções que refletem essa realidade.

  1. Copie os dados para o portátil e aponte o servidor para a cópia: rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/. Depois, defina OPENGYM_DATA como ~/opengym-data. O servidor apenas lê os dados, pelo que uma cópia não perde informação. Execute novamente o rsync quando quiser obter valores atualizados.
  2. Execute o servidor através de ssh, com command definido como ssh e args definido como ["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]. É necessário ter Node instalado na VPS e usar uma conta cujo início de sessão não escreva nada em stdout, porque stdout é o canal do protocolo.

Ambas as opções pressupõem que o próprio agente está no seu portátil. Se preferir executá-lo no mesmo servidor que contém os dados, o OneCLI fornece a cada pessoa um agente isolado no servidor, e o salto stdio de volta para data/ volta a ser local.

Se cat data/db.json devolver Permission denied, o contentor da API criou esses ficheiros como root e a sua conta não consegue lê-los. Copie-os com sudo ou altere o proprietário no host. Para servidores que devem escutar na rede em vez de usar stdio, consulte executar servidores MCP numa 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, que serve uma aplicação Django, o PostgreSQL, o Redis e um worker Celery atrás do nginx. Em troca, obtém acompanhamento de nutrição e ingredientes, uma API REST documentada, uma base de dados grande de exercícios e funcionalidades para treinadores gerirem os planos de outras pessoas.

openGym é composto por dois contentores, uma pasta de ficheiros JSON e nenhuma conta para administrar além de passkeys. Essa é toda a diferença. Se já manteve uma instalação do Chatwoot em execução, em que um backup significa um dump do Postgres juntamente com o diretório de uploads e cada atualização de versão executa migrações da base de dados, já sabe o tipo de manutenção que o wger exige.

Execute o wger se quiser acompanhar a alimentação juntamente com o treino ou se precisar de uma API para desenvolver integrações. Execute o openGym se quiser uma stack pequena o suficiente para ler de ponta a ponta numa tarde e um início de sessão sem palavra-passe para ficar 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. Fixe a versão, mantenha os backups e leia as notas de release 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 aborda as contrapartidas, e esta aplicação fica bem ao lado do Mealie para receitas ou do Actual Budget para finanças no mesmo VPS pequeno.

Atualizar sem perder dados

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

Faça checkout da release pretendida com git checkout v<new> e execute docker compose up -d --build para reconstruir os contentores a partir dessa tag. A cópia de segurança é sempre feita primeiro, porque o 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 browser está a recusar a criação de uma credencial porque está em http:// ou num endereço IP direto, como http://192.168.1.20:8080. Os browsers 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 nome de host 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 apresentado na barra de endereços.

Onde o openGym guarda 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 ficheiro state-<uid>.json por utilizador, com os treinos e o peso corporal, secret, com a chave do cookie 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/ e depois docker compose start api, e copie o arquivo para fora do servidor. Exclua media/, que ocupa 140 MB com imagens de exercícios que o trabalho de media volta a descarregar automaticamente.

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 o 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 se trata de um caminho do sistema de ficheiros, executar o openGym num VPS implica 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. Executa uma stack maior: Django sob 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 ter de gerir palavras-passe. 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.