SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-23

如何用 Docker Compose 自行代管 openGym

依照版本標籤在 VPS 部署 openGym:先完成 TLS 再建立第一個 passkey,資料存於 clone 目錄,唯讀 MCP server 在 AI 用戶端主機運作。

自行代管 openGym 的內容

你可以複製儲存庫、編輯 .env 中的兩行,然後在負責 TLS termination(傳輸層安全性)的反向代理後方執行 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 tag,而不要直接建置 default branch 當下的內容。

在首次登入前先規劃網域

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
cp .env.example .env

README 仍顯示 github.com clone URL。該位址已無法解析,上方的 Gitea repository 才是目前有效的專案位置。

編輯 .env。在 VPS 上,以下 3 行很重要。

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

RP_ID 是不含方案的主機名稱,ORIGIN 則是包含方案的完整 URL。兩者必須與瀏覽器網址列完全一致,否則登入會因 verification failed 而失敗。WEB_PORT 的用途會在「保持 port 8080 為私有」一節說明。

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

docker compose ps 應顯示 webapi 正在執行,並顯示 media 已以代碼 0 結束。這是正確結果:media job 會因為工作是一次性下載而 restart: "no"。其日誌最後會有一行以 ✓ Exercise media ready 開頭,而 ls media/img | wc -l 應列出幾百個項目,不應是 0。目錄為空表示下載失敗,應用程式接著會產生圖片空白的運動卡片。

此處不能省略 --build 旗標。Compose 檔案會指定 ghcr.io 上已預先建置的映像,但這些映像已不再發布,因此 docker compose pull 會因 deniedmanifest unknown 而失敗,接著改用你剛才 clone 的原始碼建置這 2 個服務。這 2 個服務都各自包含 build 區段,正是為此用途。如果你不熟悉 Compose,請先閱讀 VPS 上的 Docker Compose,再回到這裡。

固定版本,因為這個專案仍處於早期階段

由於該 registry namespace 已經消失,沒有可供固定的 image tag。現在應固定的是磁碟上的 checkout,因為它決定容器內使用哪個應用程式版本。

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

git status 現在會回報該 tag 的 detached HEAD,這正是伺服器所需的狀態。在 checkout 其他版本之前,內容不會自行變更。

接著告訴 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 會使用現有的原始碼建置,而不會因 pull 失敗。確認合併已生效,再以該 tag 重新建置。

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_IDORIGIN 與網址列中的 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.1ss 應顯示 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 受建立該 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 為選用項目,因為 server 會自動偵測找到的唯一 profile。它提供 8 個工具:list_routinesget_routineget_week_planlist_workoutsget_workoutget_bodyweightestimate_1rmmuscle_balance。這些工具全部都是讀取操作。沒有任何工具會寫入資料,因此 assistant 可以回答您上週進行了哪些訓練,但無法記錄訓練組數、編輯訓練例程或刪除任何內容。

使用 VPS 的讀者需要處理以下問題。OPENGYM_DATA 是檔案系統路徑,但您的資料位於 VPS,而 AI 用戶端位於筆記型電腦。以下提供 2 個如實反映此架構的選項。

  1. 將資料複製到本機,再讓 server 指向該副本:rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/,接著將 OPENGYM_DATA 設為 ~/opengym-data。server 只會讀取資料,因此複製資料不會造成遺失。需要最新數據時,重新執行 rsync。
  2. 透過 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 是通訊協定通道。

如果 cat data/db.json 回傳 Permission denied,表示 API container 以 root 身分寫入這些檔案,而您的登入帳號無法讀取。請使用 sudo 複製檔案,或變更 host 上的檔案擁有權。若 server 必須透過網路而不是 stdio 監聽,請參閱 在 VPS 上執行 MCP server

openGym 與 wger:應該執行哪一個?

wger 是這個領域中較成熟的選項,也是規模大得多的軟體。它的 compose stack 會在 nginx 後方執行 gunicorn,提供 Django 應用程式,並搭配 PostgreSQL、Redis 及 Celery worker。相應地,您可以取得營養與食材追蹤、文件完整的 REST API、大型社群運動資料庫,以及讓教練管理他人訓練計畫的功能。

openGym 由兩個容器和一個 JSON 檔案資料夾組成,除了 passkeys 之外,不需要管理任何帳戶。差異就是這些。

如果您想在訓練之外追蹤飲食,或需要用來開發的 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> 取出所需的 release,然後執行 docker compose up -d --build,讓容器從該 tag 重新建置。每次都必須先備份,因為從磁碟上的 JSON 檔案還原時,只需執行一個 tar 命令,幾秒鐘即可完成。

FAQ

為什麼 openGym 從不在手機上顯示通行密鑰提示?

瀏覽器拒絕建立憑證,因為你使用的是 http:// 或裸 IP 位址,例如 http://192.168.1.20:8080。瀏覽器只允許在 HTTPS origin 上使用通行密鑰,唯一例外是 localhost。請將 openGym 放在具備正式憑證與正式主機名稱的反向代理後方,在 .env 設定 RP_ID=gym.example.comORIGIN=https://gym.example.com,然後執行 docker compose up -d,讓容器載入新值。如果出現提示,但登入回報 verification failed,表示這兩個值與網址列中的 URL 並未完全一致。

openGym 將資料儲存在哪裡?如何備份?

資料位於 compose 檔案旁的 ./data 目錄,並掛載至 API 容器中的 /data。其中包含用於個人資料與公開通行密鑰憑證的 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 的運動圖片,media job 會自行重新下載。

Claude 能讀取我的 openGym 訓練紀錄嗎?

可以,透過 mcp/ 目錄中的選用 MCP server 讀取,而且僅限讀取。它提供 8 個工具,涵蓋訓練課表、週計畫、已記錄的訓練、體重、估計單次最大重量與肌群平衡;這些工具都不會寫回資料。它不是容器,也不會開啟連接埠。你的 client 會透過 stdio 啟動它,並直接讀取 OPENGYM_DATA 中的 JSON 檔案。由於這是檔案系統路徑,在 VPS 上執行 openGym 時,必須將 data/ 的副本同步到執行 client 的機器,或在 client 設定中透過 ssh 呼叫 server。

我應該自行代管 openGym,還是使用 wger?

如果你希望在訓練紀錄旁追蹤食物與營養,或需要可據以開發的 REST API,請選擇 wger。它執行較大型的堆疊:由 nginx 代理的 gunicorn 上 Django、PostgreSQL、Redis 與 Celery worker。如果你希望使用 2 個容器、以 cat 讀取 JSON 檔案,並使用不需要管理密碼的通行密鑰登入,請選擇 openGym。截至 19 August 2026,openGym 的第一個標記版本發布至今僅 1 個月,因此每次更新前都應先切換至 git tag,並備份 data/