Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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

Профілі дають змогу тримати в одному 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 - перелік сервісів, що будуть запущені з цим профілем.

Докладніше в документації: Профілі Compose

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).

Докладніше в документації: Сервіси Compose: healthcheck

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.

Докладніше в документації: Compose Watch

У розробці код зазвичай монтують з хоста: - .:/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.

Докладніше в документації: Томи Docker