Docker Compose для стека игрового сервера и базы данных
Стоит один раз попробовать поднять игровой сервер и его базу двумя отдельными docker run, чтобы понять — это быстро превращается в ад с ручным пробросом сети, забытыми флагами и вопросом "а где вообще лежат данные, если я снесу контейнер". Docker Compose решает это одним файлом: сервисы, сеть между ними, тома под персистентные данные и порядок запуска описаны декларативно, а поднимается всё одной командой. Разберём на практике — на примере связки FiveM-сервера с MySQL, но принципы те же для любого стека "игра + база".
Содержание
- Почему стек в одном compose-файле, а не два отдельных контейнера
- Базовый docker-compose.yml: игровой сервер + MySQL
- Сеть между контейнерами и как сервис находит базу по имени
- Volumes: что обязано пережить пересборку контейнера
- depends_on и healthcheck: порядок запуска без гонки условий
- .env и секреты: не хардкодим пароли в yml
Почему стек в одном compose-файле, а не два отдельных контейнера
Когда игровой сервер и база — просто соседние docker run без общего описания, накапливается несколько проблем сразу. Сеть: по умолчанию каждый docker run без явного указания сети попадает в дефолтный bridge, где контейнеры не резолвят друг друга по имени — придётся либо жёстко прописывать IP (который меняется при пересоздании), либо городить --link (устаревший подход). Порядок запуска: если сервер стартует раньше, чем СУБД готова принимать соединения, вы получите классическую гонку условий — плагин или скрипт фреймворка падает с ECONNREFUSED при первом же обращении к базе. И само описание стека перестаёт быть кодом — новый администратор не увидит одним взглядом, что входит в систему и как сервисы связаны.
docker-compose.yml закрывает все три проблемы разом:
- Сервисы автоматически попадают в общую пользовательскую сеть и резолвят друг друга по имени контейнера.
depends_on(особенно сcondition: service_healthy) задаёт явный порядок запуска.- Весь стек — это один файл в git, который можно поднять на новой машине командой
docker compose up -dбез ручного восстановления последовательности действий.
Если вы ещё сомневаетесь, нужен ли вам Docker вообще для игрового сервера — мы разбирали это отдельно в статье Docker-контейнер или классическая установка игрового сервера. Дальше исходим из того, что вы уже определились в пользу контейнеров и хотите связку с базой данных.
Базовый docker-compose.yml: игровой сервер + MySQL
Возьмём типичный случай — RP-сервер на FiveM с MySQL под ESX или QBCore. FXServer (движок FiveM) официального образа на Docker Hub не публикует, поэтому под него собирают свой образ через простой Dockerfile, который скачивает актуальный build с runtime.fivem.net. Структура проекта:
fivem-stack/
├── docker-compose.yml
├── .env
├── fivem/
│ ├── Dockerfile
│ └── entrypoint.sh
└── server-data/
└── server.cfg
Минимальный fivem/Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y curl xz-utils ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /fxserver
# Замените ссылку на актуальный build с runtime.fivem.net/artifacts/
RUN curl -L https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/<build>/fx.tar.xz \
-o fx.tar.xz \
&& tar xf fx.tar.xz && rm fx.tar.xz
COPY entrypoint.sh /fxserver/entrypoint.sh
RUN chmod +x /fxserver/entrypoint.sh
ENTRYPOINT ["/fxserver/entrypoint.sh"]
И сам docker-compose.yml:
services:
db:
image: mariadb:11
container_name: fivem-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/mysql
networks:
- fivem_net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
fivem:
build: ./fivem
container_name: fivem-server
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "30120:30120/tcp"
- "30120:30120/udp"
environment:
DB_CONNECTION_STRING: "mysql://${DB_USER}:${DB_PASSWORD}@db/${DB_NAME}?charset=utf8mb4"
volumes:
- ./server-data:/fxserver/server-data
networks:
- fivem_net
networks:
fivem_net:
driver: bridge
volumes:
db_data:
Обратите внимание на строку подключения к базе — хост указан не как localhost и не как IP, а как db, имя сервиса из этого же файла.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверСеть между контейнерами и как сервис находит базу по имени
Compose при первом запуске создаёт отдельную bridge-сеть для проекта (по умолчанию с именем <папка-проекта>_fivem_net, если явно не переопределить имя, как в примере выше) и подключает к ней все сервисы из файла. Внутри этой сети работает встроенный DNS Docker — каждый контейнер резолвится по имени сервиса, объявленному в docker-compose.yml, без всякой ручной настройки /etc/hosts или IP-адресов.
Поэтому в mysql_connection_string для oxmysql или в конфиге любого другого игрового сервиса хостом базы указывается именно db (имя сервиса), а не localhost и не 127.0.0.1 — это частая ошибка при переносе конфига с классической установки в Docker. На "голой" машине игра и MySQL сидели на одном хосте, поэтому localhost работал; в Compose это два разных сетевых namespace, и localhost внутри контейнера fivem указывает сам на себя, а не на контейнер db. Мы разбирали похожую путаницу с хостом подключения в статье про проблему подключения FiveM к базе MySQL — там же половина случаев по факту сводится именно к неверному хосту в connection string.
Наружу из этой внутренней сети торчат только порты, явно прописанные в ports: у сервиса fivem (30120 tcp/udp) — порт MySQL (3306) снаружи не публикуется вообще: базе не нужен доступ извне, если к ней обращается только сосед по сети. Принципы те же, что и при настройке портов и файрвола на классической установке, просто проброс идёт через ports: вместо iptables напрямую.
Если сервисов больше двух (добавляется txAdmin, Redis для кэша сессий или panel-контейнер), их достаточно перечислить в том же файле и указать ту же сеть — DNS-резолвинг по имени сервиса заработает между всеми ними без дополнительных настроек.
Volumes: что обязано пережить пересборку контейнера
Ключевое правило контейнеризации: сам контейнер эфемерен, а данные, которые должны пережить docker compose down и пересборку образа, обязаны лежать в volume, смонтированном с хоста. В примере это касается двух вещей:
db_data:/var/lib/mysql— именованный volume, в котором MariaDB хранит все таблицы. Без него при пересоздании контейнераdbвы теряете всю базу — права игроков, инвентарь, экономику, если это RP-сервер на ESX/QBCore../server-data:/fxserver/server-data— bind mount с хоста, где лежатserver.cfg, ресурсы (скрипты, карты, MLO) и прочие файлы конфигурации сервера.
Разница не только синтаксическая. Именованный volume управляется самим Docker и физически лежит в /var/lib/docker/volumes/, неудобно для ручного просмотра — зато Docker сам заботится о правах доступа, это стандарт для баз данных. Bind mount — прямая папка на хосте, куда можно зайти обычным cd и nano, что удобнее для конфигов, которые вы правите руками.
Проверить, что данные сохранились после пересоздания:
docker compose down # останавливает и удаляет контейнеры
docker compose up -d # создаёт заново
docker compose exec db mysql -u root -p -e "SHOW DATABASES;"
Если нужная база на месте — volume настроен верно. Отдельно напомним: volume с данными MySQL — это не бэкап, а просто персистентное хранилище на том же диске. Если диск умрёт, db_data умрёт вместе с ним. Регулярный mysqldump (запускается тем же способом, что и на классической установке — через docker compose exec db mysqldump ...) и общие принципы автобэкапа по расписанию разобраны в статье про автобэкапы игрового сервера — команда снятия дампа просто выполняется через docker compose exec, а не напрямую в шелле хоста.
depends_on и healthcheck: порядок запуска без гонки условий
Простой depends_on: [db] без условия решает только порядок запуска процесса контейнера, но не готовность сервиса внутри него. MariaDB может минуту-другую инициализировать файлы данных при первом старте, и всё это время процесс mysqld уже "запущен" с точки зрения Docker, но ещё не принимает соединения. Если fivem стартует в этот момент, oxmysql получит ECONNREFUSED и либо упадёт, либо будет ретраить с задержкой.
Именно для этого в примере выше у сервиса db задан healthcheck, а у fivem — depends_on с условием service_healthy, а не просто списком имён:
depends_on:
db:
condition: service_healthy
Compose будет ждать, пока healthcheck контейнера db не вернёт успех (в примере — пока mysqladmin ping не начнёт отвечать), и только после этого запустит fivem. Параметр start_period: 30s в healthcheck говорит Docker не засчитывать первые 30 секунд неудачных проверок как настоящий сбой — это время как раз на инициализацию MariaDB при первом запуске.
Посмотреть текущий статус здоровья сервисов:
docker compose ps
В колонке статуса появится healthy/unhealthy/starting — если db завис на starting дольше пары минут, стоит посмотреть логи:
docker compose logs db
Без condition: service_healthy Compose гарантирует только то, что контейнер db был запущен раньше fivem, но не то, что MySQL внутри него уже принимает подключения — это частый источник "сервер иногда не стартует с первого раза, а после restart всё работает". Healthcheck закрывает эту гонку условий системно, а не через sleep в entrypoint-скрипте.
.env и секреты: не хардкодим пароли в yml
В примерах выше пароли и имена базы вынесены в переменные ${DB_ROOT_PASSWORD}, ${DB_USER} и так далее — Compose автоматически подхватывает файл .env из той же папки, где лежит docker-compose.yml, без дополнительной настройки:
DB_ROOT_PASSWORD=SuperSecretRoot2026
DB_USER=fivem_user
DB_PASSWORD=SuperSecretPass2026
DB_NAME=fivem_db
Почему это важнее, чем кажется: сам docker-compose.yml часто хочется положить в git (это и есть основной плюс — конфигурация как код), а .env с реальными паролями — нет. Добавьте .env в .gitignore и держите в репозитории только .env.example с именами переменных без значений — структура стека документирована, а секреты не утекают в историю коммитов.
Если пароль содержит спецсимволы (@, #, :, /), которые ломают парсинг URL-подобных connection string у некоторых ресурсов (та же проблема, что и на классической установке), проще сгенерировать пароль без них, чем экранировать вручную: openssl rand -hex 16 даёт 32 символа hex без единого символа, требующего экранирования. Общие принципы работы с учётными данными и правами пользователя базы под игровые плагины разобраны в статье про MySQL для игровых плагинов — оттуда же принцип "отдельный пользователь на базу, а не root", который в контейнерном сценарии работает так же.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Обязательно ли использовать именно MariaDB, а не MySQL?
Нет, image: mysql:8 работает так же — синтаксис compose и healthcheck (mysqladmin ping) идентичны для обоих образов. MariaDB чаще выбирают из-за более лёгкого образа и полной протокольной совместимости с игровыми плагинами.
Можно ли добавить в стек ещё Redis или txAdmin?
Да, добавьте блок services: с нужным образом и подключите к той же сети (networks: [fivem_net]) — контейнеры смогут обращаться друг к другу по имени сервиса, как и к db.
Что будет при docker compose down -v вместо обычного down?
Флаг -v удаляет ещё и именованные volumes — в примере это вся база из db_data. Используйте его осознанно и никогда на проде без свежего бэкапа.
Нужен ли healthcheck и для самого игрового сервиса, не только для базы?
Можно добавить, если хотите, чтобы Docker сам перезапускал контейнер при зависании процесса. Но для порядка запуска важнее healthcheck именно у db, потому что игра обращается к базе один раз, в самом начале.
fivem падает с ошибкой подключения к базе после docker compose up -d — что проверить в первую очередь?
Что хост в connection string совпадает с именем сервиса (db), а не localhost, и что оба сервиса в одной сети (docker network inspect <имя_сети> покажет подключённые контейнеры). Это самая частая причина.