SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-27

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

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

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

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

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

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

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

Планирование домена до первого входа в систему

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

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

Хуже того, RP ID жестко прописан в каждой учетной записи, которую уже зарегистрировали ваши пользователи. Если изменить RP_ID позже, ключи доступа, сохраненные на их устройствах, перестанут соответствовать домену, и никто не сможет войти в систему. Определите имя хоста заранее, направьте 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. В настройках теперь отображается панель администратора (Admin dashboard), где можно создавать и отзывать пригласительные коды. Благодаря этому регистрироваться смогут только те, с кем вы тренируетесь, и никто другой. openGym не поддерживает внешние провайдеры идентификации, поэтому эти пригласительные коды управляют только данным приложением и ничем другим на сервере. Если вы предпочитаете выдавать одну учетную запись на человека для всех ваших сервисов, использование Authentik в качестве прокси-сервера с forward auth ограничит доступ к хосту еще до того, как загрузится собственная система входа по passkey в 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 используется для протокола.

Оба варианта предполагают, что сам агент работает на вашем ноутбуке. Если вы хотите запускать его на том же сервере, где находятся данные, OneCLI предоставляет каждому пользователю изолированный агент на сервере, поэтому переход stdio обратно к data/ снова выполняется локально.

Если 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. В этом заключается основное различие. Если вам когда-либо приходилось поддерживать работу инсталляции Chatwoot, где резервное копирование означает создание дампа Postgres вместе с директорией загрузок, а каждое обновление версии требует миграции базы данных, вы уже понимаете, чего требует обслуживание wger.

Запускайте 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/ перед каждым обновлением.