openGymをVPSで自分でホストする方法
Docker ComposeでopenGymをVPSにデプロイする手順です。リリースタグの固定、初回Passkey前のTLS、JSONデータの保存場所、読み取り専用MCPサーバーの配置を解説します。
自分で openGym をホストすると得られるもの
リポジトリをクローンし、.env の 2 行を編集して、TLS(transport layer security)を終端するリバースプロキシの背後で 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 にリリースされました。約 1 か月で 13 個のタグが付けられているため、アプリケーションは現在も活発に変更されています。そのため、default branch にある時点の内容をそのままビルドするのではなく、リリースタグを確認してチェックアウトしてください。
初回ログイン前にドメインを決める
openGym へのサインインには Passkey を使用します。Passkey は relying party ID(RP 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 を bind mount します。そのため、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 .envREADME には、まだ github.com の clone URL が記載されています。このアドレスはすでに解決できません。上記の Gitea リポジトリが現在のプロジェクトの正式な場所です。
.env を編集します。VPS では、3 行が重要です。
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=127.0.0.1:8080RP_ID はホスト名だけの値で、ORIGIN は scheme を含む完全な URL です。アドレスバーの表示と完全に一致させてください。一致しないと、verification failed でログインに失敗します。WEB_PORT の値については、ポート 8080 を非公開にする方法のセクションで説明します。
docker compose up -d --build
docker compose ps
docker compose logs mediadocker compose ps では、web と api が running、media が終了コード 0 の exited になっていることを確認します。この終了は正常です。media job は処理が 1 回限りのダウンロードであるため、restart: "no" しています。ログの末尾には ✓ Exercise media ready で始まる行があり、ls media/img | wc -l は 0 ではなく数百程度を出力するはずです。ディレクトリが空の場合、ダウンロードに失敗しています。その場合、アプリは画像が空白の exercise card を表示します。
ここでは --build flag は必須です。compose ファイルは ghcr.io にある、現在は公開されていない prebuilt image を指定しています。そのため、docker compose pull は denied または manifest unknown で失敗し、代わりに2つの service は、先ほど clone した source から build されます。どちらにも、そのための build section があります。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 は pull に失敗せず、手元のソースからビルドします。マージが適用されたことを確認してから、そのタグで再ビルドします。
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=0docker compose up -d を再度実行します。Settings に Admin ダッシュボードが表示され、招待コードを生成および無効化できます。これにより、一緒にトレーニングする人だけが登録でき、それ以外の人は登録できなくなります。openGym は外部 ID プロバイダーを認識しません。そのため、これらの招待コードが制御するのはこのアプリだけで、サーバー上の他のサービスには影響しません。運用するすべてのサービスでユーザーごとに 1 つのアカウントを使いたい場合は、forward auth proxy として Authentik を前段に置くことで、openGym 固有の passkey ログインが読み込まれる前にホスト名へのアクセスを制御できます。
データの保存場所と、それを保護するバックアップ
すべてのデータは ./data ディレクトリにあり、API コンテナ内の /data にマウントされています。ファイルは 4 種類あります。db.json にはプロフィールと公開パスキー認証情報、state-<uid>.json にはユーザー 1 人分のルーティン、ワークアウト、体重、secret にはセッション Cookie の暗号鍵、vapid.json には初回起動時に生成されたプッシュ通知用の鍵が保存されます。
cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start apitar は API がファイルを書き込んでいる可能性がある状態でコピーするため、最初に API を停止します。JSON ファイルが途中までコピーされると、復元時に壊れた JSON ファイルになります。停止と起動には約 2 秒かかります。次に、アーカイブをサーバーの外部へコピーします。VPS 上に置いたアーカイブは、その VPS が失われると残りません。media/ はバックアップから除外します。これは 140 MB の運動画像で、メディアジョブが無料で再ダウンロードできます。
復元するには、同じドメインを提供するホスト上の同じパスへ展開します。スマートフォンに保存されたパスキーは、作成時の RP ID にスコープが限定されます。そのため、新しいホスト名へ復元すると、動作するデータベースがあっても誰もサインインできません。ドメインを維持するか、すべてのパスキーを再登録する計画を立ててください。同じ手順は、ほかの運用対象にも適用できます。一般的な手順については、Docker Compose スタックのバックアップとアップグレードを参照してください。
MCP サーバーは読み取り専用で、マシン上で実行されます
MCP(model context protocol)は、Claude Desktop や Cursor などのクライアントがローカルのツールサーバーと通信するための仕組みです。openGym には 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 クライアントが laptop にある場合、次の 2 つの方法が現実的です。
- データをローカルへコピーし、コピーをサーバーに指定します。
rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/を実行し、OPENGYM_DATAを~/opengym-dataに設定します。サーバーは読み取りのみを行うため、コピーによってデータが失われることはありません。最新のデータが必要なときに rsync を再実行します。 commandをsshに、argsを["-T", "user@gym.example.com", "OPENGYM_DATA=/opt/opengym/data node /opt/opengym/mcp/src/index.js"]に設定し、ssh 経由でサーバーを実行します。VPS に Node をインストールする必要があります。また、stdout に何も出力しないログイン設定が必要です。stdout はプロトコル用の通信チャネルだからです。
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 は 2 つのコンテナ、JSON ファイルを格納する 1 つのフォルダー、そして passkey 以外に管理すべきアカウントがない構成です。違いはこれだけです。
トレーニングと食事を一緒に記録したい場合や、構築の基盤にする API が必要な場合は wger を運用してください。午後のうちに全体を読み通せるほど小さなスタックと、漏えいするパスワードのないログインを求める場合は openGym を運用してください。この選択の代償は成熟度です。19 August 2026 時点で、openGym の最初のリリースから 1 か月しか経っていません。一方、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 --tagsgit checkout v<new> で目的のリリースをチェックアウトし、docker compose up -d --build を実行して、そのタグからコンテナを再ビルドします。毎回、最初にバックアップを取得します。ディスク上の JSON ファイルの復元は tar コマンド 1 つで実行でき、数秒で完了するためです。
FAQ
openGym がスマートフォンでパスキーのプロンプトを表示しないのはなぜですか?
ブラウザーが認証情報の作成を拒否しています。http:// 上、または http://192.168.1.20:8080 のような IP アドレスを直接指定してアクセスしているためです。ブラウザーがパスキーを許可するのは HTTPS オリジンだけで、例外は localhost のみです。実在するホスト名用の正式な証明書を設定したリバースプロキシの背後に openGym を配置し、.env で RP_ID=gym.example.com と ORIGIN=https://gym.example.com を設定してから、docker compose up -d を実行し、新しい値をコンテナに反映します。プロンプトは表示されるものの、ログイン時に verification failed と表示される場合は、その2つの値がアドレスバーの 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 の運動画像で、メディアジョブが自動的に再ダウンロードします。
Claude は openGym の運動履歴を読み取れますか?
はい。mcp/ ディレクトリにあるオプションの MCP server を介して読み取れます。ただし、読み取り専用です。ルーティン、週間プラン、記録済みの運動、体重、推定1RM、筋肉のバランスを扱う8つのツールを提供しますが、書き戻しを行うツールはありません。これはコンテナではなく、ポートも開きません。クライアントが stdio 経由で起動し、OPENGYM_DATA にある JSON ファイルを直接読み取ります。これはファイルシステムのパスを使用するため、VPS 上で openGym を実行する場合は、data/ のコピーをクライアント実行マシンへ同期するか、クライアントの設定から ssh 経由で server を呼び出します。
openGym と wger のどちらをセルフホストすべきですか?
トレーニング記録と併せて食事・栄養管理も行いたい場合や、構築の基盤にできる文書化された REST API が必要な場合は、wger を選びます。wger は、nginx の背後で gunicorn 上の Django、PostgreSQL、Redis、Celery worker を動かす、より大規模な構成です。2つのコンテナ、cat で読み取れる JSON ファイル、管理するパスワードが不要なパスキーによるログインを求める場合は、openGym を選びます。2026年8月19日時点では、openGym の最初のタグ付きリリースは公開から1か月しか経っていません。そのため、更新のたびに git tag を checkout し、data/ をバックアップしてください。