---
title: 'Dockerfile для PHP і Laravel: найкращі практики | DevSense'
description: 'Як зібрати production-образ Laravel: multi-stage Dockerfile для PHP-FPM і Vite-асетів, кеш шарів Composer і npm, OPcache, non-root, секрети, healthcheck і один образ для web, воркерів і планувальника.'
faq:
    - { question: 'Чи можна використовувати образ Laravel Sail у продакшені?', answer: 'Ні. Образ Sail розрахований на розробку: усередині Xdebug, Node.js, клієнти баз даних та інші інструменти, а застосунок запускається через php artisan serve — однопотоковий сервер для розробки. Для продакшену збирають окремий мінімальний образ на PHP-FPM (або FrankenPHP) без dev-залежностей.' }
    - { question: 'Навіщо потрібен multi-stage Dockerfile для PHP?', answer: 'Composer, Node.js, npm-пакети й компілятори потрібні лише під час збирання. У multi-stage збиранні залежності й асети збираються в окремих стадіях, а в підсумковий образ копіюються лише vendor/ і public/build. Образ виходить меншим, у ньому менше вразливого ПЗ, а шари краще кешуються.' }
    - { question: 'Коли виконувати php artisan config:cache — під час збирання образу чи під час старту контейнера?', answer: 'Під час старту контейнера. config:cache запікає значення змінних оточення у файл. Якщо зробити це під час збирання, в образ потраплять змінні середовища збирання, а продакшен-налаштування, передані контейнеру, буде проігноровано. Те саме стосується route:cache, якщо маршрути залежать від конфігурації.' }
    - { question: 'Що обрати для PHP-образу: Alpine чи Debian?', answer: 'Alpine дає менший розмір, але використовує musl замість glibc: деякі розширення збираються довше, а поведінка окремих бібліотек і продуктивність можуть відрізнятися. Debian-образ (за замовчуванням в офіційного php) більший, зате передбачуваніший. Для більшості Laravel-проєктів розумний вибір — Debian, а Alpine — коли розмір критичний і ви перевірили свій застосунок на musl.' }
published: '2026-10-03'
---
# Dockerfile для PHP і Laravel: production-образ без зайвого

Локально у вас Sail: `sail up`, і все працює. Потім настає час продакшену, і найшвидший шлях — узяти той самий образ, додати `COPY . .` і запустити. Через місяць з'ясовується, що образ важить понад гігабайт, усередині Xdebug і Node.js, застосунок обслуговує `php artisan serve` в один потік, кожен білд заново завантажує всі Composer-пакети, а `.env` із ключами бази лежить просто в шарі образу.

Добрий production-образ Laravel влаштований інакше: залежності й асети збираються в окремих стадіях, у підсумковий образ потрапляє лише те, що потрібно для роботи, процес запускається не від root, а конфігурація надходить з оточення під час старту. У цій статті — повний Dockerfile з розбором кожного рішення і схема запуску web, воркерів і планувальника з одного образу.

**Навігація:** [Усі інструменти](../) · [Sail: огляд](sail) · [Sail: оточення й деплой](sail-env-deploy) · [Sail: черги](sail-queues) · [Zero-downtime deployment](../architecture/zero-downtime-deployment-laravel)

## Зміст

* [Чому образ Sail не підходить для продакшену](#sail-vs-prod)
* [Архітектура: один образ — кілька процесів](#image-layout)
* [.dockerignore: що не має потрапити в контекст](#dockerignore)
* [Multi-stage Dockerfile повністю](#multistage)
* [Порядок шарів і кеш збирання](#layer-cache)
* [PHP-FPM і OPcache для продакшену](#php-config)
* [Конфігурація й секрети](#config)
* [Web, воркери й планувальник з одного образу](#processes)
* [Healthcheck і м'яка зупинка](#healthchecks)
* [Безпека образу](#security)
* [Alpine, Debian чи FrankenPHP](#base-image)
* [Часті помилки](#common-mistakes)
* [Чеклист](#checklist)
* [Квіз для самоперевірки](#self-test-quiz)

---

<a id="sail-vs-prod"></a>
## Чому образ Sail не підходить для продакшену

Sail — чудовий інструмент розробки, і саме тому він не годиться для бойового оточення:

* застосунок запускається через `php artisan serve` під Supervisor — це вбудований сервер PHP для розробки, а не PHP-FPM;
* усередині Xdebug, Node.js, npm, клієнти MySQL/PostgreSQL та інші утиліти — більший розмір і більша поверхня атаки;
* код монтується з хоста томом, а не копіюється в образ — образ сам по собі не містить застосунку;
* права й UID підлаштовуються під користувача хоста під час старту контейнера.

Production-образ — окремий артефакт зі своїм Dockerfile. Sail при цьому залишається для локальної розробки.

---

<a id="image-layout"></a>
## Архітектура: один образ — кілька процесів

Принцип «один процес на контейнер» для Laravel означає: **один образ застосунку**, з якого запускаються різні процеси.

```
                 ┌──────────────────────┐
  HTTP ────────► │ web (nginx)          │ static files from public/
                 └──────────┬───────────┘
                            │ FastCGI :9000
                 ┌──────────▼───────────┐
                 │ app (php-fpm)        │ ┐
                 └──────────────────────┘ │
                 ┌──────────────────────┐ │  same image,
                 │ queue (queue:work)   │ ├─ different command
                 └──────────────────────┘ │
                 ┌──────────────────────┐ │
                 │ scheduler            │ ┘
                 │ (schedule:work)      │
                 └──────────────────────┘
```

Так у всіх процесів гарантовано однаковий код і однакові розширення, а масштабуються вони незалежно: більше воркерів під час імпорту, більше PHP-FPM у години пік. nginx збирається окремою маленькою стадією, у яку копіюється лише `public/`.

---

<a id="dockerignore"></a>
## .dockerignore: що не має потрапити в контекст

Усе, що лежить у каталозі збирання, надсилається демону Docker як контекст. Без `.dockerignore` в образ через `COPY . .` потраплять `.git`, локальний `vendor/`, `node_modules/` і — найнебезпечніше — `.env`.

```gitignore
# .dockerignore
.git
.env
.env.*
!.env.example
node_modules
vendor
public/build
public/hot
public/storage
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*.php
tests
docker-compose*.yml
*.log
```

`vendor/` і `public/build/` виключаються, бо збираються всередині Docker — із правильною версією PHP, без dev-пакетів і відтворювано.

---

<a id="multistage"></a>
## Multi-stage Dockerfile повністю

```dockerfile
# syntax=docker/dockerfile:1
ARG PHP_VERSION=8.5

# ---------- base: PHP runtime with the extensions the app needs ----------
FROM php:${PHP_VERSION}-fpm AS base

COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pdo_pgsql redis intl zip bcmath pcntl \
 && mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

COPY docker/php/conf.d/ $PHP_INI_DIR/conf.d/
COPY docker/php-fpm/zz-app.conf /usr/local/etc/php-fpm.d/zz-app.conf

WORKDIR /var/www/html

# ---------- vendor: production Composer dependencies ----------
FROM base AS vendor

ENV COMPOSER_HOME=/tmp/composer \
    COMPOSER_CACHE_DIR=/tmp/composer/cache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer/cache \
    composer install --no-dev --no-scripts --no-autoloader \
        --prefer-dist --no-interaction --no-progress

# ---------- assets: Vite build ----------
FROM node:24-alpine AS assets
WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund

COPY vite.config.* ./
COPY resources/ ./resources/
RUN npm run build

# ---------- runtime: the image that runs in production ----------
FROM base AS runtime

COPY --from=vendor /var/www/html/vendor ./vendor
COPY . .
COPY --from=assets /app/public/build ./public/build

# Composer is mounted only for this step and never ends up in the image.
RUN --mount=type=bind,from=composer:2,source=/usr/bin/composer,target=/usr/local/bin/composer \
    composer dump-autoload --optimize --no-dev --no-interaction \
 && mkdir -p storage/framework/cache storage/framework/sessions storage/framework/views \
             storage/logs bootstrap/cache \
 && chown -R www-data:www-data storage bootstrap/cache

COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/entrypoint

USER www-data
EXPOSE 9000
ENTRYPOINT ["entrypoint"]
CMD ["php-fpm"]

# ---------- web: nginx with public assets only ----------
FROM nginx:stable-alpine AS web
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=runtime /var/www/html/public /var/www/html/public
```

Збирання двох образів з одного файлу:

```bash
docker build --target runtime -t registry.example.com/shop:1.42.0 .
docker build --target web     -t registry.example.com/shop-web:1.42.0 .
```

Розбір стадій:

* **`base`** — PHP з розширеннями. `install-php-extensions` сам ставить системні бібліотеки й видаляє компілятори після збирання розширення. `php.ini-production` вмикає продакшен-налаштування: помилки не виводяться у відповідь, `expose_php` вимкнено. У PHP 8.5 OPcache завжди входить до ядра; на PHP 8.4 і нижче додайте `opcache` до списку розширень.
* **`vendor`** — лише `composer.json` і `composer.lock`, без коду застосунку. `--no-scripts --no-autoloader` потрібні тому, що скрипти Laravel (`package:discover`) потребують коду, якого на цій стадії ще немає. Стадія успадковується від `base`, тому Composer перевіряє вимоги до версії PHP і розширень — `--ignore-platform-reqs` не потрібен і шкідливий.
* **`assets`** — Node.js потрібен лише тут. Якщо Tailwind сканує Blade-шаблони або класи пагінації з `vendor/`, скопіюйте в цю стадію і їх.
* **`runtime`** — підсумковий образ: код, `vendor/`, зібрані асети. `composer dump-autoload --optimize` генерує classmap і запускає `package:discover`. Composer підключається через `--mount=type=bind` лише на час цієї команди.
* **`web`** — nginx і вміст `public/`. PHP-коду в ньому немає.

Конфігурація nginx для цієї схеми:

```nginx
# docker/nginx/default.conf
server {
    listen 80;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}
```

---

<a id="layer-cache"></a>
## Порядок шарів і кеш збирання

Docker перезбирає шар, якщо змінилися його вхідні дані, і всі шари після нього. Звідси головне правило: **спочатку те, що змінюється рідко, потім те, що часто**.

* `composer.json` і `composer.lock` копіюються окремо від коду — правка Blade-шаблону не запускає `composer install` заново.
* `package.json` і `package-lock.json` — так само для npm.
* `COPY . .` стоїть у самому кінці, після всіх важких кроків.

Другий рівень — **кеш-монтування BuildKit**: `RUN --mount=type=cache,target=...`. Навіть коли шар із залежностями перезбирається (ви додали пакет), Composer і npm беруть уже завантажені архіви з кешу, а не з мережі. Кеш не потрапляє в образ.

У CI кеш між запусками зберігають через `docker buildx build --cache-to/--cache-from` (наприклад, у registry або кеш GitHub Actions). Без цього кожен білд у чистому раннері починається з нуля.

---

<a id="php-config"></a>
## PHP-FPM і OPcache для продакшену

```ini
; docker/php/conf.d/opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
; Code never changes inside a running container: skip file timestamp checks.
opcache.validate_timestamps=0
```

```ini
; docker/php/conf.d/app.ini
memory_limit=256M
upload_max_filesize=20M
post_max_size=25M
expose_php=Off
```

```ini
; docker/php-fpm/zz-app.conf
[www]
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 1000
ping.path = /ping
clear_env = no
```

* **`opcache.validate_timestamps=0`** — у контейнері код незмінний, тому PHP не потрібно перевіряти час зміни файлів на кожному запиті. Новий код надходить лише з новим контейнером.
* **`pm.max_children`** рахують від пам'яті: якщо контейнеру виділено 1 ГБ, а воркер PHP-FPM займає близько 50 МБ, понад 15–18 процесів він не витримає. Виміряйте реальне споживання під навантаженням.
* **`pm.max_requests`** перезапускає процес FPM після N запитів — страховка від повільних витоків пам'яті.
* **`clear_env = no`** — за замовчуванням PHP-FPM очищає змінні оточення для своїх процесів. Якщо конфігурацію не закешовано, застосунок їх не побачить.

Логи застосунку в контейнері мають іти в stdout/stderr, а не у файл усередині контейнера: `LOG_CHANNEL=stderr`. Тоді їх збирає Docker або оркестратор, і вони не губляться під час перестворення контейнера.

---

<a id="config"></a>
## Конфігурація й секрети

**В образі немає `.env`.** Той самий образ має запускатися на стейджингу й у продакшені — різниця лише в змінних оточення, переданих контейнеру. Образ, у якому лежить `.env`, прив'язаний до одного оточення й роздає секрети всім, хто має доступ до registry.

**Кеші Laravel збираються під час старту контейнера, а не під час збирання.** `config:cache` запікає поточні значення змінних оточення в `bootstrap/cache/config.php`. Якщо виконати його в Dockerfile, в образ потраплять значення середовища збирання. З маршрутами та сама історія, якщо вони залежать від конфігурації. Наприклад, у DevSense маршрут ключа IndexNow реєструється, лише якщо задано `config('seo.indexnow_key')`, — `route:cache` під час збирання зафіксував би набір маршрутів без нього.

```sh
#!/bin/sh
# docker/entrypoint.sh
set -e

# Build config, route, view and event caches from the runtime environment.
if [ "${LARAVEL_OPTIMIZE:-true}" = "true" ]; then
    php artisan optimize
fi

# exec replaces the shell, so PHP becomes PID 1 and receives SIGTERM directly.
exec "$@"
```

**Міграції — окремий крок релізу, а не частина entrypoint.** Якщо `migrate --force` виконується під час старту кожного контейнера, десять реплік запустять міграції одночасно. Запускайте їх один раз перед перемиканням трафіку: `docker compose run --rm app php artisan migrate --force` або окремою задачею в оркестраторі. Як писати міграції, сумісні з робочим кодом, — у статті про [деплой без простою](../architecture/zero-downtime-deployment-laravel#migrations).

**Секрети на етапі збирання — через `--mount=type=secret`.** Якщо для `composer install` потрібен токен приватного репозиторію, не передавайте його через `ARG` або `ENV`: значення видно в історії образу (`docker history`). BuildKit монтує секрет лише на час однієї команди:

```dockerfile
RUN --mount=type=cache,target=/tmp/composer/cache \
    --mount=type=secret,id=composer_auth,target=/tmp/composer/auth.json \
    composer install --no-dev --no-scripts --no-autoloader --prefer-dist --no-interaction
```

```bash
docker build --secret id=composer_auth,src=$HOME/.config/composer/auth.json --target runtime .
```

---

<a id="processes"></a>
## Web, воркери й планувальник з одного образу

```yaml
# docker-compose.prod.yml
services:
  app:
    image: registry.example.com/shop:${RELEASE}
    env_file: .env.production
    init: true
    restart: unless-stopped

  web:
    image: registry.example.com/shop-web:${RELEASE}
    ports: ["80:80"]
    depends_on: [app]
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/up"]
      interval: 10s
      timeout: 3s
      retries: 3

  queue:
    image: registry.example.com/shop:${RELEASE}
    env_file: .env.production
    command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=120", "--max-time=3600", "--memory=256"]
    init: true
    stop_grace_period: 180s
    restart: unless-stopped

  scheduler:
    image: registry.example.com/shop:${RELEASE}
    env_file: .env.production
    command: ["php", "artisan", "schedule:work"]
    init: true
    restart: unless-stopped
```

* `command` перевизначає `CMD`, а `ENTRYPOINT` з `optimize` спрацьовує для всіх процесів.
* `restart: unless-stopped` замінює Supervisor: `queue:work`, що завершився за `--max-time` або після `queue:restart`, підніметься заново.
* `init: true` запускає маленький init-процес, який пересилає сигнали й прибирає «зомбі»-процеси.
* `schedule:work` — планувальник без cron: команда щохвилини запускає `schedule:run`. У Kubernetes замість нього часто використовують CronJob із `schedule:run`.

Параметри воркерів — таймаути, повтори, пам'ять — докладно розібрано в статті про [черги в продакшені](../architecture/laravel-queues-production).

---

<a id="healthchecks"></a>
## Healthcheck і м'яка зупинка

**Перевірка здоров'я.** У Laravel 11+ є вбудований маршрут `/up` (параметр `health` у `bootstrap/app.php`): він відповідає `200`, якщо застосунок завантажився, і `500`, якщо ні. Перевірка через nginx (`wget` є в образі на Alpine) перевіряє весь ланцюжок nginx → PHP-FPM → Laravel. Для окремої перевірки PHP-FPM є `ping.path = /ping` з конфігурації пулу; запитати його можна утилітою `cgi-fcgi` з пакета fcgi. Kubernetes ігнорує інструкцію `HEALTHCHECK` з Dockerfile і використовує свої readiness- і liveness-проби — ті самі URL підходять і для них.

**М'яка зупинка.** `docker stop` надсилає головному процесу `SIGTERM`, а через `stop_grace_period` (за замовчуванням 10 секунд) — `SIGKILL`. Що потрібно, щоб зупинка була м'якою:

* головний процес — сам PHP, а не shell: в entrypoint стоїть `exec "$@"`, а `CMD` записано в exec-формі (`["php-fpm"]`, а не `php-fpm` рядком);
* у воркера черги встановлено розширення **pcntl** — `queue:work` отримує `SIGTERM`, допрацьовує поточну задачу й виходить;
* `stop_grace_period` більший за найдовшу задачу — інакше Docker уб'є воркер посеред роботи. Це той самий принцип, що й `stopwaitsecs` у Supervisor.

---

<a id="security"></a>
## Безпека образу

* **Не root.** `USER www-data` наприкінці Dockerfile. Якщо в застосунку знайдуть RCE, атакувальник отримає права `www-data` всередині контейнера, а не root. PHP-FPM слухає порт 9000, привілеї для нього не потрібні. Попередження FPM про директиву `user`, яка ігнорується без root, безпечне.
* **Мінімум ПЗ.** Жодного Xdebug, Node.js, Composer, git і компіляторів у підсумковому образі. Чого немає, того не можна експлуатувати.
* **Зафіксовані версії.** `php:8.5-fpm` змінюється з кожним патч-релізом. Для відтворюваності фіксуйте digest: `FROM php:8.5-fpm@sha256:...`, а оновлення базових образів автоматизуйте через Dependabot або Renovate.
* **Регулярне перезбирання.** Вразливості в OpenSSL чи glibc закриваються новим базовим образом — навіть якщо ваш код не змінювався, образ потрібно перезбирати хоча б раз на тиждень-два.
* **Сканування.** `docker scout cves` або `trivy image` у CI знаходять відомі вразливості в системних пакетах і залежностях. Для PHP-залежностей лишаються `composer audit` і `npm audit`.
* **Лише для читання.** На наступному рівні кореневу файлову систему контейнера монтують `read_only: true`, а доступними для запису залишають лише `storage/`, `bootstrap/cache` і `/tmp`.

Налаштування сервера навколо контейнерів — заголовки безпеки, TLS, ліміти — описано в статті про [hardening серверів та інфраструктури](../security/server-and-infrastructure-hardening).

---

<a id="base-image"></a>
## Alpine, Debian чи FrankenPHP

| Варіант | Плюси | Мінуси |
|---------|-------|--------|
| `php:8.x-fpm` (Debian) | Передбачуваний glibc, швидкі готові пакети, менше сюрпризів | Образ більший |
| `php:8.x-fpm-alpine` | Малий розмір | musl замість glibc: довше збирання розширень, відмінності в поведінці окремих бібліотек і продуктивності |
| FrankenPHP | Один процес: вебсервер Caddy і PHP разом, HTTPS, режим воркера для Laravel Octane | Інша модель виконання: у режимі воркера стан живе між запитами, витоки й «брудні» синглтони стають вашою проблемою |

Для типового Laravel-проєкту розумний старт — Debian-образ із PHP-FPM і nginx, як у цій статті. Alpine має сенс, коли розмір справді важливий і ви перевірили застосунок на musl. Моделі виконання PHP — FPM, воркери, event loop — порівнюються в статті [PHP на сервері](../php/runtimes).

---

<a id="common-mistakes"></a>
## Типові помилки

**1. Образ Sail або dev-образ у продакшені.**
`artisan serve`, Xdebug і Node.js у бойовому оточенні: повільно й небезпечно.

**2. `.env` усередині образу.**
Секрети в кожному шарі й у кожного, хто може завантажити образ. Конфігурація — лише через оточення.

**3. `config:cache` у Dockerfile.**
В образ запікаються змінні середовища збирання, продакшен-налаштування ігноруються.

**4. `COPY . .` до `composer install`.**
Будь-яка правка коду інвалідує кеш залежностей, і кожен білд завантажує всі пакети заново.

**5. `--ignore-platform-reqs` у Composer.**
Збирання проходить, а в рантаймі бракує розширення PHP. Встановлюйте залежності на тому самому PHP, що й у продакшені.

**6. Секрети через `ARG` або `ENV`.**
Значення видно в `docker history`. Використовуйте `--mount=type=secret`.

**7. Shell-форма `CMD` і entrypoint без `exec`.**
`SIGTERM` отримує shell, а не PHP. Воркер не зупиняється м'яко й убивається через 10 секунд посеред задачі.

**8. `migrate --force` в entrypoint кожної репліки.**
Кілька контейнерів запускають міграції одночасно. Міграції — окремий крок релізу.

**9. Процес від root.**
Будь-яка вразливість у застосунку одразу дає права root усередині контейнера.

---

<a id="checklist"></a>
## Чеклист

1. Є `.dockerignore`: `.env`, `.git`, `vendor/`, `node_modules/` не потрапляють у контекст.
2. Multi-stage збирання: Composer і Node.js лише в стадіях збирання.
3. `composer.json`/`composer.lock` і `package*.json` копіюються до коду; використовуються кеш-монтування BuildKit.
4. Залежності встановлюються на тому самому PHP з тими самими розширеннями, що в рантаймі, без `--ignore-platform-reqs`.
5. `php.ini-production`, OPcache з `validate_timestamps=0`, `pm.max_children` розраховано за пам'яттю.
6. В образі немає `.env` і секретів; приватні токени — через `--mount=type=secret`.
7. `php artisan optimize` виконується під час старту контейнера; міграції — окремим кроком релізу.
8. Процес працює від `www-data`; базовий образ зафіксовано за digest і регулярно перезбирається.
9. web, queue і scheduler запускаються з одного образу з різними командами.
10. Є healthcheck за `/up`, `exec` в entrypoint, pcntl і `stop_grace_period`, більший за найдовшу задачу.
11. Образ сканується на вразливості в CI.

---

## Підсумок

Production-образ Laravel — це не «Sail плюс COPY», а окремий артефакт: зібраний у кілька стадій, містить лише потрібне для роботи, нічого не знає про конкретне оточення й запускається без root. Порядок шарів і кеш BuildKit роблять збирання швидким, `optimize` під час старту підхоплює справжню конфігурацію, а один образ для web, воркерів і планувальника гарантує, що всі процеси працюють на тому самому коді.

---

<a id="self-test-quiz"></a>
## Квіз для самоперевірки

### Питання 1: Чому `composer.json` і `composer.lock` копіюють в образ окремо й раніше за решту коду?
- А) Composer не вміє читати файли, скопійовані командою `COPY . .`.
- Б) Щоб шар зі встановленими залежностями перевикористовувався з кешу, доки не змінилися самі маніфести, а правки коду не запускали `composer install` заново.
- В) Цього вимагає формат multi-stage збирання.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
Docker перезбирає шар у разі зміни його вхідних даних. Якщо залежності ставляться після `COPY . .`, будь-яка правка шаблону інвалідує шар із `vendor/`. Окреме копіювання маніфестів відв'язує встановлення залежностей від змін коду.
</details>

### Питання 2: Команда `php artisan config:cache` виконується в Dockerfile. Що станеться в продакшені?
- А) Нічого особливого: Laravel перечитає змінні оточення під час старту.
- Б) Застосунок використовуватиме значення, які були в оточенні під час збирання, а змінні, передані контейнеру, проігнорує.
- В) Збирання впаде, бо `config:cache` не можна виконувати без бази даних.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: Б**
Закешована конфігурація не читає `env()` повторно. Кеш потрібно збирати під час старту контейнера, коли змінні оточення вже передано, — наприклад, `php artisan optimize` в entrypoint.
</details>

### Питання 3: Під час `docker stop` воркер черги обривається посеред задачі. Що з переліченого НЕ допоможе?
- А) `exec "$@"` в entrypoint і exec-форма `CMD`, щоб `SIGTERM` отримував PHP.
- Б) Встановлене розширення pcntl і `stop_grace_period`, більший за найдовшу задачу.
- В) Збільшити `memory_limit` у `php.ini`.

<details>
<summary>Показати правильну відповідь</summary>

**Правильна відповідь: В**
М'яка зупинка залежить від доставки сигналу процесу PHP, обробки `SIGTERM` (pcntl) і часу, який Docker дає до `SIGKILL`. Ліміт пам'яті до цього не має стосунку.
</details>