Middle: питання на співбесіді з теми «Compose для розробки»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Compose вміє зливати кілька файлів: базова конфігурація + доповнення для конкретного оточення.
Автоматичне злиття: якщо поруч з compose.yaml є compose.override.yaml, docker compose up бере обидва - перевизначення застосовується поверх бази.
# compose.yaml - спільне для всіх оточень
services:
app:
image: myapp:${TAG:-latest}
environment:
APP_ENV: production
# compose.override.yaml - лише для локальної розробки (часто в .gitignore)
services:
app:
build: .
environment:
APP_ENV: local
APP_DEBUG: "true"
volumes:
- .:/var/www/html
ports:
- "127.0.0.1:8000:8000"
mailpit:
image: axllent/mailpit
Явний набір файлів - прапорець -f (файл override тоді не підхоплюється автоматично):
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.ci.yaml run --rm app php artisan test
Або через змінну COMPOSE_FILE=compose.yaml:compose.ci.yaml.
Правила злиття:
- одиночні значення (
image,command,restart) - заміняються значенням з пізнішого файлу; - словники (
environment,labels) - зливаються за ключами; - списки
ports,volumes- об'єднуються (з урахуванням однакових цілей), а не заміняються; !resetі!override- теги YAML, щоб явно скинути чи повністю замінити значення з базового файлу (наприклад, прибрати порти, оголошені в базі).
services:
app:
ports: !reset []
Відносні шляхи в усіх файлах обчислюються від першого файлу (базової директорії проєкту) - типова пастка при -f інша-папка/compose.yaml.
Перевірити результат злиття:
docker compose -f compose.yaml -f compose.prod.yaml config
Типові схеми:
compose.yaml(база) +compose.override.yaml(розробка, автоматично) +compose.prod.yaml(явно на сервері);- окремі файли для CI (без томів з кодом, з тестовою базою в пам'яті).
Альтернатива злиттю - include (підключення окремих частин) і профілі (сервіси, що вмикаються за потреби).
Профілі дають змогу тримати в одному compose.yaml сервіси, які запускаються лише за потреби.
services:
app:
build: .
postgres:
image: postgres:18
mailpit:
image: axllent/mailpit
profiles: [tools]
phpmyadmin:
image: phpmyadmin
profiles: [tools]
horizon:
build: .
command: php artisan horizon
profiles: [queue]
playwright:
image: mcr.microsoft.com/playwright
profiles: [e2e]
Правила:
- сервіс без
profilesзапускається завжди; - сервіс з профілем - лише коли профіль активовано:
docker compose up -d # app, postgres
docker compose --profile tools up -d # + mailpit, phpmyadmin
docker compose --profile tools --profile queue up -d
COMPOSE_PROFILES=tools,queue docker compose up -d
- явно названий сервіс запускається навіть без активного профілю:
docker compose run --rm playwright- профіль не потрібен; - якщо сервіс з профілем залежить (
depends_on) від іншого сервісу з неактивним профілем - Compose повідомить про помилку; залежності мають бути або без профілю, або в тому самому.
Коли профілі зручніші за окремі файли:
- допоміжні інструменти розробки (адмінки баз, перехоплювачі пошти, профайлери), які потрібні не щодня;
- важкі сервіси (Elasticsearch, браузерні тести), що споживають багато пам'яті, - вмикати лише для відповідних задач;
- ролі одного застосунку - вебсервер, воркер черги, планувальник - запускаються за потреби;
- усе в одному файлі - видно повну картину оточення, без пошуку по кількох файлах.
Коли краще окремі файли (-f): коли відрізняється конфігурація тих самих сервісів між оточеннями (розробка, CI, продакшен), а не набір сервісів.
Перевірка: docker compose --profile tools config --services - перелік сервісів, що будуть запущені з цим профілем.
Healthcheck - команда, яку Docker періодично виконує всередині контейнера, щоб визначити, чи сервіс справді працює, а не просто запущений процес. Результат - стан starting, healthy чи unhealthy.
services:
postgres:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s # як часто перевіряти
timeout: 3s # скільки чекати на відповідь
retries: 10 # скільки невдач поспіль до unhealthy
start_period: 20s # пільговий період на старті: невдачі не рахуються
start_interval: 1s # частіші перевірки під час start_period
app:
build: .
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/up"]
interval: 10s
timeout: 3s
retries: 3
Що робить healthcheck добрим:
- перевіряє здатність обслуговувати запити, а не лише живий процес:
pg_isready,redis-cli ping, HTTP-запит до ендпойнта здоров'я (у Laravel 11+ - маршрут/up); - легкий і швидкий - він виконується кожні кілька секунд; важкі запити до бази в healthcheck створюють навантаження;
- не залежить від зовнішніх сервісів у перевірці «живучості»: якщо застосунок вважає себе unhealthy через недоступний сторонній API, оркестратор почне його перезапускати, що нічого не виправить;
- інструмент є в образі:
curlчиwgetчасто відсутні в мінімальних образах - перевірка падає не через сервіс, а через відсутню програму; $$у Compose - щоб змінна підставилася всередині контейнера, а не при розборі файлу.
Як використовується стан:
depends_onзcondition: service_healthy- залежний сервіс стартує лише після готовності;docker compose psпоказує стан, аdocker inspect- історію останніх перевірок з виводом команди;docker compose up --wait- дочекатися, поки всі сервіси стануть healthy (зручно в CI);- оркестратори (Swarm) перезапускають unhealthy-контейнери. Звичайний Docker сам по собі не перезапускає unhealthy-контейнер - лише позначає його.
Healthcheck в образі (HEALTHCHECK у Dockerfile) задає перевірку за замовчуванням; у Compose її можна перевизначити чи вимкнути (disable: true).
Bind mount (volumes: - .:/var/www/html) робить каталог хоста видимим у контейнері напряму: зміна файлу на хості миттєво видна в контейнері.
docker compose watch (Compose 2.22+) - інший підхід: Compose стежить за файлами на хості й при змінах виконує дію з контейнером.
services:
app:
build: .
develop:
watch:
- action: sync # скопіювати змінені файли в контейнер
path: ./app
target: /var/www/html/app
- action: sync+restart # скопіювати й перезапустити сервіс
path: ./config
target: /var/www/html/config
- action: rebuild # перезібрати образ і перестворити контейнер
path: composer.lock
- action: sync
path: ./resources
target: /var/www/html/resources
ignore:
- node_modules/
docker compose watch # або docker compose up --watch
Дії:
sync- копіює змінені файли в контейнер (для коду, що підхоплюється «на льоту»: PHP, шаблони, фронтенд з HMR);sync+restart- копіює й перезапускає основний процес (змінилася конфігурація);sync+exec- копіює й виконує команду в контейнері;rebuild- перезбирає образ (змінилися залежності:composer.lock,package.json,Dockerfile).
Чим відрізняється від bind mount:
- синхронізація лише потрібних каталогів, з виключеннями -
vendor,node_modulesі кеші не синхронізуються, у контейнері лишаються свої версії, зібрані для Linux; - продуктивність на macOS і Windows: bind mount через віртуальну машину Docker Desktop повільний на великих кодових базах; watch копіює лише змінені файли, а читання в контейнері йде з його власної файлової системи;
- права й власники файлів - менше проблем, ніж зі спільними каталогами;
- автоматичний rebuild при зміні залежностей - не треба пам'ятати про
--build.
Обмеження:
- зміни з контейнера назад на хост не синхронізуються (згенеровані файли, міграції, створені командою
artisan make:*) - для таких робочих процесів bind mount зручніший; - потрібен
buildу конфігурації сервісу (watch працює з сервісами, які Compose збирає).
Для Laravel-розробки часто поєднують: bind mount для каталогів, де генеруються файли, і watch - для залежностей з rebuild.
У розробці код зазвичай монтують з хоста: - .:/var/www/html. Разом із кодом у контейнер потрапляють і каталоги залежностей хоста - vendor і node_modules. Звідси три проблеми:
1. Різні платформи. На хості macOS (ARM чи x86), у контейнері - Linux. Нативні модулі npm (esbuild, rollup, sharp) і деякі Composer-пакети з бінарними файлами, встановлені на хості, у контейнері не запрацюють - і навпаки.
2. Продуктивність. vendor і node_modules - десятки тисяч дрібних файлів. На Docker Desktop (macOS, Windows) кожне читання через bind mount проходить через межу віртуальної машини - автозавантаження Composer і збирання фронтенду помітно сповільнюються.
3. Конфлікти. Залежності, встановлені в контейнері, перезаписують хостові (і навпаки) - нескінченне «а в мене працює».
Рішення - іменований том поверх каталогу залежностей:
services:
app:
build: .
volumes:
- .:/var/www/html # код з хоста
- vendor:/var/www/html/vendor # залежності - у томі Docker
- node_modules:/var/www/html/node_modules
volumes:
vendor:
node_modules:
Пізніше змонтований том «закриває» відповідний підкаталог bind mount: у контейнері vendor - з тому (Linux-версії, швидкий доступ), а на хості свій vendor (чи порожньо).
Що варто знати:
- встановлення залежностей - у контейнері:
docker compose run --rm app composer install,docker compose run --rm app npm ci; - IDE на хості не бачить залежностей з тому - автодоповнення й аналіз коду страждають. Варіанти: встановлювати й на хості теж, налаштувати IDE на віддалений інтерпретатор у контейнері, devcontainers;
- новий том порожній при першому створенні, але якщо в образі за цим шляхом уже є файли, Docker копіює їх у порожній том - зручно, якщо залежності встановлені при збиранні образу;
- оновлення залежностей: після зміни
composer.lockтом треба оновити (composer installзнову) - сам по собі він не «знає» про зміни; - скинути -
docker compose down -vвидаляє томи проєкту (разом з даними бази, якщо вони теж у томах!) - або видалити конкретний том:docker volume rm проєкт_vendor.
Альтернативи: docker compose watch з виключенням каталогів залежностей, синхронізовані файлові спільні каталоги Docker Desktop.