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