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

Питання на співбесіді: Compose для розробки

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

14 питань

Docker Compose описує багатоконтейнерне оточення одним файлом: сервіси, мережі, томи, секрети. Одна команда docker compose up піднімає все разом.

# compose.yaml
services:
  app:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      DB_HOST: postgres
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - .:/var/www/html

  postgres:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s

volumes:
  pgdata:

Основні розділи верхнього рівня:

  • services - контейнери з образом чи інструкціями збирання, портами, змінними, томами, залежностями;
  • volumes - іменовані томи для даних, що мають переживати перестворення контейнерів;
  • networks - мережі (за замовчуванням створюється одна для всього проєкту);
  • secrets, configs - файли конфігурації й секретів;
  • name - назва проєкту (префікс для контейнерів, мереж, томів).

Чому без version: колись файли починалися з version: "3.8", і версія визначала доступні можливості. Тепер Compose реалізує єдину Compose Specification - поле version застаріле й ігнорується. Сучасний Compose виводить попередження: «the attribute version is obsolete». Його можна просто видалити.

Назва файлу: рекомендована - compose.yaml (також підтримуються compose.yml і старі docker-compose.yml). Команда - docker compose (вбудований плагін, Compose v2), а не окрема програма docker-compose першої версії.

Корисні команди:

docker compose up -d            # запустити у фоні
docker compose ps               # стан сервісів
docker compose logs -f app      # логи сервісу
docker compose down             # зупинити й видалити контейнери й мережі (томи лишаються)
docker compose config           # підсумкова конфігурація після підстановки змінних і злиття файлів

docker compose config - найкорисніша команда для налагодження: показує, що Compose реально «бачить».

Докладніше в документації: Специфікація Compose

Тут дві різні речі, які часто плутають: змінні для самого файлу Compose і змінні всередині контейнера.

1. Підстановка (interpolation) - Compose заміняє ${...} у compose.yaml до запуску контейнерів. Значення беруться з оточення shell і з файлу .env поруч із compose.yaml:

services:
  app:
    image: myapp:${APP_VERSION:-latest}       # за замовчуванням latest
    ports:
      - "${APP_PORT:-8000}:8000"
    environment:
      DB_HOST: ${DB_HOST:?DB_HOST is required} # помилка, якщо не задано
  • ${VAR:-default} - значення за замовчуванням, якщо змінна порожня чи не задана;
  • ${VAR:?повідомлення} - зупинити з помилкою, якщо не задана;
  • $$ - буквальний знак долара.

2. Змінні контейнера - потрапляють в оточення процесу всередині:

services:
  app:
    environment:            # явно в compose.yaml
      APP_ENV: local
    env_file:               # з файлу - усі змінні файлу йдуть у контейнер
      - .env.docker

Ключова різниця: .env поруч із compose.yaml не передається в контейнер автоматично - він лише використовується для підстановки ${...}. А env_file передає змінні в контейнер, але не бере участі в підстановці.

Плутанина з Laravel: у Laravel-проєкті .env - це ще й файл конфігурації застосунку. Laravel Sail використовує це: той самий .env і для підстановки в compose.yaml (${APP_PORT}, ${FORWARD_DB_PORT}), і для застосунку, який читає його з каталогу проєкту через bind mount.

Пріоритет змінних контейнера (від вищого): docker compose run -e, environment у файлі, env_file, ENV в образі.

Перевірка: docker compose config показує файл після підстановки - видно, яке значення реально потрапило.

Безпека: .env з секретами - у .gitignore. Для продакшену секрети краще передавати механізмом секретів, а не змінними (їх видно в docker inspect).

Докладніше в документації: Підстановка змінних у Compose

Обидві команди виконують команду «в сервісі», але по-різному.

docker compose exec - виконує команду в уже запущеному контейнері сервісу:

docker compose exec app php artisan migrate
docker compose exec app sh                       # оболонка в працюючому контейнері
docker compose exec -u root app apk add htop     # від іншого користувача
  • контейнер має бути запущений;
  • команда бачить той самий стан: файли, процеси, змінні оточення, з'єднання;
  • після завершення контейнер продовжує працювати.

docker compose run - створює новий тимчасовий контейнер з конфігурації сервісу й виконує в ньому команду:

docker compose run --rm app composer install
docker compose run --rm app php artisan test
docker compose run --rm --no-deps node npm run build
  • працює, навіть якщо сервіс не запущено;
  • за замовчуванням запускає залежності (depends_on) - --no-deps вимикає це;
  • не публікує порти сервісу (щоб не конфліктувати із запущеним), якщо не вказати --service-ports;
  • --rm - видалити контейнер після завершення, інакше накопичуються зупинені контейнери.

Коли що:

Задача Команда
міграції, tinker, черга в робочому оточенні exec
подивитися, що відбувається в працюючому контейнері exec
одноразова задача без запущеного сервісу (встановлення залежностей, тести в CI) run --rm
команда в чистому оточенні, щоб не зачіпати запущений контейнер run --rm

Laravel Sail обгортає саме ці команди: sail artisan migrate - це docker compose exec laravel.test php artisan migrate, а sail shell - оболонка в працюючому контейнері.

Типова помилка: docker compose run app php artisan queue:work без --rm щодня - десятки забутих контейнерів. Подивитися їх: docker compose ps -a.

Докладніше в документації: docker compose run

Коли щось не працює в Compose-оточенні, є стандартний порядок діагностики.

1. Стан сервісів:

docker compose ps -a

Колонки STATUS (Up, Exited (1), Restarting) і (healthy)/(unhealthy) одразу показують, який сервіс проблемний. Exited (137) - зазвичай вбито через нестачу пам'яті, Exited (1) - помилка застосунку.

2. Логи:

docker compose logs app                 # усі логи сервісу
docker compose logs -f --tail=100 app   # останні 100 рядків і далі в реальному часі
docker compose logs --since 10m         # усі сервіси за 10 хвилин

Видно лише те, що процес пише в stdout/stderr. Якщо Laravel пише в storage/logs/laravel.log, у docker compose logs цього не буде - для контейнерів краще LOG_CHANNEL=stderr.

3. Вхід у контейнер:

docker compose exec app sh
# усередині: перевірити файли, змінні, з'єднання
env | grep DB_
nc -zv postgres 5432
php artisan about

Якщо контейнер одразу падає і exec неможливий - запустити той самий образ з іншою командою:

docker compose run --rm --entrypoint sh app

4. Конфігурація й деталі:

docker compose config                       # що Compose реально застосовує
docker inspect $(docker compose ps -q app)  # мережі, томи, змінні, healthcheck, причина зупинки
docker compose top                          # процеси в контейнерах
docker stats                                # пам'ять і CPU в реальному часі

Типові причини проблем і що перевірити:

  • «Connection refused» до бази - використано localhost замість імені сервісу (DB_HOST=postgres), база ще не готова (healthcheck), сервіси в різних мережах;
  • зміни коду не видно - немає bind mount, кеш конфігурації Laravel (php artisan optimize:clear), OPcache без перевірки часу змін;
  • права на файли - UID у контейнері не збігається з користувачем на хості;
  • порт зайнятий - інший процес на хості вже слухає той самий порт;
  • старий образ - після зміни Dockerfile потрібен docker compose up --build.

Docker Desktop має графічний інтерфейс для логів, терміналу й файлів контейнера - зручно для швидкого огляду.

Докладніше в документації: docker compose logs

Laravel Sail - легкий інструмент для локальної розробки Laravel у Docker. По суті це файл compose.yaml у корені проєкту і скрипт sail, що спрощує команди Docker Compose. Окремої «магії» немає - це звичайний Compose.

Встановлення:

composer require laravel/sail --dev
php artisan sail:install          # обрати сервіси: mysql, pgsql, redis, meilisearch, mailpit...
./vendor/bin/sail up -d

sail:install публікує compose.yaml і додає в .env змінні для підключення до сервісів у контейнерах. Додати сервіс пізніше - php artisan sail:add.

Скрипт sail - обгортка над docker compose:

Sail Що виконується
sail up -d docker compose up -d
sail artisan migrate php artisan migrate у контейнері застосунку
sail composer require ... Composer у контейнері
sail npm run dev Node у контейнері
sail test тести в контейнері
sail shell / sail root-shell оболонка в контейнері
sail tinker Tinker

Зручно зробити аліас: alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'.

Що варто знати:

  • версія PHP обирається в compose.yaml (образ на основі runtimes/8.5), підтримуються кілька версій;
  • WWWUSER/WWWGROUP - UID користувача в контейнері відповідає користувачу хоста, щоб файли, створені в контейнері, не належали root;
  • налаштування образів - sail artisan sail:publish копіює Dockerfile-и в каталог docker/ для змін (розширення PHP, пакети);
  • Xdebug вмикається змінною SAIL_XDEBUG_MODE у .env (develop,debug,coverage);
  • порти змінюються в .env (APP_PORT, FORWARD_DB_PORT), якщо стандартні зайняті.

Чого Sail не робить: це інструмент розробки, а не продакшен-оточення. Образи Sail розраховані на зручність (вбудований сервер, інструменти, Node), а не на безпеку чи розмір. Для продакшену будують власні образи (наприклад, на FrankenPHP чи php-fpm + nginx) або використовують хостинг на кшталт Laravel Cloud.

Альтернативи: Laravel Herd (нативно на macOS/Windows без Docker), DDEV, власний compose.yaml.

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

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

Контейнери Linux потребують ядра Linux. На macOS (і Windows) Docker Desktop запускає легку віртуальну машину, і контейнери працюють усередині неї. Файли проєкту лежать на диску macOS, а процес у контейнері читає їх через межу віртуальної машини - механізм спільного доступу до файлів.

Чому це повільно для PHP і Node-проєктів: проблема не в розмірі файлів, а в їх кількості. Автозавантажувач Composer, node_modules, кеші фреймворку - кожен запит до Laravel відкриває й перевіряє сотні файлів; збирання фронтенду - тисячі. Кожна операція з файлом через межу ВМ коштує набагато дорожче, ніж на локальному диску.

Механізми й еволюція:

  • gRPC FUSE, osxfs - старі механізми, дуже повільні на великих проєктах;
  • VirtioFS - сучасний механізм за замовчуванням на macOS, значно швидший, але на великих репозиторіях усе ще відчутно повільніший за нативну файлову систему;
  • Synchronized file shares - Docker Desktop тримає синхронізовану копію каталогу всередині ВМ (з файловим кешем), і контейнери читають її з нативною швидкістю. Розрахований на великі репозиторії (сотні тисяч файлів). Доступний у платних підписках Docker (Pro, Team, Business).

Практичні прийоми, що працюють незалежно від механізму:

  1. не монтувати залежності: vendor і node_modules - в іменованих томах (лишаються всередині ВМ) або встановлювати в образі;
  2. монтувати лише потрібне: не весь проєкт з .git, логами й кешем, а каталоги з кодом;
  3. кеші й скомпільовані файли (storage/framework, bootstrap/cache) - у томі чи tmpfs, а не на bind mount;
  4. OPcache з validate_timestamps і розумною частотою перевірки - менше звернень до файлової системи;
  5. docker compose watch з sync - копіювати змінені файли в контейнер замість спільного доступу;
  6. ресурси ВМ: достатньо пам'яті й процесорів у налаштуваннях Docker Desktop.

Альтернативи Docker Desktop: OrbStack (швидший доступ до файлів і менше споживання ресурсів на macOS), Colima. Або відмовитися від Docker для самого PHP у розробці - Laravel Herd запускає PHP нативно, а в Docker лишаються лише бази й допоміжні сервіси.

На Linux проблеми немає: контейнери працюють на тому самому ядрі, і bind mount - це звичайне монтування без накладних витрат.

Докладніше в документації: Синхронізовані файлові спільні каталоги

Коли compose.yaml росте до десятків сервісів, з'являються дублювання й конфлікти. Compose має кілька механізмів структурування.

1. include (Compose 2.20+) - підключити інший файл Compose як окремий підпроєкт зі своїми відносними шляхами:

# compose.yaml
include:
  - infra/compose.yaml          # бази, Redis, пошук
  - path: tools/compose.yaml    # інструменти розробки
    env_file: tools/.env

services:
  app:
    build: .
    depends_on: [postgres, redis]   # сервіси з підключених файлів доступні

На відміну від злиття через -f, кожен підключений файл розв'язує шляхи відносно свого розташування, тож команда, що відповідає за інфраструктуру, може тримати свій файл окремо. Конфлікт імен сервісів - помилка, а не тихе злиття.

2. extends - успадкувати конфігурацію сервісу з того самого чи іншого файлу:

services:
  php-base:
    build: .
    environment:
      APP_ENV: local
    volumes:
      - .:/var/www/html

  app:
    extends: php-base
    ports: ["127.0.0.1:8000:8000"]

  worker:
    extends: php-base
    command: php artisan queue:work

  scheduler:
    extends: php-base
    command: php artisan schedule:work

3. Якорі YAML і поля розширення x-:

x-php: &php
  build: .
  env_file: .env
  volumes: [".:/var/www/html"]

services:
  app:
    <<: *php
    ports: ["127.0.0.1:8000:8000"]
  worker:
    <<: *php
    command: php artisan queue:work

Поля верхнього рівня з префіксом x- Compose ігнорує, тож туди зручно класти спільні фрагменти. Злиття <<: - поверхневе: вкладені словники заміняються повністю, а не зливаються.

Як обрати:

  • кілька незалежних частин (інфраструктура, застосунок, інструменти), різні власники - include;
  • кілька ролей одного образу (web, worker, scheduler) - extends чи якорі YAML;
  • різні оточення для тих самих сервісів - кілька файлів через -f чи профілі.

Перевірка результату - завжди docker compose config: усі три механізми розгортаються в підсумкову конфігурацію, і саме її варто перевіряти, а не покладатися на уявлення про злиття.

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

Xdebug - налагоджувач PHP: точки зупинки, покрокове виконання, перегляд змінних. У Docker головна складність - мережева: Xdebug сам ініціює з'єднання з IDE (порт 9003), а IDE працює на хості, поза контейнером.

Мінімальна конфігурація PHP у контейнері:

[xdebug]
xdebug.mode=debug,develop
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.start_with_request=trigger   ; лише за запитом, а не на кожен

host.docker.internal - ім'я, що вказує на хост із контейнера. На Docker Desktop (macOS, Windows) воно працює автоматично; на Linux його треба додати явно:

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"

У Laravel Sail це вже налаштовано: досить змінної в .env - SAIL_XDEBUG_MODE=develop,debug,coverage - і перезапуску контейнерів. Для CLI-команд - sail debug artisan ....

Налаштування IDE (PhpStorm):

  • слухати вхідні з'єднання налагодження на порту 9003;
  • відображення шляхів (path mappings): код у контейнері лежить у /var/www/html, а на хості - у ~/projects/app. Без відображення IDE отримує з'єднання, але не може зіставити файли й не зупиняється на точках;
  • ім'я сервера (PHP_IDE_CONFIG=serverName=...) збігається з налаштуваннями сервера в IDE.

Чому з'єднання не приходить - чек-лист:

  1. Xdebug не ввімкнено: php -v у контейнері має показувати Xdebug, php -i | grep xdebug.mode;
  2. немає тригера при start_with_request=trigger - потрібне розширення браузера Xdebug Helper чи параметр XDEBUG_TRIGGER;
  3. неправильний client_host - особливо на Linux без host-gateway;
  4. фаєрвол хоста блокує вхідний порт 9003;
  5. IDE не слухає чи слухає інший порт (старий Xdebug 2 використовував 9000);
  6. журнал Xdebug показує причину: xdebug.log=/tmp/xdebug.log - там видно спробу з'єднання і помилку.

Продуктивність: Xdebug помітно сповільнює PHP навіть без активного налагодження. Тому start_with_request=trigger чи окремий образ/профіль з Xdebug, а не постійно ввімкнений режим. У продакшені Xdebug не встановлюють узагалі.

Докладніше в документації: Laravel Sail: налагодження з Xdebug

Ресурси Compose (контейнери, мережі, томи) належать проєкту. Назва проєкту - префікс усіх ресурсів: myapp-app-1, myapp_default, myapp_pgdata. За замовчуванням назва - ім'я каталогу з compose.yaml; явно задається полем name: у файлі, змінною COMPOSE_PROJECT_NAME чи прапорцем -p.

Рівні «скидання» - від м'якого до радикального:

docker compose restart app            # перезапустити процес, контейнер той самий
docker compose up -d --force-recreate # перестворити контейнери (записуваний шар скинуто)
docker compose up -d --build          # перезібрати образи й перестворити
docker compose down                   # видалити контейнери й мережі проєкту, ТОМИ ЛИШАЮТЬСЯ
docker compose down -v                # + іменовані томи проєкту: дані бази ЗНИКНУТЬ
docker compose down --rmi local       # + образи, зібрані для проєкту

Головне - розуміти, що де живе:

  • записуваний шар контейнера зникає при перестворенні;
  • іменовані томи (pgdata) переживають down, але не down -v;
  • bind mount - це файли хоста, їх Docker не видаляє ніколи;
  • анонімні томи накопичуються, якщо не видаляти їх разом з контейнерами (down -v чи docker compose rm -v).

Небезпечні звички:

  • docker system prune -a --volumes - чистить усе невикористовуване на машині: томи, образи, мережі всіх проєктів, включно з базами сусідніх проєктів, які зараз просто не запущені. Для чистки одного проєкту - команди docker compose з його назвою;
  • однакова назва проєкту для двох різних каталогів (обидва app/) - вони ділять мережі й томи, і down -v в одному зносить дані іншого. Явне name: у compose.yaml знімає проблему;
  • down -v «за звичкою» при кожному перезапуску - щоразу порожня база й повторні міграції з сидерами.

Безпечне скидання бази розробки:

docker compose exec app php artisan migrate:fresh --seed   # схема й тестові дані
# або точково видалити один том
docker compose down
docker volume rm myapp_pgdata
docker compose up -d

Перед видаленням томів із цінними даними - бекап: docker compose exec -T postgres pg_dump -U postgres app > backup.sql.

Перелік ресурсів проєкту: docker compose ps -a, docker volume ls --filter label=com.docker.compose.project=myapp.

Докладніше в документації: Назва проєкту Compose