Docker Compose: PHP, MySQL, Nginx и phpMyAdmin - локальный стенд и связка с HTTP из 1С
Разработка - DevOps и автоматизация разработки
Docker и Docker Compose позволяют поднять на рабочей машине согласованный набор PHP, MySQL, Nginx и phpMyAdmin без «монолитной» локальной сборки вроде Denwer и без смешивания версий с системными службами Windows. Один сомросе-файл описывает сервисы, порты, сеть и тома; исходники PHP и конфиги Nginx лежат в каталоге проекта и правятся как обычные файлы. К концу настройки получится воспроизводимый изолированный веб-стек: сайт на порту 80, phpMyAdmin на 8000, база с отдельным пользователем приложения и возможность вызвать тот же НТТР-сервис из 1С на том же ПК через localhost.
Зачем Docker Compose вместо «всё-ин-оне» на Windows
Раньше я держал PHP, MySQL и phpMyAdmin в одной «коробке» под Windows. На старте это удобно, но версии PHP и MySQL жёстко привязаны к пакету, каталоги разъезжаются с рабочими проектами, а второй параллельный стенд под другую ветку интеграции превращается в отдельную возню с портами и службами.
Контейнеры решают другую задачу: каждый компонент живёт в своём образе с зафиксированной версией, не регистрируется в системе как служба MySQL или Apache и переносится на другой ПК по схеме «склонировал репозиторий - compose up». Docker Compose сводит описание стека в один YAML: зависимости сервисов, порты на хосте, монтирование каталогов с машины разработчика, место хранения данных БД между пересборками образа php.
Для разработчика или администратора 1С это не замена клиент-серверной базы платформы и не публикация НТТР-сервисов самой 1С на IIS. Это отдельный PHP и MySQL на хосте, к которым удобно ходить из конфигурации через НТТРСоединение, когда нужно проверить обмен JSON или XML с внешним API, не разворачивая полноценный веб-сервер в прод-контуре.
Главное, что я держу в голове при локальной работе: изоляция и переносимость. Код правите в src/public, перезапускаете только нужный сервис; данные MySQL не пропадают после docker compose build, если том подключён как named volume, а не как временный каталог внутри контейнера.
Подготовка: Docker Desktop и структура проекта
На Windows ставлю Docker Desktop с официального сайта Docker. После установки проверяю, что в терминале отвечают docker version и docker compose version. Актуальный синтаксис - docker compose (пробел); старый бинарь docker-compose ещё встречается в статьях и скриптах, смысл команд тот же. Эталонную раскладку каталогов беру из публичного репозитория komissarovev/docker_php на GitHub: клонирую себе, открываю корень проекта в терминале.
До первой сборки смотрю карту каталогов: где лежит public, откуда Nginx подхватывает conf.d и в какой папке собирается php-fpm.
Каталоги src, nginx и php относительно сомросе-файла
Путаница между путями на хосте и путями внутри контейнера даёт «белый экран» и 404 на .php; иллюстрация выше как раз про привязку src, nginx и php к одному сомросе-файлу.
Сомросе-файл задаёт общую сеть tools_php-network (имя может отличаться у вашей копии): внутри неё сервисы видят друг друга по ДНС-имени сервиса - mysql, php, nginx, а не по 127.0.0.1. На хосте доступны только опубликованные порты, обычно 80 для Nginx и 8000 для phpMyAdmin.
Пароли и логины для учебного стенда выношу в .env и подключаю через env_file в compose, чтобы не коммитить секреты в git. В репозиториях для статей иногда оставляют явный MYSQL_ROOT_PASSWORD в YAML - для своей машины привычнее сразу работать через .env.
Запуск стека и остановка без потери данных
Первый проход: из корня проекта docker compose build собирает образ сервиса php по Dockerfile, остальные сервисы тянут готовые образы mysql, nginx, phpmyadmin. Затем docker compose up поднимает контейнеры и пишет логи в текущую консоль. Если менял Dockerfile или набор расширений, удобнее сразу docker compose up --build.
Для фоновой работы добавляю флаг -d: docker compose up --build -d. Консоль освобождается, контейнеры остаются запущенными. Запущенный без -d стек останавливаю Ctrl+C; это не всегда равно полному удалению контейнеров. Чтобы остановить и убрать контейнеры проекта, использую docker compose down. Named volume с данными MySQL при обычном down не удаляется, пока явно не указать -v.
После успешного up открываю в браузере localhost:80 - должна открыться страница из src/public (часто index.php). phpMyAdmin слушает localhost:8000, потому что 80-й порт на хосте уже отдан Nginx.
Если порт 80 занят Skype, IIS или другим стендом, compose упадёт с ошибкой bind. Либо освобождаю порт, либо меняю левую часть маппинга ports у nginx, например 8080:80, и тогда URL сайта и вызовы из 1С строю с тем же портом.
Пересборка образа php не должна пересоздавать базу: данные лежат в named volume mysql_php, подключённом к сервису mysql. Команда build меняет слои образа приложения, том живёт отдельно. Я разделяю «сломался РНР-код или Dockerfile» (build/up php) и «сломалась схема БД» (миграции, дамп, phpMyAdmin).
MySQL: том, root и пользователь приложения
В compose у mysql задают образ с фиксированным тегом, например mysql:8.0.29, переменную MYSQL_ROOT_PASSWORD и монтирование тома mysql_php в каталог данных MySQL внутри контейнера (в эталонном проекте путь в volume описан так, чтобы данные переживали rebuild). Root нужен для администрирования, приложению на PHP root не даю.
Базу и пользователя создаю через docker exec в контейнер mysql, не обязательно через GUI. Имя контейнера смотрю в container_name в compose или в docker ps: docker exec -it tools_php-mysql sh (имя замените на своё), внутри контейнера mysql -uroot -p и пароль из compose или .env.
CREATE DATABASE `demo_app`;
SHOW DATABASES;
CREATE USER 'app_user'@'%' IDENTIFIED BY 'задайте_пароль';
GRANT ALL PRIVILEGES ON `demo_app`.* TO 'app_user'@'%';
FLUSH PRIVILEGES;
Хост '%' в MySQL означает «подключение с любого хоста в сети сервера БД». В Docker у каждого контейнера свой IP; php подключается к хосту mysql по имени сервиса, не с localhost контейнера php. Если создать пользователя только для 'localhost' внутри контейнера mysql, php из соседнего контейнера часто не проходит аутентификацию.
Удаление пользователя, если переделываю стенд: сначала REVOKE, затем DROP USER. Пароль root храню только для админских операций и входа в phpMyAdmin при отладке; в переменных сервиса php указываю app_user и demo_app.
MySQL 8 по умолчанию использует плагин caching_sha2_password. Расширения pdo_mysql и mysqli в php-fpm 8.2 и 8.3 обычно работают с ним из коробки. Если клиент ругается на метод аутентификации, проверяю версию образа mysql и при необходимости создаю пользователя заново или смотрю default_authentication_plugin в конфиге образа - для учебного стенда чаще проще пересоздать пользователя с актуальным клиентом PHP, чем откатывать mysql на 5.7.
Логический бэкап для переноса на другой ПК: через phpMyAdmin на порту 8000 (экспорт demo_app) или mysqldump из exec. Физически данные named volume лежат в хранилище Docker; для переноса между машинами мне удобнее логический дамп, чем копирование volume без опыта.
phpMyAdmin и Nginx: порты, сеть, PHP-FPM
Сервис phpmyadmin в compose тянет образ phpmyadmin; тег latest для прод-подобного стенда я заменяю на конкретный, чтобы обновление образа не меняло поведение неожиданно. Переменная PMA_ARBITRARY=1 разрешает вход не только на преднастроенный хост: в форме логина указываю сервер mysql, пользователя app_user или root, пароль из .env.
UPLOAD_LIMIT, MEMORY_LIMIT и MAX_EXECUTION_TIME поднимают потолок для импорта дампов через веб-интерфейс. Без них большой SQL из бэкапа обрывается на дефолтных лимитах PHP внутри контейнера phpMyAdmin. depends_on: mysql задаёт порядок старта; готовность mysql к подключениям всё равно стоит проверить повторным входом, если контейнер mysql только что поднялся.
Маппинг 8000:80 пробрасывает веб-интерфейс phpMyAdmin на хост: внутри контейнера Apache слушает 80, снаружи мы не конфликтуем с Nginx. depends_on связывает phpMyAdmin с mysql в одной user-defined сети compose.
Nginx публикует 80:80, монтирует ./src в /var/www и ./nginx/conf.d в /etc/nginx/conf.d. Сверяю сервер-блок с тем, что реально лежит в nginx/conf.d:
server {
listen 80;
root /var/www/public;
index index.php;
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass php:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
Строка fastcgi_pass php:9000 связывает Nginx с сервисом php по DNS compose; SCRIPT_FILENAME должен указывать на файл внутри смонтированного /var/www.
location / с try_files отправляет «красивые» URL на index.php при необходимости. Если PHP отвечает 502, смотрю, запущен ли контейнер php, совпадает ли имя в fastcgi_pass с именем сервиса в compose и нет ли опечатки в SCRIPT_FILENAME.
Критично не путать два уровня имён: на хосте 127.0.0.1 или localhost и порты 80 и 8000; внутри сети compose - mysql, php:9000. Подмена MYSQL_HOST на 127.0.0.1 в environment сервиса php ломает подключение: внутри контейнера php localhost - это сам php, а не mysql.
Сервис PHP: Dockerfile, расширения, переменные окружения
Сервис php в compose использует build:./php и Dockerfile на базе php:8.2-fpm или php:8.3-fpm. Базовый тег в Dockerfile я обновляю, сохраняя тот же набор расширений: docker-php-ext-install pdo mysqli pdo_mysql, при необходимости ставлю Composer для зависимостей проекта. Образ собирается на build; после правок Dockerfile нужен compose build php.
working_dir /var/www и volume ./src:/var/www дают тот же код, что видит Nginx. В environment сервиса php передаю MYSQL_HOST=mysql, MYSQL_DATABASE=demo_app, MYSQL_USER=app_user, MYSQL_PASSWORD - значения должны совпасть с GRANT в MySQL и с тем, что вводите в phpMyAdmin.
В index.php или общем include читаю их через $_ENV и подключаюсь к TCP mysql:3306, без сокета localhost:
$link = mysqli_connect(
$_ENV['MYSQL_HOST'],
$_ENV['MYSQL_USER'],
$_ENV['MYSQL_PASSWORD'],
$_ENV['MYSQL_DATABASE']
);
Для нового кода тот же набор параметров удобно передать в PDO с DSN вида mysql:host=…;dbname=…; для проверки стенда достаточно вывести результат простого SELECT на странице порта 80.
Проверка связки: страница на порту 80 показывает результат запроса от app_user к demo_app. Ошибка Access denied - неверный пароль или пользователь не создан с '%'. Unknown MySQL сервер host - опечатка в MYSQL_HOST или контейнер mysql не в той сети. No such file or directory при mysqli - часто путаница с сокетом; для Docker нужен ТСР-хост mysql:3306, не localhost socket.
Образы mysql и nginx фиксирую тегами (mysql:8.0.x, nginx:stable-alpine), phpMyAdmin - выбираю тег осознанно, а не тяну latest без контроля. PHP через свой Dockerfile и так пересобирается при изменениях; инфраструктурные образы на новой машине не должны «уплывать» после каждого compose pull.
Вызов из 1С, актуальные теги и очистка Docker
На том же ПК, где крутится Docker Desktop, платформа 1С ходит на опубликованный порт хоста, а не к имени сервиса mysql. Для сайта при стандартном маппинге это HTTP к localhost на порт 80; для phpMyAdmin - порт 8000.
На рисунке ниже цепочка HTTP: запрос с сервера 1С на localhost:80, ответ JSON от PHP за Nginx. Это проверка внешнего API, не обмен с базой 1С.
Запрос из 1С на localhost:80 и ответ JSON от PHP
SSL для localhost обычно не нужен; если включить HTTPS в Nginx без доверенного сертификата, НТТРСоединение из 1С может упасть на проверке сертификата. Заголовки Content-Type и кодировка тела должны совпадать с тем, как вы разбираете ответ в 1С; для JSON удобно UTF-8 без BOM.
В src/public добавляю простой endpoint, например api/ping.php, который возвращает JSON с полем status. Так отделяю «дошёл ли HTTP» от «отработала ли логика БД».
Ниже фрагмент для общей формы или внешней обработки: GET к ping.php, разбор JSON через ЧтениеJSON. URL тестового API держу в константе или реквизите настройки интеграции, а не в десяти модулях подряд.
&НаСервере
Функция ПроверитьЛокальныйPhpСтенд(АдресСервера = "localhost", Порт = 80) Экспорт
Путь = "/api/ping.php";
Соединение = Новый HTTPСоединение(АдресСервера, Порт,,,, 30);
Запрос = Новый HTTPЗапрос(Путь);
Запрос.Заголовки.Вставить("Accept", "application/json; charset=utf-8");
Ответ = Соединение.Получить(Запрос);
Если Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение "HTTP " + Формат(Ответ.КодСостояния, "ЧГ=0")
+ ": " + Ответ.ПолучитьТелоКакСтроку("UTF-8");
КонецЕсли;
Тело = Ответ.ПолучитьТелоКакСтроку("UTF-8");
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(Тело);
Данные = ПрочитатьJSON(Чтение);
Чтение.Закрыть();
Если ТипЗнч(Данные) <> Тип("Структура") Тогда
ВызватьИсключение "Ожидалась структура JSON в ответе PHP";
КонецЕсли;
Если Не Данные.Свойство("status") Тогда
ВызватьИсключение "В ответе нет поля status";
КонецЕсли;
Возврат Данные.status = "ok";
КонецФункции
НТТРСоединение задаёт хост, порт и таймаут в секундах; для тяжёлых сценариев таймаут увеличиваю, иначе при отладке с точкой останова в PHP получу обрыв. НТТРЗапрос несёт путь без схемы и хоста. Код 404 часто значит неверный путь или root Nginx смотрит не туда; connection refused - compose не поднят или на хосте другой порт. Блокировка firewall на localhost встречается реже, но корпоративный агент иногда режет обращения к 127.0.0.1 - проверяю тем же URL из браузера на той же машине.
Для POST с JSON телом использую НТТРЗапрос с телом строкой и заголовком Content-Type: application/json; charset=utf-8; метод задаётся при вызове ОтправитьДляОбработки или аналога - сверяюсь со справкой платформы по НТТРМетод, если переношу пример с GET на POST.
Замкнутый сценарий от клонирования репозитория до ответа функции на сервере 1С помогает не пропустить шаг после очередного compose down.
От clone до проверки функции на сервере 1С
compose up, demo_app и app_user в mysql, согласованные MYSQL_* у php, ping.php в браузере, затем вызов проверочной функции на сервере 1С - порядок, по которому я сам прохожу стенд после чистой машины.
Когда стенд на другой машине не нужен, docker system prune удаляет неиспользуемые образы, остановленные контейнеры и сети без привязки к томам. На рабочей машине с несколькими проектами prune не жму вслепую: можно снести образы, которые потом долго качаются заново. Для одного учебного compose после экспериментов prune освобождает место; named volume mysql_php prune по умолчанию не трогает, но docker volume prune уже опасен для данных, если volume не примонтирован.
Перед сдачей задачи интеграции проверяю: docker compose ps показывает все сервисы Up; в phpMyAdmin под app_user видна demo_app; сайт на порту 80 и ping.php отдают JSON; функция на сервере 1С возвращает Истина. Если что-то из цепочки падает, иду по порядку сети compose, а не меняю сразу код обмена в конфигурации.
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.
См. также
10.11.2026 406 Iliaelagin 0
02.11.2026 633 vanya_gonchar 2