Двенадцать проектов, у каждого свой docker-compose.yml в корне. Один поднимается через docker compose up, другой требует флага с файлом переопределения, третий хочет переменные из .env, которого нет в репозитории. Вернувшись к проекту через два месяца, я каждый раз тратил двадцать минут на то, чтобы его просто запустить.
Сейчас во всех двенадцати одинаковый каталог и одинаковые три команды. Это не рефакторинг ради красоты — это экономия примерно часа в неделю.
Почему конфигурация вынесена из корня
В корне лежит compose.yaml, который создал Symfony Flex при установке пакетов. Он полезен как черновик и неудобен как рабочая конфигурация: рецепты пакетов дописывают в него сервисы, которые мне не нужны. Обычная установка пакета не затирает мои изменения, а recipes:update применяет патч и оставляет конфликты на ручное разрешение. Но смешивать автоматически предложенную конфигурацию с моей рабочей средой я всё равно не хочу.
Рабочая конфигурация живёт в отдельном каталоге:
.local/
├── docker-compose.yml
├── Makefile
├── .env
├── .env.example
├── .docker/
│ ├── php/
│ │ ├── Dockerfile
│ │ └── php.ini
│ ├── nginx/conf.d/nginx.conf
│ ├── supervisor/worker.conf
│ ├── mercure/Caddyfile
│ └── crontab
└── logs/
├── nginx/
└── php/Отделение решает и вторую задачу: понятно, что является источником истины. Вопрос «а какой из двух compose тут настоящий» отпадает, потому что настоящий тот, что в .local, и это записано в первой строке инструкции.
Имя проекта из переменной
Ключевая строка во всём файле:
name: ${PROJECT_NAME}
services:
db:
image: postgres:alpinePROJECT_NAME задаётся в .local/.env и определяет имя проекта в Docker и префикс томов. Явное container_name — отдельная настройка: переменная лишь делает такое имя уникальным.
Без фиксированного имени проекта два checkout с одинаковым именем корневого каталога могут попасть в один namespace Compose. С ним teamspace-app и lms-app живут рядом, у каждого свои контейнеры и тома.
Внутри Compose я обращаюсь к контейнерам по именам сервисов: docker compose exec webserver не требует container_name. Явные имена оставил там, где к контейнеру обращается внешний обратный прокси. Это осознанный компромисс: имя стабильно, но такой сервис уже нельзя масштабировать несколькими репликами через Compose.
Одинаковые имена сервисов
Во всех двенадцати проектах сервисы называются одинаково: db, redis, webserver, nginx, при необходимости mercure, worker, scheduler.
Это соглашение важнее, чем кажется. Оно означает, что любая команда работает в любом проекте без изменений:
docker compose --env-file .local/.env -f .local/docker-compose.yml exec -T webserver php bin/consoleОдин и тот же вызов, один и тот же скрипт развёртывания, одна и та же строка в документации. Когда сервис PHP в одном проекте называется app, в другом php, а в третьем php-fpm, каждый скрипт приходится писать заново.
Именно webserver для PHP-FPM — исторически неудачное имя, веб-сервером тут работает nginx. Менять я не стал: цена переименования в двенадцати проектах выше, чем польза от правильного слова.
Псевдонимы сети
Nginx должен знать, куда проксировать запросы к PHP. Прямая ссылка на контейнер завязывает конфигурацию на имя проекта:
fastcgi_pass teamspace-app_webserver:9000; # так плохоКонфигурация nginx перестаёт быть переносимой. Решается псевдонимом в сети:
webserver:
networks:
internal:
aliases:
- php-upstreamfastcgi_pass php-upstream:9000; # так хорошоТеперь файл nginx одинаков во всех проектах и копируется без правок. То же с Mercure: хаб получает псевдоним mercure-upstream, и секция проксирования потока событий тоже переносится как есть.
Три сети
Сетей у проекта три, и разделение осмысленное.
internal — внутренняя, создаётся самим проектом с флагом internal: true. В ней живут база, кеш, PHP и nginx. Docker не даёт контейнерам этой сети прямого внешнего доступа; наружу приложение выходит только через сервисы, подключённые к другим сетям.
proxy-dev — общая внешняя, в неё смотрит только nginx. Через неё до проекта достаёт обратный прокси.
mail-dev — общая внешняя, в неё смотрит только PHP. Через неё письма уходят в общий перехватчик почты.
webserver:
networks:
internal:
aliases: [php-upstream]
mail-dev: {}
nginx:
networks:
internal: {}
proxy-dev: {}
networks:
internal:
internal: true
proxy-dev:
external: true
mail-dev:
external: trueБаза в общие сети не смотрит вообще. Это не паранойя: при десятке одновременно поднятых проектов в одной сети хватает одной опечатки в строке подключения, чтобы приложение пошло в чужую базу. Оно даже подключится, если пароли совпадают.
Логи наружу
Каталоги логов монтируются в проект:
volumes:
- ./logs/nginx:/var/log/nginx
- ./logs/php:/var/log/phpЧитать текущие логи через docker compose logs можно и фильтровать обычными инструментами, но долговременного архива это не даёт: после удаления старого контейнера его локальная история исчезает.
Смонтированный каталог даёт обычные файлы, которые открываются любимым инструментом и переживают пересборку.
Что в репозитории, а что нет
В репозиторий едут: docker-compose.yml, Makefile, весь каталог .docker/, .env.example.
Не едут: .env с паролями, содержимое logs/, тома.
Каталог logs/ при этом должен существовать, иначе Docker создаст его владельцем root и nginx не сможет писать. Решается пустым файлом-заглушкой:
.local/logs/nginx/.gitkeep
.local/logs/php/.gitkeepМелочь, на которой я спотыкался трижды, пока не добавил её в шаблон.
Перенос на новый проект
У меня перенос занимает около пятнадцати минут:
- копирую каталог
.localиз ближайшего по стеку проекта; - правлю
PROJECT_NAMEи пароли в.env; - проверяю список сервисов: нужен ли Mercure и нужен ли воркер;
- правлю корень в конфигурации nginx, если структура каталогов отличается;
- добавляю запись в динамическую конфигурацию обратного прокси.
Всё. Ни Dockerfile, ни php.ini, ни конфигурация nginx правок не требуют — они одинаковы благодаря псевдонимам сети.
Чего не хватает
Одного места, где лежит эталон. Сейчас при улучшении раскладки я переношу изменение в двенадцать проектов руками, и половина из них отстаёт. Правильно было бы держать шаблон отдельным репозиторием и обновлять из него, но такой инструмент я пока не написал.
И версия PHP в Dockerfile разъезжается. В четырёх проектах 8.4, в остальных 8.3, и обновляю я их по мере того, как до них доходят руки. Это единственное место, где однообразие пока не выдержало.