SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-21

Как развернуть openGym на VPS через Docker Compose

Пошаговое руководство по установке openGym на сервер. Узнайте, как настроить TLS до первого входа по Passkey, где хранятся JSON-данные и как правильно запустить read-only MCP сервер.

Что вы получаете при самостоятельном хостинге openGym

Вы разворачиваете openGym самостоятельно, клонируя репозиторий, изменяя две строки в .env и запуская docker compose up -d --build за обратным прокси-сервером, который выполняет TLS termination (безопасность транспортного уровня). openGym — это трекер тренировок и веса тела: еженедельные планы, тренировки с инструкциями, логирование каждого подхода и отслеживание веса во времени. Проект распространяется по лицензии AGPL-3.0 и хранит все данные в обычных JSON-файлах на диске, поэтому запускать сервер базы данных не требуется.

Стек состоит из двух постоянно работающих контейнеров: контейнера nginx, который отдает сборку React, и контейнера Node, содержащего API, а также однократной задачи, которая загружает около 140 МБ изображений и GIF-анимаций упражнений при первом запуске.

Две вещи, которые README проекта подразумевает, но не разъясняет для тех, кто развертывает приложение на публичном сервере. Вход по Passkey привязан к имени хоста, поэтому домен и его сертификат должны существовать до первого входа, а не после. А опциональный сервер MCP работает только в режиме чтения и запускается на машине, где работает ваш AI-клиент, а не внутри стека, что меняет порядок действий, когда данные находятся на VPS.

openGym — молодой проект. Первый помеченный релиз, v1.0.0, датирован 20 июля 2026 года, а v1.2.7 вышел 18 августа 2026 года. Тринадцать тегов примерно за месяц означают, что приложение активно развивается, поэтому выбирайте тег релиза, а не собирайте то, что находится в ветке по умолчанию.

Планирование домена перед первым входом

Для входа в openGym используются passkeys. Passkey привязан к идентификатору полагающейся стороны (RP ID) — это домен, на котором были созданы учетные данные. Браузеры создают passkeys только при работе по протоколу HTTPS. Единственным исключением является localhost.

Это создает проблему, с которой пользователи сталкиваются при работе с мобильных устройств. Откройте http://203.0.113.10:8080 с другого устройства, и запрос на использование passkey не появится, так как браузер отказывается создавать учетные данные для источника с обычным HTTP или для прямого IP-адреса. В примечаниях по устранению неполадок проекта указано то же самое: отсутствие запроса означает, что вы используете http:// или IP-адрес.

Хуже того, RP ID жестко прописан в каждом наборе учетных данных, которые уже зарегистрировали ваши пользователи. Если изменить RP_ID позже, passkeys, сохраненные на их устройствах, перестанут соответствовать, и никто не сможет войти в систему. Определите имя хоста заранее, направьте DNS на VPS и настройте сертификат до того, как кто-либо нажмет кнопку Create profile.

Развертывание openGym с помощью Docker Compose

Файл compose использует bind-mount для ./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 .env

В README все еще указан URL для клонирования github.com. Этот адрес больше не разрешается, а репозиторий Gitea, указанный выше, является актуальным домом проекта.

Отредактируйте .env. На VPS важны три строки.

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 описано в разделе о том, как сделать порт 8080 приватным.

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

docker 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, начните с Docker Compose на VPS и вернитесь сюда.

Закрепление версии, так как проект находится на ранней стадии

Поскольку пространство имен в реестре было удалено, не осталось тегов образов для закрепления. Вместо этого следует закрепить состояние репозитория на диске, так как именно оно определяет, какая версия приложения попадет в контейнер.

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. Если появляется запрос passkey, а затем при входе отображается verification failed, RP_ID или ORIGIN не совпадает с URL в адресной строке. Исправьте .env и снова выполните docker compose up -d, что приведет к пересозданию контейнеров для считывания новых значений. Команда docker compose restart не перезагружает .env.

Ограничение доступа к пор 8080 из публичной сети

По умолчанию веб-сервис публикует 8080 на всех интерфейсах, поэтому приложение доступно по обычному HTTP через ваш публичный IP-адрес, в то время как прокси-сервер обслуживает HTTPS на той же машине. Правило межсетевого экрана не решает эту проблему. Docker публикует порт с помощью правила DNAT в таблице nat, и этот трафик затем обрабатывается в цепочке 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

В объединенной конфигурации, в разделе 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. В разделе настроек появится панель администратора, где можно создавать и отзывать пригласительные коды. Это позволит регистрироваться только тем, с кем вы тренируетесь. openGym не поддерживает внешние провайдеры идентификации, поэтому пригласительные коды действуют только в рамках этого приложения. Если вы предпочитаете использовать единую учетную запись для всех сервисов на сервере, использование Authentik в качестве forward auth proxy ограничит доступ к хосту до того, как загрузится форма входа openGym.

Где хранятся данные и как работает их резервное копирование

Все данные находятся в каталоге ./data, который примонтирован в контейнер API по пути /data. Существует четыре типа файлов: db.json содержит профили и публичные учетные данные passkey, state-<uid>.json хранит рутины, тренировки и вес одного пользователя, secret — это ключ сессионных cookie, а vapid.json содержит ключи для push-уведомлений, которые генерируются при первом запуске.

cd /opt/opengym
docker compose stop api
tar czf ~/opengym-$(date +%F).tar.gz data/
docker compose start api

Сначала остановите API, так как tar копирует файлы в то время, когда API может записывать данные, а частично скопированный JSON-файл при восстановлении окажется поврежденным. Остановка и запуск занимают около 2 секунд. Затем скопируйте архив с сервера, поскольку архив, хранящийся на VPS, не переживет выход VPS из строя. Исключите media/ из резервной копии: это 140 МБ изображений упражнений, которые задача обработки медиафайлов при необходимости загрузит заново.

Восстановление подразумевает распаковку архива по тому же пути на хосте, обслуживающем тот же домен. Ключ passkey, сохраненный на вашем телефоне, привязан к RP ID, для которого он был создан, поэтому восстановление на новом имени хоста даст вам рабочую базу данных, в которую никто не сможет войти. Сохраняйте домен или будьте готовы заново регистрировать каждый passkey. Тот же принцип применим ко всему остальному, что вы запускаете, а резервное копирование и обновление стека 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 необязателен при однопользовательской установке, так как сервер автоматически обнаруживает единственный найденный профиль. Он предоставляет восемь инструментов: list_routines, get_routine, get_week_plan, list_workouts, get_workout, get_bodyweight, estimate_1rm и muscle_balance. Все они работают только на чтение. Инструменты не выполняют запись, поэтому ассистент может ответить, какие веса вы использовали на прошлой неделе, но не может записать подход, изменить программу тренировок или удалить данные.

Вот задача, которую необходимо решить пользователю VPS. OPENGYM_DATA — это путь в файловой системе, а ваши данные находятся на VPS, в то время как AI-клиент запущен на ноутбуке. Предлагаются два честных способа решения.

  1. Скопируйте данные локально и укажите серверу путь к копии: rsync -a --delete user@gym.example.com:/opt/opengym/data/ ~/opengym-data/, затем установите OPENGYM_DATA в значение ~/opengym-data. Сервер только читает данные, поэтому копия ничего не теряет. Повторно запускайте rsync, когда потребуются актуальные показатели.
  2. Запустите сервер через 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, см. запуск MCP-серверов на VPS.

openGym или wger: что выбрать для запуска?

wger — это признанный вариант в данной нише, представляющий собой гораздо более крупное программное решение. Его стек compose включает gunicorn для обслуживания приложения на Django, PostgreSQL, Redis и воркер Celery за nginx. Взамен вы получаете инструменты для отслеживания питания и ингредиентов, документированный REST API, обширную базу упражнений сообщества и функции для тренеров, управляющих планами других пользователей.

openGym состоит из двух контейнеров, папки с JSON-файлами и не требует администрирования учетных записей, кроме использования passkeys. В этом заключается вся разница.

Запускайте wger, если хотите отслеживать питание наряду с тренировками или если вам нужен API для разработки. Запускайте openGym, если вам нужен стек, который достаточно мал, чтобы изучить его целиком за один вечер, и авторизация без паролей, которые можно скомпрометировать. Цена такого выбора — зрелость проекта: по состоянию на 19 августа 2026 года первому релизу openGym всего месяц, в то время как за wger стоят годы обновлений. Фиксируйте версии, делайте резервные копии и читайте примечания к выпуску перед каждым обновлением.

Если вы все еще решаете, что стоит разместить на сервере, в статье что стоит хостить самостоятельно в 2026 году рассматриваются все плюсы и минусы, а это приложение отлично соседствует с Mealie для рецептов или Actual Budget для финансов на одном и том же небольшом VPS.

Обновление без потери данных

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 не запрашивает passkey на моем телефоне?

Браузер отказывается создавать учетные данные, так как вы используете http:// или прямой IP-адрес, например http://192.168.1.20:8080. Браузеры разрешают использование passkey только для HTTPS-источников, за единственным исключением в виде localhost. Разместите openGym за reverse proxy с действительным сертификатом для реального доменного имени, укажите RP_ID=gym.example.com и ORIGIN=https://gym.example.com в .env и выполните docker compose up -d, чтобы контейнеры применили новые значения. Если запрос появляется, но при входе возникает ошибка verification failed, значит, эти два значения не совпадают в точности с URL в адресной строке.

Где openGym хранит данные и как их резервировать?

В каталоге ./data рядом с файлом compose, который монтируется в контейнер API как /data. Там содержатся db.json для профилей и публичных ключей passkey, по одному файлу state-<uid>.json на каждого пользователя для тренировок и веса тела, secret для ключа сессионных cookie и vapid.json для ключей push-уведомлений. Для резервного копирования используйте docker compose stop api, затем tar czf ~/opengym-$(date +%F).tar.gz data/ и docker compose start api, после чего скопируйте архив с сервера. Пропускайте media/: это 140 МБ изображений упражнений, которые медиа-задача загрузит повторно самостоятельно.

Может ли Claude читать историю моих тренировок в openGym?

Да, через опциональный MCP-сервер в каталоге mcp/, только в режиме чтения. Он предоставляет восемь инструментов для работы с программами, недельными планами, записанными тренировками, весом тела, расчетным максимумом на одно повторение и балансом мышц; запись данных не поддерживается. Это не контейнер, он не открывает порты: ваш клиент запускает его через stdio, и сервер напрямую читает JSON-файлы по пути OPENGYM_DATA. Поскольку это путь в файловой системе, при запуске openGym на VPS необходимо либо синхронизировать копию data/ на машину с клиентом, либо вызывать сервер через ssh в конфигурации клиента.

Что выбрать для self-hosting: openGym или wger?

Выбирайте wger, если вам нужно отслеживание питания и диеты помимо журнала тренировок или задокументированный REST API для разработки. Он использует более крупный стек: Django под управлением gunicorn, PostgreSQL, Redis и Celery worker за nginx. Выбирайте openGym, если вам достаточно двух контейнеров, JSON-файлов, которые можно прочитать с помощью cat, и входа через passkey без необходимости управлять паролями. По состоянию на 19 августа 2026 года первому релизу openGym исполнился месяц, поэтому используйте git tag и делайте резервную копию data/ перед каждым обновлением.