Cómo alojar openGym con Docker Compose
Despliega openGym en un VPS con Docker Compose: fija un tag de git, configura TLS antes de la primera passkey, ubica los JSON y usa el MCP de solo lectura.
Qué se obtiene al alojar openGym por cuenta propia
Para alojar openGym por cuenta propia, clone el repositorio, edite dos líneas en .env y ejecute docker compose up -d --build detrás de un reverse proxy que termine TLS (seguridad de la capa de transporte). openGym es un gestor de gimnasio y peso corporal: planes semanales, entrenamientos guiados, registro de cada serie y evolución del peso. Tiene licencia AGPL-3.0 y almacena todo en archivos JSON sin formato en el disco, por lo que no hay que ejecutar un servidor de base de datos.
La pila consta de dos contenedores de ejecución continua: un contenedor nginx que sirve la compilación de React y un contenedor Node que aloja la API. También incluye un trabajo de ejecución única que descarga unos 140 MB de imágenes y GIF de ejercicios la primera vez que se inicia.
Hay dos aspectos que el README del proyecto da a entender, pero no explica de forma explícita para quien despliega en un servidor público. El inicio de sesión con passkey está vinculado a un nombre de host, por lo que el dominio y su certificado deben existir antes del primer inicio de sesión, no después. Además, el servidor MCP opcional es de solo lectura y se ejecuta en la máquina donde se ejecuta el cliente de IA, no dentro de la pila. Esto cambia las acciones necesarias cuando los datos están en un VPS.
openGym es reciente. La primera versión etiquetada, v1.0.0, está fechada el 20 de julio de 2026, y v1.2.7 se publicó el 18 de agosto de 2026. Trece etiquetas en aproximadamente un mes indican que la aplicación todavía cambia con frecuencia. Por eso, compruebe una etiqueta de versión en lugar de compilar lo que haya en la rama predeterminada.
Planifique el dominio antes del primer inicio de sesión
Las passkeys son el método de inicio de sesión en openGym. Una passkey está vinculada a un identificador de relying party (RP ID), que es el dominio en el que se creó la credencial, y los navegadores sólo crean passkeys mediante HTTPS. La única excepción es localhost.
Esto provoca un problema habitual en el teléfono. Abra http://203.0.113.10:8080 desde otro dispositivo y no aparecerá ninguna solicitud para crear una passkey, porque el navegador no permite crear credenciales en un origen HTTP sin cifrar ni en una dirección IP sin nombre de dominio. Las propias notas de resolución de problemas del proyecto indican lo mismo: si no aparece ninguna solicitud, está usando http:// o una IP.
El problema es aún mayor porque el RP ID queda incorporado en cada credencial que los usuarios ya han registrado. Si cambia RP_ID más adelante, las passkeys almacenadas en sus dispositivos dejarán de coincidir y nadie podrá iniciar sesión. Decida primero el nombre de host, apunte el DNS al VPS y haga funcionar el certificado antes de que alguien pulse Crear perfil.
Implementar openGym con Docker Compose
El archivo de Compose monta ./data y ./media mediante bind mounts relativos a él mismo. Por tanto, el directorio donde se clona el proyecto es también la base de datos. Colóquelo en una ubicación 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 .envEl README todavía muestra una URL de clonación github.com. Esa dirección ya no resuelve, y el repositorio de Gitea anterior es la ubicación activa del proyecto.
Edite .env. En un VPS hay tres líneas importantes.
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080RP_ID es el nombre de host sin más y ORIGIN es la URL completa, incluido el esquema. Deben coincidir exactamente con lo que aparece en la barra de direcciones; de lo contrario, el inicio de sesión falla con verification failed. El valor WEB_PORT se explica en la sección sobre cómo mantener privado el puerto 8080.
docker compose up -d --build
docker compose ps
docker compose logs mediadocker compose ps debería mostrar web y api en ejecución, y media como finalizado con el código 0. Esa finalización es correcta: el trabajo de medios tiene restart: "no" porque su tarea es una descarga única. Su registro termina con una línea que comienza por ✓ Exercise media ready, y ls media/img | wc -l debería mostrar unos cientos, no 0. Un directorio vacío indica que la descarga falló. En ese caso, la aplicación muestra tarjetas de ejercicios con las imágenes en blanco.
La opción --build es obligatoria en este caso. El archivo de Compose especifica imágenes precompiladas en ghcr.io que ya no se publican. Por eso, docker compose pull falla con denied o manifest unknown, y los dos servicios se compilan a partir del código fuente que acaba de clonar. Ambos incluyen una sección build precisamente para eso. Si Compose todavía es nuevo para usted, empiece por Docker Compose en un VPS y vuelva después.
Fija la versión porque este proyecto es reciente
Como ese espacio de nombres del registro ya no existe, no queda ninguna etiqueta de imagen que fijar. En su lugar, fija la revisión del checkout en disco, porque determina qué versión de la aplicación termina en el contenedor.
cd /opt/opengym
git fetch --tags
git checkout v1.2.7git status ahora informa de un HEAD separado en esa etiqueta, que es lo que necesitas en un servidor. Nada cambia mientras no hagas checkout de otra revisión.
A continuación, indica a Compose que deje de consultar el registro. Coloca esto en docker-compose.override.yml, que Compose carga automáticamente y combina sobre el archivo versionado. Las claves escalares se sustituyen por las del archivo de anulación, por lo que no es necesario editar nada en git y git pull permanece limpio. Consulta cómo combina Compose un archivo de anulación para conocer todas las reglas de combinación.
services:
api:
pull_policy: build
web:
pull_policy: buildCon esto, un docker compose up -d posterior compila a partir del código fuente disponible en lugar de fallar al intentar hacer pull. Comprueba que la combinación se haya aplicado y vuelve a compilar en esa etiqueta.
docker compose config | grep pull_policy
docker compose up -d --buildTerminar TLS con un proxy inverso
Los contenedores usan HTTP sin cifrar. Un componente frontal debe gestionar el certificado. Caddy es la opción más sencilla porque solicita y renueva el certificado de Let's Encrypt automáticamente.
gym.example.com {
reverse_proxy 127.0.0.1:8080
}nginx, Traefik y Nginx Proxy Manager funcionan de la misma forma. Cloudflare Tunnel también funciona así. El proyecto lo documenta y no requiere abrir ningún puerto entrante.
curl -sI https://gym.example.com | head -1El comando debe devolver HTTP/2 200 sin advertencias sobre el certificado. Ahora abra el sitio en un navegador y pulse Create profile. Si aparece la solicitud de passkey y después el inicio de sesión muestra verification failed, RP_ID o ORIGIN no coincide con la URL de la barra de direcciones. Corrija .env y vuelva a ejecutar docker compose up -d. Esto recrea los contenedores para que lean los valores nuevos. Un docker compose restart no vuelve a cargar .env.
No exponga el puerto 8080 a Internet
De forma predeterminada, el servicio web publica 8080 en todas las interfaces. Por tanto, la aplicación queda accesible mediante HTTP sin cifrar en la IP pública, mientras el proxy sirve HTTPS en el mismo equipo. Una regla del firewall no corrige esto. Docker publica un puerto mediante una regla DNAT en la tabla nat, y ese tráfico se procesa después en la cadena FORWARD, donde las reglas propias de Docker lo aceptan, mientras las reglas de ufw se encuentran en la ruta INPUT. Por tanto, sudo ufw deny 8080/tcp no bloquea nada.
La solución es publicar el puerto sólo en la dirección de loopback. El archivo compose asigna "${WEB_PORT:-8080}:${NGINX_PORT:-80}", por lo que el valor definido en WEB_PORT se sustituye a la izquierda de esa asignación, y la sintaxis abreviada de Docker acepta allí un par ip:port. Por eso funciona WEB_PORT=127.0.0.1:8080.
docker compose config
sudo ss -ltnp | grep 8080En la configuración combinada, dentro de ports del servicio web, debe aparecer host_ip: 127.0.0.1. ss debe mostrar 127.0.0.1:8080 y no 0.0.0.0:8080. Desde otra máquina, curl http://<your-vps-ip>:8080 debe rechazarse o agotar el tiempo de espera, mientras el nombre de host HTTPS sigue funcionando.
Cierre el registro después de crear su perfil
El registro está abierto de forma predeterminada y el modo invitado está activado. En un nombre de host público, cualquier persona que encuentre la URL puede crear un perfil en su servidor. Registre primero su propio perfil y busque después su ID de usuario: ls data/ muestra un archivo llamado state-<uid>.json para cada usuario, y ese <uid> es el valor que necesita.
ADMIN_UIDS=<your-uid>
INVITE_ONLY=1
ALLOW_GUEST=0Ejecute docker compose up -d de nuevo. Settings ahora muestra un panel de administración donde puede generar y revocar códigos de invitación. Así, las personas con las que entrena pueden registrarse y nadie más. openGym no conoce los proveedores de identidad externos. Por tanto, esos códigos de invitación sólo controlan esta aplicación y ninguna otra del servidor. Si prefiere asignar una sola cuenta a cada persona para todos los servicios que ejecuta, colocar Authentik delante como proxy de autenticación forward auth controla el nombre de host antes de que se cargue el inicio de sesión con passkey de openGym.
Dónde están los datos y qué copia de seguridad los protege
Todo está en el directorio ./data, montado en el contenedor de la API en /data. Hay cuatro tipos de archivo: db.json contiene los perfiles y las credenciales públicas de passkey, state-<uid>.json contiene las rutinas, los entrenamientos y el peso corporal de un usuario, secret es la clave de la cookie de sesión y vapid.json contiene las claves de notificaciones push generadas durante la primera ejecución.
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start apiDetenga primero la API porque tar copia los archivos mientras la API puede estar escribiendo en uno de ellos, y un archivo JSON copiado parcialmente se restaura como un archivo JSON dañado. Detenerla y arrancarla de nuevo tarda unos dos segundos. Después, copie el archivo comprimido fuera del servidor, porque un archivo comprimido almacenado en el VPS no sobrevive a la pérdida del VPS. Excluya media/ de la copia de seguridad: contiene 140 MB de imágenes de ejercicios que el trabajo de medios vuelve a descargar sin coste.
Restaurar consiste en extraer el archivo en la misma ruta, en un host que sirva el mismo dominio. Una passkey almacenada en el teléfono está asociada al RP ID con el que se creó, por lo que restaurarla en un nombre de host nuevo proporciona una base de datos operativa, pero nadie puede iniciar sesión. Mantenga el dominio o planifique volver a registrar todas las passkeys. La misma disciplina se aplica al resto de servicios que ejecute, y hacer copias de seguridad y actualizar una pila de Docker Compose cubre el procedimiento general.
El servidor MCP es de solo lectura y se ejecuta en su máquina
MCP (model context protocol) es el mecanismo que usa un cliente como Claude Desktop o Cursor para comunicarse con un servidor de herramientas local. openGym incluye uno en mcp/. No forma parte del archivo compose, no es un contenedor y no escucha en ningún puerto. El cliente lo inicia como proceso hijo y se comunica con él mediante stdio. Por eso el README indica que nunca sale de su máquina.
Instálelo donde se ejecuta el cliente, no en el servidor:
cd openGym/mcp
npm installDespués, agréguelo 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 es opcional en una instalación de un solo usuario, en la que el servidor detecta el único perfil que encuentra. Expone ocho herramientas: list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm y muscle_balance. Todas son de solo lectura. Ninguna modifica datos. Por tanto, un asistente puede responder qué registró la semana pasada, pero no puede registrar una serie, editar una rutina ni eliminar nada.
Esta es la parte que debe resolver quien usa un VPS. OPENGYM_DATA es una ruta del sistema de archivos, y sus datos están en el VPS mientras que el cliente de IA está en su portátil. Hay dos opciones que reflejan esta situación correctamente.
- Copie los datos y haga que el servidor use la copia:
rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/; después, establezcaOPENGYM_DATAen~/opengym-data. El servidor sólo lee los datos, por lo que una copia no pierde información. Vuelva a ejecutar rsync cuando necesite datos actualizados. - Ejecute el servidor mediante ssh, con
commandestablecido ensshyargsestablecido en["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]. Node debe estar instalado en el VPS y la sesión de inicio de sesión no debe escribir nada en stdout, porque stdout es el canal del protocolo.
Si cat data/db.json devuelve Permission denied, el contenedor de la API escribió esos archivos como root y su cuenta no puede leerlos. Cópielos con sudo o cambie el propietario en el host. Para servidores que deben escuchar en la red en lugar de usar stdio, consulte ejecutar servidores MCP en un VPS.
openGym o wger: ¿cuál debería ejecutar?
wger es la opción consolidada en este ámbito y es un software mucho más grande. Su stack de Compose ejecuta gunicorn para servir una aplicación Django, PostgreSQL, Redis y un worker de Celery detrás de nginx. A cambio, ofrece seguimiento de nutrición e ingredientes, una API REST documentada, una base de datos de ejercicios amplia y funciones para que los entrenadores gestionen los planes de otras personas.
openGym consta de dos contenedores, una carpeta de archivos JSON y ninguna cuenta que administrar, aparte de las passkeys. Esa es toda la diferencia.
Ejecute wger si quiere registrar la alimentación junto con el entrenamiento o si necesita una API sobre la que desarrollar. Ejecute openGym si quiere un stack lo bastante pequeño como para leerlo de principio a fin en una tarde y un inicio de sesión sin contraseñas que puedan filtrarse. El coste de esa elección es la madurez: a fecha del 19 de agosto de 2026, la primera versión de openGym tiene un mes, mientras que wger acumula años de versiones. Fije la versión, conserve las copias de seguridad y lea las notas de la versión antes de cada actualización.
Si todavía está decidiendo qué merece espacio en el servidor, qué merece la pena alojar por cuenta propia en 2026 explica las ventajas y desventajas, y esta aplicación encaja bien junto a Mealie para recetas o Actual Budget para las finanzas en el mismo VPS pequeño.
Actualizar sin perder datos
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api
git fetch --tagsCambie a la versión que necesita con git checkout v<new> y, después, ejecute docker compose up -d --build para reconstruir los contenedores a partir de esa etiqueta. La copia de seguridad se realiza siempre antes, porque restaurar archivos JSON desde el disco requiere un solo comando tar y tarda unos segundos.
FAQ
¿Por qué openGym nunca muestra una solicitud de clave de acceso en mi teléfono?
El navegador rechaza la creación de una credencial porque está en http:// o en una dirección IP sin nombre de host, como http://192.168.1.20:8080. Los navegadores sólo permiten claves de acceso en orígenes HTTPS, con localhost como única excepción. Coloque openGym detrás de un reverse proxy que use un certificado válido para un nombre de host real, establezca RP_ID=gym.example.com y ORIGIN=https://gym.example.com en .env y ejecute docker compose up -d para que los contenedores recojan los valores nuevos. Si aparece la solicitud pero el inicio de sesión informa verification failed, esos dos valores no coinciden exactamente con la URL de la barra de direcciones.
¿Dónde almacena openGym mis datos y cómo puedo hacer una copia de seguridad?
En el directorio ./data, junto al archivo compose, montado en el contenedor de la API como /data. Contiene db.json para los perfiles y las credenciales públicas de claves de acceso, un archivo state-<uid>.json por usuario para los entrenamientos y el peso corporal, secret para la clave de las cookies de sesión y vapid.json para las claves de notificaciones push. Haga la copia de seguridad con docker compose stop api, después tar czf ~/opengym-$(date +%F).tar.gz data/ y luego docker compose start api, y copie el archivo comprimido fuera del servidor. Omita media/, que ocupa 140 MB con imágenes de ejercicios que el trabajo de medios vuelve a descargar por su cuenta.
¿Puede Claude leer mi historial de entrenamientos de openGym?
Sí, mediante el servidor MCP opcional del directorio mcp/, y sólo para lectura. Expone ocho herramientas para consultar rutinas, planes semanales, entrenamientos registrados, peso corporal, el máximo estimado de una repetición y el equilibrio muscular; ninguna escribe datos. No es un contenedor ni abre ningún puerto: el cliente lo inicia mediante stdio y lee directamente los archivos JSON de OPENGYM_DATA. Como se trata de una ruta del sistema de archivos, ejecutar openGym en un VPS requiere sincronizar una copia de data/ con el equipo que ejecuta el cliente o invocar el servidor mediante ssh desde la configuración del cliente.
¿Debería alojar openGym o wger en mi propio servidor?
Elija wger si quiere registrar alimentos y nutrición junto con sus entrenamientos, o si necesita una API REST documentada sobre la que desarrollar. Ejecuta una pila más grande: Django con gunicorn, PostgreSQL, Redis y un worker de Celery detrás de nginx. Elija openGym si quiere dos contenedores, archivos JSON que pueda leer con cat y un inicio de sesión con clave de acceso sin administrar contraseñas. A fecha del 19 de agosto de 2026, la primera versión etiquetada de openGym tiene un mes, por lo que debe consultar una etiqueta de git y hacer una copia de seguridad de data/ antes de cada actualización.