如何用 Docker Compose 自托管 openGym
在 VPS 上部署 openGym:检出固定版本标签,首次创建 Passkey 前配置域名与 TLS,了解 JSON 数据位置,并正确运行只读 MCP 服务器。
自托管 openGym 的结果
通过克隆代码库、编辑 .env 中的两行,并在负责终止 TLS(传输层安全)的反向代理后运行 docker compose up -d --build,即可自托管 openGym。openGym 是一款健身和体重训练跟踪器,提供每周计划、指导训练、逐组记录,以及随时间变化的体重记录。它采用 AGPL-3.0 许可证,所有数据都以纯 JSON 文件存储在磁盘上,因此无需运行数据库服务器。
该技术栈包含两个长期运行的容器:一个提供 React 构建文件的 nginx 容器,以及一个承载 API 的 Node 容器。此外,首次启动时还会运行一个一次性任务,下载约 140 MB 的训练动作图片和 GIF。
项目的 README 暗示了两点,但没有明确说明这对部署到公网服务器的影响。Passkey 登录绑定到主机名,因此域名及其证书必须在首次登录前就绪,而不是登录后再配置。可选的 MCP 服务器是只读的,并且运行在 AI 客户端所在的计算机上,而不是该技术栈内部;当数据存储在 VPS 上时,这会改变所需的配置方式。
openGym 仍处于早期阶段。第一个带标签的版本 v1.0.0 发布于 20 July 2026,v1.2.7 于 18 August 2026 发布。约一个月内发布了 13 个标签,说明该应用仍在快速变化。因此,应检出某个 release 标签,而不要直接构建默认分支当前的代码。
首次登录前规划域名
Passkey 是登录 openGym 的方式。Passkey 绑定到依赖方 ID(RP ID),也就是创建凭据时使用的域名。浏览器只会通过 HTTPS 创建 Passkey。唯一的例外是 localhost。
手机用户会遇到这个问题。在其他设备上打开 http://203.0.113.10:8080 时,完全不会出现 Passkey 提示。这是因为浏览器拒绝在普通 HTTP 来源或裸 IP 地址上创建凭据。项目自己的故障排查说明也指出了同样的问题:没有提示,说明您正在使用 http:// 或 IP 地址。
更严重的是,RP ID 已写入用户注册的每个凭据。之后如果更改 RP_ID,用户设备上存储的 Passkey 就不再匹配,因此所有人都无法登录。请先确定主机名,将 DNS 指向 VPS,并在任何人点击 Create profile 之前完成证书配置。
使用 Docker Compose 部署 openGym
Compose 文件会将 ./data 和 ./media 按相对于自身的路径绑定挂载,因此您克隆到的目录就是数据库。请将其放在持久化存储中。
sudo install -d -o "$USER" -g "$USER" /opt/opengym
git clone https://gitea.com/DuarteSantos/openGym /opt/opengym
cd /opt/opengym
cp .env.example .envREADME 仍显示 github.com 克隆 URL。该地址已无法解析,上文的 Gitea 仓库才是项目当前使用的地址。
编辑 .env。在 VPS 上,有 3 行配置很重要。
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080RP_ID 是不带协议的主机名,ORIGIN 是包含协议的完整 URL。两者必须与地址栏中的地址完全一致,否则登录会因 verification failed 失败。WEB_PORT 的含义在“保持端口 8080 私有”一节中说明。
docker compose up -d --build
docker compose ps
docker compose logs mediadocker compose ps 应显示 web 和 api 正在运行,并显示 media 已以代码 0 退出。该退出状态是正确的:媒体任务已 restart: "no",因为它只需执行一次下载。其日志末尾会有一行以 ✓ Exercise media ready 开头,ls media/img | wc -l 应输出几百个文件,而不是 0。目录为空表示下载失败,应用随后会显示没有图片的练习卡片。
这里 --build 标志不可省略。Compose 文件引用了 ghcr.io 上的预构建镜像,但这些镜像已不再发布,因此 docker compose pull 会因 denied 或 manifest unknown 失败,两个服务会改为使用您刚刚克隆的源代码构建。两个服务都包含 build 配置段,正是为此用途设置的。如果您不熟悉 Compose,请先阅读 VPS 上的 Docker Compose,然后再返回这里。
固定版本,因为这个项目还不成熟
由于该镜像仓库命名空间已经不存在,因此没有可供固定的镜像标签。此时应固定磁盘上的代码检出版本,因为它决定容器中最终运行的应用版本。
cd /opt/opengym
git fetch --tags
git checkout v1.2.7git status 现在会报告处于该标签对应的 detached HEAD 状态,这正是服务器所需的状态。除非您检出其他版本,否则当前版本不会发生变化。
然后让 Compose 完全停止访问该镜像仓库。将以下内容放入 docker-compose.override.yml。Compose 会自动加载此文件,并将其合并到受版本控制的文件之上。标量键会被覆盖文件中的值替换,因此无需修改 git 中的内容,git pull 也会保持干净。完整的合并规则请参阅Compose 如何合并覆盖文件。
services:
api:
pull_policy: build
web:
pull_policy: build完成这些设置后,后续执行 docker compose up -d 时会基于现有源代码构建,而不会因拉取镜像失败。先确认合并已生效,然后在该标签对应的版本上重新构建。
docker compose config | grep pull_policy
docker compose up -d --build使用反向代理终止 TLS
容器使用纯 HTTP。前端必须负责保存证书。Caddy 是最简便的方案,因为它会自行从 Let's Encrypt 申请和续期证书。
gym.example.com {
reverse_proxy 127.0.0.1:8080
}nginx、Traefik 和 Nginx Proxy Manager 的工作方式相同。Cloudflare Tunnel 也一样。该项目提供了相关文档,而且完全不需要开放入站端口。
curl -sI https://gym.example.com | head -1此时应返回 HTTP/2 200,且不显示证书警告。现在在浏览器中打开网站,然后点击创建配置文件。如果出现 passkey 提示,随后登录报告 verification failed,说明 RP_ID 或 ORIGIN 与地址栏中的 URL 不匹配。修正 .env,然后再次运行 docker compose up -d,以重新创建容器,使其读取新值。docker compose restart 不会重新加载 .env。
禁止端口 8080 暴露到公网
默认情况下,Web 服务会在所有网络接口上发布 8080。因此,当代理在同一台服务器上提供 HTTPS 时,应用仍可通过服务器公网 IP 使用明文 HTTP 访问。防火墙规则无法解决此问题。Docker 会在 nat 表中通过 DNAT 规则发布端口,随后该流量会在 FORWARD 链中处理;Docker 自己的规则会接受流量,而 ufw 的规则位于 INPUT 路径上。因此,sudo ufw deny 8080/tcp 不会阻止任何流量。
解决方法是仅在回环地址上发布端口。Compose 文件映射 "${WEB_PORT:-8080}:${NGINX_PORT:-80}",因此你在 WEB_PORT 中设置的值会替换该映射左侧的内容;Docker 的简写语法允许在那里使用一个 ip:port 对。因此,WEB_PORT=127.0.0.1:8080 可以正常工作。
docker compose config
sudo ss -ltnp | grep 8080在合并后的配置中,应在 Web 服务的 ports 下看到 host_ip: 127.0.0.1。ss 应显示 127.0.0.1:8080,而不是 0.0.0.0:8080。从另一台机器访问时,curl http://<your-vps-ip>:8080 现在应被拒绝或超时,而 HTTPS 主机名仍可正常访问。
在配置个人资料后关闭注册
注册默认处于开放状态,访客模式也已启用。在公网主机名下,这意味着任何找到 URL 的人都可以在您的服务器上创建个人资料。请先注册您自己的个人资料,然后查找用户 ID:ls data/ 会为每位用户列出一个名为 state-<uid>.json 的文件,该 <uid> 就是所需的值。
ADMIN_UIDS=<your-uid>
INVITE_ONLY=1
ALLOW_GUEST=0再次运行 docker compose up -d。此时,Settings 中会显示 Admin dashboard,您可以在其中生成和撤销邀请代码。这样,只有与您一起训练的人员可以注册,其他人则无法注册。openGym 不支持外部身份提供商,因此这些邀请代码只管理此应用,不影响服务器上的其他服务;如果您希望为运行的所有服务向每个人分配一个统一账号,可以在前面部署 Authentik 作为 forward auth 代理,在 openGym 自带的 passkey 登录页面加载前先控制对该主机名的访问。
数据存放位置及其备份保护
所有数据都位于 ./data 目录中,并挂载到 API 容器的 /data。文件分为四类:db.json 存储用户资料和公开的 passkey 凭据,state-<uid>.json 存储单个用户的训练计划、锻炼记录和体重,secret 是会话 Cookie 密钥,vapid.json 存储首次运行时生成的推送通知密钥。
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api请先停止 API,因为 tar 复制文件时 API 可能正在写入文件,复制不完整的 JSON 文件恢复后会变成损坏的 JSON 文件。停止和启动大约需要两秒。然后将归档文件复制到服务器之外,因为存放在 VPS 上的归档无法在 VPS 故障后保留。备份时不要包含 media/:其中是 140 MB 的锻炼图片,媒体任务会重新下载这些图片,且无需额外费用。
恢复数据时,应将归档解压到同一路径,并由使用相同域名的主机提供服务。手机中存储的 passkey 与创建它时使用的 RP ID 绑定,因此恢复到新主机会得到一个可用的数据库,但用户无法登录。请保留原域名,否则需要重新注册所有 passkey。同样的原则也适用于其他服务;备份和升级 Docker Compose 堆栈介绍了通用流程。
MCP 服务器为只读,并在您的机器上运行
MCP(模型上下文协议)用于让 Claude Desktop 或 Cursor 等客户端与本地工具服务器通信。openGym 在 mcp/ 中提供了一个 MCP 服务器。它不属于 compose 文件,不是容器,也不监听任何端口。客户端将其作为子进程启动,并通过 stdio 与其通信,因此 README 才会说明它不会离开您的机器。
请在客户端运行的机器上安装,而不是安装在服务器上:
cd openGym/mcp
npm install然后将其添加到 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 是可选的,因为服务器会自动检测找到的唯一配置文件。它提供 8 个工具:list_routines、get_routine、get_week_plan、list_workouts、get_workout、get_bodyweight、estimate_1rm 和 muscle_balance。这些工具全部只读。它们都不会写入数据,因此助手可以回答您上周记录了什么训练,但不能记录训练组、编辑训练计划或删除任何内容。这份列表简要体现了智能体设计反复强调的一项原则:您公开的工具决定了模型能够执行的全部操作,而通过亲自编写循环来了解智能体的工作方式,是理解只读工具集为何是设计选择而非功能限制的最快方法。
VPS 用户需要解决以下问题。OPENGYM_DATA 是文件系统路径,而您的数据位于 VPS 上,AI 客户端位于笔记本电脑上。以下两种方案都明确处理了这一点。
- 将数据复制到本地,并让服务器指向副本:
rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/,然后将OPENGYM_DATA设置为~/opengym-data。服务器只读取数据,因此复制不会造成数据丢失。需要获取最新数据时,重新运行 rsync。 - 通过 ssh 运行服务器,将
command设置为ssh,并将args设置为["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]。此方案要求 VPS 已安装 Node,并且登录过程不能向 stdout 输出任何内容,因为 stdout 是协议通信通道。
这两个选项都假设代理本身运行在您的笔记本电脑上。如果您希望代理与数据运行在同一台服务器上,OneCLI 会为每个人在服务器上提供一个隔离的代理环境,这样通过 stdio 返回 data/ 的链路也会再次变为本地连接。
如果 cat data/db.json 返回 Permission denied,说明 API 容器以 root 身份写入了这些文件,而您的登录用户无权读取。请使用 sudo 复制这些文件,或在主机上修改其所有权。对于需要通过网络而不是 stdio 监听的服务器,请参阅在 VPS 上运行 MCP 服务器。
openGym 还是 wger:应该运行哪个?
wger 是这一领域中较成熟的选择,软件规模也大得多。它的 Compose 栈会在 nginx 后运行提供 Django 应用的 gunicorn、PostgreSQL、Redis 和 Celery worker。作为交换,您可以获得营养和食材跟踪、文档完善的 REST API、大型社区运动数据库,以及供教练管理他人训练计划的功能。
openGym 由两个容器和一个 JSON 文件目录组成,除 passkey 外无需管理任何账户。区别仅此而已。如果您曾经维护 Chatwoot 安装,知道备份意味着保存一份 Postgres dump 和 uploads 目录,而且每次升级版本都会运行数据库迁移,那么您已经了解维护 wger 所需面对的复杂度。
如果您希望在训练的同时跟踪饮食,或者需要基于 API 进行开发,请运行 wger。如果您希望整个栈小到可以在一个下午内从头读完,并且登录不使用可能泄露的密码,请运行 openGym。这种选择的代价是成熟度:截至 19 August 2026,openGym 的首个版本发布还不到一个月,而 wger 已经经过多年的版本发布。请固定版本,保留备份,并在每次更新前阅读发布说明。
如果您仍在决定哪些服务值得部署到这台服务器上,2026 年哪些服务值得自行托管介绍了其中的权衡;在同一台小型 VPS 上,这个应用也很适合与用于管理食谱的 Mealie或用于管理财务的 Actual Budget并列运行。
更新时不丢失任何内容
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api
git fetch --tags使用 git checkout v<new> 切换到所需的版本,然后运行 docker compose up -d --build,以便根据该标签重新构建容器。每次都应先备份,因为从磁盘上的 JSON 文件恢复时,只需执行一个 tar 命令,几秒钟即可完成。
FAQ
为什么 openGym 在手机上始终不显示通行密钥提示?
浏览器拒绝创建凭据,因为您使用的是 http:// 或裸 IP 地址,例如 http://192.168.1.20:8080。浏览器只允许在 HTTPS 源上使用通行密钥,唯一例外是 localhost。请将 openGym 放在反向代理后面,让代理使用真实主机名对应的有效证书;在 .env 中设置 RP_ID=gym.example.com 和 ORIGIN=https://gym.example.com,然后运行 docker compose up -d,使容器获取新值。如果提示出现,但登录报告 verification failed,说明这两个值与地址栏中的 URL 不完全一致。
openGym 将数据存储在哪里?如何备份?
数据位于 compose 文件旁边的 ./data 目录中,并作为 /data 挂载到 API 容器。该目录包含用于存储配置文件和公开通行密钥凭据的 db.json、每个用户一个的锻炼和体重数据文件 state-<uid>.json、会话 Cookie 密钥文件 secret,以及推送通知密钥文件 vapid.json。使用 docker compose stop api 进行备份,然后运行 tar czf ~/opengym-$(date +%F).tar.gz data/,再运行 docker compose start api,并将归档文件复制到服务器之外。跳过 media/;其中包含 140 MB 的锻炼图片,媒体任务会自行重新下载这些图片。
Claude 能读取我的 openGym 锻炼历史吗?
可以。它通过 mcp/ 目录中的可选 MCP server 读取数据,但只能读取。该 server 提供 8 个工具,涵盖训练计划、周计划、已记录的锻炼、体重、估算的单次最大重量和肌肉平衡,且这些工具都不会写回数据。它不是容器,也不会开放端口:客户端通过 stdio 启动它,并直接读取 OPENGYM_DATA 中的 JSON 文件。由于这里使用的是文件系统路径,在 VPS 上运行 openGym 时,您需要将 data/ 的副本同步到运行客户端的机器,或在客户端配置中通过 ssh 调用该 server。
应该自行托管 openGym 还是 wger?
如果您希望在训练记录旁边跟踪饮食和营养,或需要一个可用于构建应用的文档化 REST API,请选择 wger。它运行的组件更多:nginx 后面是 gunicorn、PostgreSQL、Redis 和 Celery worker。若您希望只运行两个容器、使用 cat 读取 JSON 文件,并通过无需管理密码的通行密钥登录,请选择 openGym。截至 19 August 2026,openGym 的第一个带标签版本发布仅 1 个月,因此每次更新前都应检出一个 git tag,并备份 data/。