openGym 自托管部署:Docker Compose、TLS 与数据目录
了解如何在 VPS 上部署 openGym:检出固定 Git 标签,首次创建 Passkey 前配置 TLS,确认 JSON 数据位置,并使用只读 MCP 服务器。
自托管 openGym 的内容
要自托管 openGym,请先克隆代码仓库,编辑 .env 中的两行,然后在负责终止 TLS(传输层安全)的反向代理后运行 docker compose up -d --build。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。大约 1 个月内发布了 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.7现在,git 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,且不显示证书警告。现在在浏览器中打开网站,然后点击 Create profile。如果出现通行密钥提示,但随后登录报告 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 文件恢复后会损坏。停止和启动大约需要两秒。然后将归档文件复制到服务器之外,因为存放在 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 是协议通道。
如果 cat data/db.json 返回 Permission denied,说明 API 容器以 root 身份写入了这些文件,而您的登录用户无权读取它们。请使用 sudo 复制文件,或在主机上修改文件所有权。对于需要通过网络而不是 stdio 监听的服务器,请参阅 在 VPS 上运行 MCP 服务器。
openGym 还是 wger:应该运行哪个?
wger 是这一细分领域中较成熟的选择,软件规模也大得多。它的 compose stack 通过 nginx 运行 Gunicorn,为 Django 应用、PostgreSQL、Redis 和 Celery worker 提供服务。作为交换,您可以获得营养和食材跟踪、文档完善的 REST API、大型社区运动数据库,以及供教练管理他人训练计划的功能。
openGym 由两个容器和一个 JSON 文件目录组成。除 passkey 外,不需要管理任何用户账户。这就是两者的全部区别。
如果您希望在训练的同时跟踪饮食,或者需要基于 API 开发,请运行 wger。如果您希望使用一个小到可以在一个下午从头读完的 stack,并且登录时不需要担心密码泄露,请运行 openGym。这一选择的代价是成熟度:截至 19 August 2026,openGym 的首个 release 仅发布了一个月,而 wger 已经持续 release 了多年。请固定版本,保留备份,并在每次更新前阅读 release notes。
如果您还在决定服务器上哪些服务值得占用空间,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 目录中,并挂载到 API 容器的 /data。其中包括用于存储用户资料和公开通行密钥凭据的 db.json、每个用户一个的 workout 和体重数据文件 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 workout 历史吗?
可以。它通过 mcp/ 目录中的可选 MCP server 读取数据,但只能读取。该 server 提供 8 个工具,涵盖训练例程、周计划、已记录的 workout、体重、估算的单次最大重量和肌肉平衡;这些工具都不会写回数据。它不是容器,也不会打开端口:您的客户端通过 stdio 启动它,并直接读取 OPENGYM_DATA 中的 JSON 文件。由于这是文件系统路径,在 VPS 上运行 openGym 时,您需要将 data/ 的副本同步到运行客户端的计算机,或在客户端配置中通过 ssh 调用该 server。
我应该自行托管 openGym 还是 wger?
如果您希望在训练日志旁边记录饮食和营养数据,或需要一个可用于构建应用的文档完善的 REST API,请选择 wger。它运行的组件更多:由 nginx 反向代理的 gunicorn、Django、PostgreSQL、Redis 和 Celery worker。 如果您需要两个容器、可使用 cat 读取的 JSON 文件,以及无需管理密码的通行密钥登录,请选择 openGym。截至 19 August 2026,openGym 的第一个带标签版本发布仅一个月,因此每次更新前都应检出一个 git tag,并备份 data/。