openGym 自行託管:Docker Compose 與 Passkey 設定
使用 Docker Compose 將 openGym 部署到 VPS:固定 Git 標記版本、首次 Passkey 前完成 TLS,掌握純 JSON 資料位置,以及在 AI 用戶端主機執行唯讀 MCP 伺服器。
自行託管 openGym 的內容
您可以複製儲存庫、編輯 .env 中的兩行,然後在終止 TLS(傳輸層安全性)的反向代理後方執行 docker compose up -d --build,以自行託管 openGym。openGym 是健身與體重追蹤器,提供每週計畫、引導式訓練、每組訓練記錄,以及體重隨時間的變化。它採用 AGPL-3.0 授權,所有資料都以純 JSON 檔案儲存在磁碟上,因此不需要執行資料庫伺服器。
此堆疊包含 2 個長時間執行的容器:提供 React 建置檔的 nginx 容器,以及負責 API 的 Node 容器;另外還有一個僅執行一次的工作,首次啟動時會下載約 140 MB 的運動圖片與 GIF。
專案的 README 提到 2 件事,但沒有明確說明部署到公開伺服器時的注意事項。Passkey 登入繫結至主機名稱,因此網域與其憑證必須在首次登入前就緒,而不是登入後才設定。選用的 MCP 伺服器是唯讀的,並且執行在 AI 用戶端所在的機器上,而不是此堆疊內。當資料位於 VPS 時,這會改變所需的設定方式。
openGym 仍處於早期階段。第一個標記版本 v1.0.0 的日期是 20 July 2026,v1.2.7 於 18 August 2026 發布。第一個月就有 13 個標記版本;截至 9 October 2026 已有 34 個標記版本,且 v1.4.0 於當天建立。這表示應用程式仍在快速變動,因此請切換至某個發布標記,而不要直接建置預設分支當下的內容。
首次登入前先規劃網域
Passkey 是登入 openGym 的方式。Passkey 會綁定至 relying party ID(RP ID),也就是建立該憑證的網域。瀏覽器只會透過 HTTPS 建立 passkey。唯一的例外是 localhost。
這會造成使用者在手機上遇到問題。從其他裝置開啟 http://203.0.113.10:8080 時,完全不會出現 passkey 提示,因為瀏覽器拒絕在純 HTTP origin 或單獨的 IP 位址上建立憑證。專案本身的疑難排解說明也指出相同情況:沒有提示表示你使用的是 http:// 或 IP。
更嚴重的是,RP ID 會寫入使用者已註冊的每個憑證。之後如果變更 RP_ID,儲存在使用者裝置上的 passkey 就不再相符,任何人都無法登入。先決定主機名稱,將 DNS 指向 VPS,並在任何人點選 Create profile 前完成憑證設定。
使用 Docker Compose 部署 openGym
Compose 檔案會將 ./data 和 ./media 以相對於該檔案的路徑繫結掛載,因此你 clone 到的目錄就是資料庫。請將它放在持久儲存位置。
sudo install -d -o "$USER" -g "$USER" /opt/opengym
git clone https://gitea.com/DuarteSantos/openGym /opt/opengym
cd /opt/opengym
git checkout v1.2.7
cp .env.example .envREADME 會列出 github.com/DuarteSantos8/openGym clone URL。截至 9 October 2026,該 repository 已恢復解析,並將自身描述為專案的主要位置;上方的 Gitea repository 仍持續更新,且包含所有 tag,因此兩者皆可使用。第一次建置前,請先切換至指定 tag:default branch 已超過本指南說明的版本。在該分支中,exercise 動畫已包含在 image 內,且下方的 media job 已不存在。關於 pinning 的原因,請參閱相關章節。
編輯 .env。在 VPS 上有三行設定很重要。
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080RP_ID 是不含 scheme 的主機名稱,ORIGIN 是包含 scheme 的完整 URL。兩者必須與瀏覽器網址列完全相同,否則登入會因 verification failed 而失敗。WEB_PORT 的用途,請參閱將 port 8080 維持為私有的章節。
docker compose up -d --build
docker compose ps
docker compose logs mediadocker compose ps 應顯示 web 和 api 正在執行,並顯示 media 已以 code 0 結束。這個退出狀態是正確的:media job 的工作是一次性的下載,因此已 restart: "no"。其 log 應以 ✓ Exercise media ready 開頭的行結束,而 ls media/img | wc -l 應輸出 1,324,而不是 0;這是截至 9 October 2026,上游 exercise dataset 提供的 thumbnail 數量。空目錄表示下載失敗,應用程式之後會以空白圖片呈現 exercise card。
請保留 --build flag。此 tag 的 Compose 檔案會使用 ghcr.io 上以 latest tag 發布的預建 image。這些 image 截至 9 October 2026 已再次發布,但 latest 會跟隨最新 release,因此 docker compose pull 執行的 code 會比你剛 checkout 的 source 新。兩個 service 都包含 build 區段,而 --build 會改為從你的 checkout 建置。如果你還不熟悉 Compose,請先閱讀 VPS 上的 Docker Compose,再回到這裡。
固定版本,因為此專案仍處於早期階段
registry 再次提供帶有版本的映像檔;截至 9 October 2026,其中也包含 1.2.7 標籤。但 v1.2.7 的 compose 檔案仍要求 latest,而該標籤在當天指向 1.3.10。需要固定的是磁碟上的 checkout,因為它決定容器中最終使用的應用程式版本。
cd /opt/opengym
git fetch --tags
git checkout v1.2.7git status 現在會顯示該標籤上的 detached HEAD,這正是伺服器所需的狀態。在你 checkout 其他版本前,目前版本不會變動。
v1.2.7 是本指南撰寫時所依據的版本,現在已不是最新版本。後續版本會變更部署本身(在 default branch 上,media job 已移除),因此變更固定版本前,請先閱讀 release notes,並比較 compose 檔案。
接著讓 Compose 完全停止從 registry 取得映像檔。將以下內容放入 docker-compose.override.yml;Compose 會自動載入此檔案,並將其合併到受追蹤的檔案之上。純量鍵會由 override 檔案取代,因此不需要修改 git 中的內容,git pull 也能保持乾淨。如需完整的合併規則,請參閱Compose 如何合併 override 檔案。
services:
api:
pull_policy: build
web:
pull_policy: build完成後,稍後執行的 docker compose up -d 會使用現有的原始碼建置,而不是拉取該日 latest 所指向的內容。請先確認合併已生效,再以該標籤重新建置。
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。如果出現 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 因此無法封鎖任何流量。
修正方式是只繫結至 loopback 位址發布連接埠。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 proxy,在 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 的運動圖片,media job 會再次免費下載這些圖片。
還原時,請在提供相同網域的主機上,將封存檔解開至相同路徑。儲存在手機上的 passkey 受建立時使用的 RP ID 限制,因此還原至新的主機名稱後,雖然資料庫可正常運作,卻沒有任何人能登入。請保留原有網域,或規劃重新註冊所有 passkey。同樣的原則也適用於其他服務;備份與升級 Docker Compose stack 說明一般作業流程。
MCP server 僅供讀取,且在你的電腦上執行
MCP(model context protocol)是 Claude Desktop 或 Cursor 等用戶端與本機工具伺服器通訊的方式。openGym 在 mcp/ 中隨附一個 MCP server。它不屬於 compose file,也不是 container,且不監聽任何埠。用戶端會將它作為子程序啟動,並透過 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 可省略,因為伺服器會自動偵測找到的唯一 profile。目前的預設分支公開九個工具:list_routines、get_routine、preview_session、get_week_plan、list_workouts、get_workout、get_bodyweight、estimate_1rm 和 muscle_balance。preview_session 會顯示你開始執行 routine 時,應用程式實際顯示的內容;此內容已套用 progression policy 和你的歷史紀錄,可能不同於原始計畫。v1.2.7 tag 只隨附另外八個工具,因此從固定版本 checkout 執行的 server 不會有 preview_session。這些工具全部只會讀取資料。它們不會寫入任何資料,因此 assistant 可以回答你上週臥推了多少,但無法記錄一組訓練、編輯 routine 或刪除任何內容。這份清單簡潔呈現了 agent design 一再回到的一項決策:你公開的工具就是 model 能執行的全部操作;而透過親自撰寫迴圈來了解 agent 的運作方式,是最快看出唯讀工具集屬於設計選擇而非限制的方法。
VPS 使用者需要處理以下問題。OPENGYM_DATA 是 filesystem path,但你的資料位於 VPS,而 AI client 位於筆記型電腦。以下兩個選項都如實反映這項差異。
- 將資料複製到本機,並讓 server 指向複本:
rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/,然後將OPENGYM_DATA設為~/opengym-data。server 只會讀取資料,因此複本不會遺失任何內容。需要最新數據時,再次執行 rsync。 - 透過 ssh 執行 server,將
command設為ssh,並將args設為["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]。這需要在 VPS 上安裝 Node,且登入程序不得在 stdout 輸出任何內容,因為 stdout 是協定通道。
這兩個選項都假設 agent 本身在筆記型電腦上執行。如果你希望 agent 與資料位於同一台機器,OneCLI 會在 server 上為每位使用者提供隔離的 sandbox agent,如此一來,回到 data/ 的 stdio 傳輸就會再次成為本機通訊。
如果 cat data/db.json 回傳 Permission denied,表示 API container 以 root 身分寫入這些檔案,而你的登入帳號無法讀取。請使用 sudo 複製檔案,或在 host 上變更檔案擁有者。若伺服器預期透過 network 而非 stdio 監聽,請參閱在 VPS 上執行 MCP server。
openGym 與 wger:應該執行哪一個?
wger 是這個領域中較成熟的選項,也是規模大得多的軟體。它的 compose stack 會在 nginx 後方執行 gunicorn,提供 Django 應用程式,並搭配 PostgreSQL、Redis 與 Celery worker。相對地,您會取得營養與食材追蹤、文件完整的 REST API、大型社群運動資料庫,以及供教練管理他人訓練計畫的功能。
openGym 由兩個容器和一個 JSON 檔案資料夾組成,除了 passkeys 外,沒有需要管理的帳號。差異就只有這些。如果您曾經 維護 Chatwoot 安裝環境,知道備份代表保存一份 Postgres dump 和 uploads 目錄,而且每次升級版本都會執行資料庫遷移,那麼您已經了解維護 wger 所需面對的架構。
如果您想在訓練之外追蹤飲食,或需要用來建置功能的 API,請執行 wger。如果您希望 stack 小到能在一個下午從頭讀完,且登入不使用可能外洩的密碼,請執行 openGym。這項選擇的代價是成熟度:截至 19 August 2026,openGym 的第一個版本才發布一個月,而 wger 已經累積多年的版本。請固定版本、保留備份,並在每次更新前閱讀 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:// 或未經 DNS 解析的 IP 位址,例如 http://192.168.1.20:8080。瀏覽器只允許在 HTTPS origin 上使用通行密鑰,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,而且僅限讀取。在目前的預設分支中,它提供九個工具(v1.2.7 中為八個),涵蓋訓練課表、下一次訓練將安排內容的預覽、每週計畫、已記錄的訓練、體重、估計單次最大重量與肌群平衡;這些工具都不會回寫資料。它不是容器,也不會開啟連接埠。您的用戶端會透過 stdio 啟動它,而它會直接讀取 OPENGYM_DATA 中的 JSON 檔案。由於這是檔案系統路徑,在 VPS 上執行 openGym 時,您必須將 data/ 的副本同步到執行用戶端的電腦,或在用戶端設定中透過 ssh 呼叫伺服器。
我應該自行託管 openGym 還是 wger?
如果您希望在訓練紀錄旁追蹤食物與營養,或需要可據以開發的 REST API,請選擇 wger。它執行較大型的堆疊:由 nginx 代理的 gunicorn 上 Django、PostgreSQL、Redis 和 Celery worker。如果您需要兩個容器、可使用 cat 讀取的 JSON 檔案,以及不必管理密碼的通行密鑰登入,請選擇 openGym。截至 19 August 2026,openGym 的第一個標記版本發布至今僅一個月,因此每次更新前請先切換至 git tag,並備份 data/。