// DevOps

Как устроен деплой и панели управления: phpMyAdmin и RabbitMQ за nginx

Опубликовано 29.05.2026

Как устроен деплой и панели управления: phpMyAdmin и RabbitMQ за nginx

В этой заметке разберём практическую схему деплоя приложения через GitHub Actions и Docker Compose, а также подключение административных панелей phpMyAdmin и RabbitMQ через единый nginx-gateway.

Основная идея простая: наружу смотрит только nginx, а все внутренние сервисы — API, frontend, база данных, phpMyAdmin, RabbitMQ и воркеры — остаются во внутренней Docker-сети. При этом деплой полностью автоматизирован: после push в main CI собирает окружение, синхронизирует код на сервер, пересобирает контейнеры, запускает миграции и масштабирует воркеры.

Публичные домены, имена серверов и пути в статье анонимизированы. В примерах используются example.com, api.example.com, pma.example.com, rmq.example.com и условный путь /srv/project.


Общая схема

GitHub (push → main)
  GitHub Actions
  ┌─────────────────────────────┐
  │ 1. Сборка .env              │
  │ 2. Генерация .htpasswd      │
  │ 3. rsync → сервер           │
  │ 4. docker compose build     │
  │ 5. docker compose up -d     │
  │ 6. Миграции                 │
  │ 7. nginx -s reload          │
  │ 8. Масштабирование workers  │
  └─────────────────────────────┘
    Сервер приложения
  ┌──────────────────────────────────────────┐
  │  gateway (nginx)  ← единственный вход    │
  │  ├── example.com       → frontend        │
  │  ├── api.example.com   → api-php-fpm     │
  │  ├── pma.example.com   → phpmyadmin      │
  │  └── rmq.example.com   → rabbitmq:15672  │
  └──────────────────────────────────────────┘

Здесь gateway — это контейнер с nginx, который принимает HTTP/HTTPS-трафик и проксирует запросы во внутреннюю сеть Docker. Снаружи у сервера открыты только порты 80 и 443. Панели phpMyAdmin и RabbitMQ не публикуют свои порты напрямую наружу.


Часть 1. Что происходит при push в main

Шаг 1. Сборка .env

Файл .env не хранится в Git. Он создаётся заново при каждом деплое прямо в CI. Это снижает риск случайной публикации секретов в репозитории и упрощает управление окружениями.

Все секреты живут в GitHub Actions в разделе:

Repository → Settings → Secrets and variables → Actions

Логика разделения такая:

  • vars.* — несекретные настройки: имена сервисов, домены, режимы работы, публичные параметры;
  • secrets.* — пароли, токены, приватные ключи и любые значения, которые нельзя показывать в логах или хранить в коде.

Пример CI-шагa:

# deploy.yml
- name: Create .env
  env:
    MYSQL_USER: ${{ vars.MYSQL_USER }}
    MYSQL_DATABASE: ${{ vars.MYSQL_DATABASE }}
    MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}
    RABBITMQ_USER: ${{ vars.RABBITMQ_USER }}
    RABBITMQ_PASSWORD: ${{ secrets.RABBITMQ_PASSWORD }}
    RABBITMQ_VHOST: ${{ vars.RABBITMQ_VHOST }}
  run: |
    echo "DB_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}" >> .env
    echo "MESSENGER_TRANSPORT_DSN=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672/${RABBITMQ_VHOST}" >> .env

Важный момент: GitHub маскирует значения из secrets, но не стоит специально выводить их в лог. Даже если секреты скрываются, лучше строить пайплайн так, чтобы пароли и токены вообще не попадали в stdout.

Также нужно следить за спецсимволами в паролях. Если пароль содержит символы, значимые для URL, его нужно корректно кодировать, иначе DSN может стать невалидным. Это особенно актуально для строк подключения вида:

mysql://user:password@db:3306/database
amqp://user:password@rabbitmq:5672/vhost

Шаг 2. Генерация .htpasswd для панелей

На этом же этапе создаются файлы basic-auth для nginx. Они нужны, чтобы закрыть административные панели дополнительным браузерным логином и паролем.

Для phpMyAdmin используется отдельный логин:

# Для phpMyAdmin — отдельный логин
mkdir -p gateway/docker/production/nginx/auth

echo "${PMA_AUTH_USER}:$(openssl passwd -apr1 "${PMA_AUTH_PASSWORD}")" \
  > gateway/docker/production/nginx/auth/.htpasswd

Для RabbitMQ используются те же реквизиты, что и у самого RabbitMQ:

# Для RabbitMQ — используются те же реквизиты, что и у самого RMQ
mkdir -p gateway/docker/production/nginx/auth

echo "${RABBITMQ_USER}:$(openssl passwd -apr1 "${RABBITMQ_PASSWORD}")" \
  > gateway/docker/production/nginx/auth/.htpasswd_rmq

Команда:

openssl passwd -apr1 "password"

создаёт хэш в формате Apache MD5, он же apr1. Nginx умеет читать такие хэши в файлах, подключённых через директиву auth_basic_user_file.

Файлы попадают в каталог:

gateway/docker/production/nginx/auth/

После этого они синхронизируются на сервер вместе с кодом.

В Git эти файлы хранить не нужно. Каталог с auth-файлами или сами файлы должны быть добавлены в .gitignore:

gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq

Иначе можно случайно закоммитить хэши паролей. Хэш — это не пароль в открытом виде, но публиковать его всё равно нельзя: его можно пытаться подбирать офлайн.


Шаг 3. rsync на сервер

После подготовки .env и .htpasswd код синхронизируется на сервер.

Пример команды:

rsync -avz --delete \
  --exclude='.git' \
  --exclude='api/vendor' \
  --exclude='frontend/node_modules' \
  ./ deploy@app-server:/srv/project/

Ключ --delete означает, что файлы, удалённые в рабочей копии CI, будут удалены и на сервере. Это полезно для чистой синхронизации, потому что сервер не превращается в склад старых файлов.

Но у --delete есть важное следствие: всё, что должно жить только на сервере, не должно попадать в директорию, которую полностью перетирает rsync. Например, runtime-данные, uploads, volume-данные базы и файлы, создаваемые приложением, лучше хранить вне каталога синхронизации или подключать через Docker volumes.

В этом варианте из синхронизации исключены:

--exclude='.git'
--exclude='api/vendor'
--exclude='frontend/node_modules'

Это логично: .git на production-сервере не нужен, PHP-зависимости и Node.js-зависимости обычно устанавливаются или собираются внутри образов, а не копируются как локальные каталоги разработчика.


Шаги 4–8. Деплой на сервере

После синхронизации CI заходит на сервер и выполняет команды Docker Compose.

Сначала собираются новые образы:

# Собираем новые образы. Старые контейнеры в этот момент ещё работают.
docker compose -f docker-compose-production.yml build

Этот шаг не переключает приложение сам по себе. Он только собирает новые версии образов. Старые контейнеры продолжают работать до команды up.

Дальше контейнеры обновляются:

# Переключаем контейнеры
docker compose -f docker-compose-production.yml up -d --remove-orphans

up -d запускает контейнеры в фоне. Если конфигурация сервиса или образ изменились, Compose пересоздаёт соответствующие контейнеры. Ключ --remove-orphans удаляет контейнеры от сервисов, которые раньше были в compose-файле, но теперь удалены из конфигурации.

Даунтайм здесь обычно минимальный, но не стоит называть такую схему полноценным zero-downtime деплоем. Если контейнер API пересоздаётся в единственном экземпляре, короткий разрыв возможен. Для настоящего zero-downtime нужны дополнительные механизмы: несколько реплик, healthcheck, корректное переключение трафика и аккуратная стратегия обновления.

После запуска контейнеров выполняются миграции:

# Ждём MySQL и гоняем миграции
docker compose -f docker-compose-production.yml run --rm api-php-cli \
  php bin/console doctrine:migrations:migrate -n

Здесь используется отдельный CLI-контейнер приложения. Это правильный подход: миграции запускаются в том же окружении, что и приложение, с теми же переменными окружения и зависимостями.

Перед reload nginx лучше проверять конфигурацию:

# Проверяем конфигурацию nginx
docker compose -f docker-compose-production.yml exec gateway nginx -t

# Перечитываем конфиг nginx без полного перезапуска контейнера
docker compose -f docker-compose-production.yml exec gateway nginx -s reload

nginx -s reload перечитывает конфигурацию без остановки всего контейнера. Но если конфигурация невалидна, reload может привести к проблемам. Поэтому nginx -t перед reload — простая и полезная страховка.

В конце масштабируются воркеры:

# Масштабируем воркеры
docker compose -f docker-compose-production.yml up -d \
  --scale api-workers=5 \
  --scale log-workers=1 \
  --no-recreate

Ключ --scale задаёт количество контейнеров для конкретного сервиса. Например, здесь поднимается пять экземпляров api-workers и один экземпляр log-workers.

Ключ --no-recreate говорит Compose не пересоздавать уже существующие контейнеры, если это не требуется. Для воркеров это удобно: можно поменять количество реплик без лишнего пересоздания того, что уже работает.


Часть 2. Как подключены phpMyAdmin и RabbitMQ

Сетевая изоляция

Все контейнеры находятся во внутренней Docker-сети, условно назовём её backend.

Снаружи открыты только порты 80 и 443, и оба слушает gateway — контейнер с nginx.

Internet → 443 → gateway (nginx) → internal Docker network
                                     ├── frontend
                                     ├── api-php-fpm
                                     ├── phpmyadmin:80
                                     └── rabbitmq:15672

phpMyAdmin и RabbitMQ не имеют опубликованных наружу портов. Добраться до них можно только через gateway.

Это важный момент безопасности. Даже если кто-то знает внутреннее имя контейнера или стандартный порт RabbitMQ Management, напрямую из интернета он туда не попадёт.


Конфиг nginx для phpMyAdmin

Пример файла pma.conf:

server {
    listen 443 ssl;
    server_name pma.example.com;

    # Basic auth — запрашивает логин/пароль перед любым запросом
    auth_basic "Admin Area";
    auth_basic_user_file /etc/nginx/auth/.htpasswd;

    location / {
        proxy_pass http://phpmyadmin;
    }
}

Как это работает:

  1. Браузер идёт на https://pma.example.com.
  2. Nginx показывает браузерный диалог «введите логин/пароль».
  3. Nginx сверяет введённые данные с файлом /etc/nginx/auth/.htpasswd.
  4. Если данные не совпали — nginx отдаёт 401 Unauthorized.
  5. Если данные совпали — nginx проксирует запрос в контейнер phpmyadmin на порт 80.

Запись:

proxy_pass http://phpmyadmin;

работает за счёт Docker DNS. Имя phpmyadmin должно совпадать с именем сервиса или сетевым alias в Docker Compose, а сам nginx-контейнер должен находиться в той же Docker-сети.

Если nginx находится не в той же сети, имя контейнера не разрешится, и nginx выдаст ошибку вида:

host not found in upstream "phpmyadmin"

Конфиг nginx для RabbitMQ

Пример файла rmq.conf:

server {
    listen 443 ssl;
    server_name rmq.example.com;

    auth_basic "Admin Area";
    auth_basic_user_file /etc/nginx/auth/.htpasswd_rmq;

    location / {
        proxy_pass http://rabbitmq:15672;
    }
}

Здесь логика такая же, как у phpMyAdmin, но upstream другой:

proxy_pass http://rabbitmq:15672;

Порт 15672 — это HTTP-интерфейс RabbitMQ Management. Он используется для веб-интерфейса и HTTP API управления RabbitMQ.

В описанной схеме используется management-вариант образа RabbitMQ:

rabbitmq:3.13.2-management

У такого образа management plugin уже включён, поэтому веб-интерфейс RabbitMQ доступен внутри Docker-сети на порту 15672.


Почему для RabbitMQ получается two-layer auth

У RabbitMQ Management есть собственная авторизация: пользователь вводит логин и пароль RabbitMQ уже в самой форме RabbitMQ Management UI.

Но перед этим стоит nginx basic auth.

Получается два слоя:

Браузер
nginx basic auth
RabbitMQ Management auth
RabbitMQ Management UI

На практике пользователь проходит два логина:

  1. Сначала браузерный basic-auth диалог от nginx.
  2. Потом форму авторизации RabbitMQ Management.

Зачем это нужно:

  • RabbitMQ Management UI не светится в интернете без предварительной авторизации nginx.
  • Сканеры и случайные посетители не видят форму RabbitMQ напрямую.
  • Даже если пароль RabbitMQ известен, нужно пройти ещё внешний слой basic auth.

Важно: nginx basic auth не заменяет авторизацию RabbitMQ. Это именно дополнительный слой перед management UI.


Часть 3. SSL-сертификаты

SSL-сертификаты Let’s Encrypt покрывают все публичные имена проекта:

example.com
www.example.com
api.example.com
rmq.example.com
pma.example.com

Рядом с основными контейнерами работает certbot. Он проверяет срок действия сертификата по расписанию и обновляет сертификаты автоматически.

Nginx читает сертификаты из общего volume, например:

/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem

или из другого пути внутри контейнера, если volume примонтирован иначе.

Общая схема такая:

certbot → обновляет сертификаты → shared volume → nginx читает сертификаты

После успешного обновления сертификата nginx должен перечитать сертификаты. Для этого нужен reload nginx. В контейнерной схеме это обычно решается одним из вариантов:

  • deploy-hook у certbot, который вызывает reload nginx;
  • отдельный скрипт обслуживания;
  • команда reload внутри CI/CD;
  • периодический безопасный reload nginx после проверки конфигурации.

Минимальная проверка автоматического продления:

certbot renew --dry-run

Если certbot работает в контейнере, команда будет выглядеть с учётом Docker Compose, например:

docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run

После обновления сертификата полезно проверять, какой сертификат реально отдаёт nginx:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -issuer -subject

Это помогает поймать ситуацию, когда certbot сертификат обновил, но nginx продолжает отдавать старый сертификат из памяти.


Часть 4. Экстренное ручное управление

Автоматический деплой закрывает обычный сценарий, но всегда нужен понятный аварийный путь: как посмотреть логи, перезапустить сервисы, выполнить миграции или сделать полный передеплой.

Полный передеплой

Если что-то сломалось, можно запустить отдельный workflow вручную:

GitHub → Actions → Full Redeploy → Run workflow

Такой workflow может называться redeploy.yml.

Его задача — не просто обновить изменившиеся контейнеры, а полностью пересобрать окружение:

# Остановить контейнеры
docker compose -f docker-compose-production.yml down

# Собрать образы с нуля
docker compose -f docker-compose-production.yml build --no-cache

# Поднять окружение заново
docker compose -f docker-compose-production.yml up -d --remove-orphans

Такой режим полезен, если есть подозрение на битый слой Docker image, конфликт старых контейнеров или некорректное состояние после неудачного деплоя.

Но это более жёсткая операция, чем обычный deploy: она может дать больший даунтайм, поэтому её лучше использовать именно как аварийный инструмент.


Посмотреть логи gateway

Чтобы посмотреть логи nginx-gateway:

docker compose -f docker-compose-production.yml logs -f gateway

Эта команда нужна, если:

  • не открывается frontend;
  • API отдаёт ошибку через nginx;
  • не работает phpMyAdmin;
  • не открывается RabbitMQ Management UI;
  • есть подозрение на ошибку upstream;
  • nginx не может найти контейнер по имени;
  • не применился новый конфиг.

Типовые ошибки, которые можно увидеть в логах:

host not found in upstream
connect() failed
upstream timed out
SSL_do_handshake() failed
no user/password was provided for basic authentication
user "..." was not found in "/etc/nginx/auth/.htpasswd"

Посмотреть логи RabbitMQ

Для RabbitMQ:

docker compose -f docker-compose-production.yml logs -f rabbitmq

Эта команда помогает проверить:

  • стартовал ли RabbitMQ;
  • подключились ли приложения к брокеру;
  • создался ли нужный vhost;
  • есть ли ошибки авторизации;
  • работают ли очереди;
  • нет ли проблем с диском или памятью.

Зайти в MySQL вручную через приложение

Для простой проверки подключения к базе можно выполнить SQL-запрос через Symfony CLI-контейнер:

docker compose -f docker-compose-production.yml run --rm api-php-cli \
  php bin/console doctrine:query:sql "SELECT 1"

Если команда возвращает результат, значит:

  • CLI-контейнер приложения стартует;
  • переменные окружения прочитаны;
  • DSN до базы корректный;
  • MySQL доступен из Docker-сети;
  • Doctrine может выполнить запрос.

Для диагностики это часто удобнее, чем заходить напрямую в MySQL-контейнер, потому что проверяется именно путь приложения до базы.


Что важно проверить перед публикацией такой схемы в production

Ниже не новая архитектура, а чек-лист к уже описанной схеме. Он помогает не пропустить мелочи, из-за которых деплой обычно ломается в самый неудобный момент.

1. .env не должен попадать в Git

Проверьте .gitignore:

.env
.env.*

Если нужны шаблоны, лучше хранить только пример:

.env.example

В нём должны быть имена переменных без реальных секретов.


2. Auth-файлы не должны попадать в Git

Проверьте:

gateway/docker/production/nginx/auth/.htpasswd
gateway/docker/production/nginx/auth/.htpasswd_rmq

Можно исключить весь каталог:

gateway/docker/production/nginx/auth/

Но тогда нужно убедиться, что CI создаёт каталог перед записью файлов:

mkdir -p gateway/docker/production/nginx/auth

3. Nginx должен быть в той же Docker-сети, что phpMyAdmin и RabbitMQ

Если в конфиге nginx написано:

proxy_pass http://phpmyadmin;
proxy_pass http://rabbitmq:15672;

то контейнер gateway должен видеть сервисы phpmyadmin и rabbitmq по Docker DNS.

Проверить можно так:

docker compose -f docker-compose-production.yml exec gateway getent hosts phpmyadmin
docker compose -f docker-compose-production.yml exec gateway getent hosts rabbitmq

Если имена не резолвятся, проблема обычно в сетях Docker Compose: сервисы находятся в разных сетях или nginx не подключён к нужной сети.


4. Перед reload nginx нужно выполнять nginx -t

Безопасный порядок:

docker compose -f docker-compose-production.yml exec gateway nginx -t
docker compose -f docker-compose-production.yml exec gateway nginx -s reload

Если nginx -t падает, reload делать нельзя. Сначала нужно исправить конфигурацию.


5. Certbot нужно проверять dry-run’ом

Команда:

certbot renew --dry-run

или контейнерный вариант:

docker compose -f docker-compose-production.yml run --rm certbot renew --dry-run

должна успешно проходить после первичной настройки и после изменений в nginx, DNS, firewall или reverse proxy.


6. RabbitMQ Management лучше не публиковать напрямую

В этой схеме management UI доступен только через nginx и basic auth. Это лучше, чем открывать порт 15672 наружу через Docker ports.

То есть для production-сценария лучше избегать такого варианта:

ports:
  - "15672:15672"

Если порт опубликован наружу, RabbitMQ Management UI будет доступен напрямую, в обход nginx basic auth.


Итог

Схема строится вокруг простой идеи: весь внешний трафик проходит через один nginx-gateway, а внутренние сервисы остаются закрытыми в Docker-сети.

При push в main GitHub Actions:

  1. Создаёт .env из vars и secrets.
  2. Генерирует .htpasswd для phpMyAdmin и RabbitMQ.
  3. Синхронизирует код на сервер через rsync.
  4. Собирает Docker-образы.
  5. Обновляет контейнеры через Docker Compose.
  6. Запускает миграции.
  7. Перечитывает nginx.
  8. Масштабирует воркеры.

phpMyAdmin и RabbitMQ при этом не открываются напрямую в интернет. Доступ к ним идёт только через nginx:

pma.example.com → nginx basic auth → phpMyAdmin
rmq.example.com → nginx basic auth → RabbitMQ Management auth → RabbitMQ UI

Для production это нормальная и понятная схема: секреты не лежат в Git, административные панели закрыты дополнительным слоем авторизации, SSL обновляется автоматически, а аварийное управление остаётся доступным через отдельные команды и ручной workflow.

Главное — не забыть технические проверки: .gitignore для секретов, nginx -t перед reload, certbot renew --dry-run, отсутствие прямой публикации 15672 наружу и корректную Docker-сеть между gateway, phpmyadmin и rabbitmq.

// Reviews

Отзывы по теме

Опыт сотрудничества оставил максимально позитивное впечатление, в первую очередь профессионализмом и подходом к решению возникающих проблем.

Опыт сотрудничества оставил максимально позитивное впечатление, в первую очередь профессионализмом и подходом к решению возникающих проблем.

mendarinno384

Jitsi meet: персональный zoom, настройка jitsi meet в docker и на VPS

11.11.2025 · ★ 5/5

Была задача наладить работу n8n, redis и базы данных. Заказывал раньше у другого исполнителя, постоянно все ломалось. Заказал у Михаила, на следующий же день все стало работать быстро, как часы!

Была задача наладить работу n8n, redis и базы данных. Заказывал раньше у другого исполнителя, постоянно все ломалось. Заказал у Михаила, на следующий же день все стало работать быстро, как часы!

christ_media

N8n установка на ваш vps сервер. Настройка n8n, docker, ai, telegram

24.09.2025 · ★ 5/5

Опытный покупатель

ladohinpy

N8n установка на ваш vps сервер. Настройка n8n, docker, ai, telegram

25.08.2025 · ★ 5/5

Михаил выполнил настройку очередного VPS. Быстро, профессионально обходя определенные ограничение хостинг провайдеров.

Михаил выполнил настройку очередного VPS. Быстро, профессионально обходя определенные ограничение хостинг провайдеров.

NadoBy

NadoBy

N8n установка на ваш vps сервер. Настройка n8n, docker, ai, telegram

12.08.2025 · ★ 5/5

Освоившийся покупатель

// Contact

Нужна помощь?

Свяжись со мной и я помогу решить проблему

Отправить заявку
Написать и получить быстрый ответ