---
title: 'Dockerfile за PHP и Laravel: най-добри практики | DevSense'
description: 'Как да изградите production образ за Laravel: multi-stage Dockerfile за PHP-FPM и Vite асети, кеширане на слоевете за Composer и npm, OPcache, non-root, секрети, healthcheck и един образ за web, worker-и и планировчик.'
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, worker-и и планировчик от един образ.

**Навигация:** [Всички инструменти](../) · [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, worker-и и планировчик от един образ](#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 и други инструменти — по-голям размер и по-голяма повърхност за атака;
* кодът се монтира от хоста като volume, а не се копира в образа — самият образ не съдържа приложението;
* правата и 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)      │
                 └──────────────────────┘
```

Така всички процеси гарантирано имат еднакъв код и еднакви разширения, а се мащабират независимо: повече worker-и по време на импорт, повече 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 . .` стои най-накрая, след всички тежки стъпки.

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

В CI кешът между стартиранията се запазва чрез `docker buildx build --cache-to/--cache-from` (например в registry или в кеша на GitHub Actions). Без това всеки билд в чист runner започва от нулата.

---

<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 GB, а един PHP-FPM worker заема около 50 MB, той няма да издържи повече от 15–18 процеса. Измерете реалното потребление под натоварване.
* **`pm.max_requests`** рестартира FPM процеса след N заявки — застраховка срещу бавни изтичания на памет.
* **`clear_env = no`** — по подразбиране PHP-FPM изчиства променливите на средата за своите процеси. Ако конфигурацията не е кеширана, приложението няма да ги види.

Логовете на приложението в контейнера трябва да отиват в stdout/stderr, а не във файл вътре в контейнера: `LOG_CHANNEL=stderr`. Тогава ги събира Docker или оркестраторът и те не се губят при пресъздаване на контейнера.

---

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

**В образа няма `.env`.** Един и същ образ трябва да се стартира и на staging, и в продукция — разликата е само в променливите на средата, подадени на контейнера. Образ, в който лежи `.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, worker-и и планировчик от един образ

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

Параметрите на worker-ите — таймаути, повторни опити, памет — са разгледани подробно в статията за [опашките в продукция](../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` като низ);
* при worker-а на опашката е инсталирано разширението **pcntl** — `queue:work` получава `SIGTERM`, довършва текущата задача и излиза;
* `stop_grace_period` е по-голям от най-дългата задача — иначе Docker ще убие worker-а по средата на работата. Това е същият принцип като `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` се променя при всеки patch релийз. За възпроизводимост фиксирайте 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`.

Настройките на сървъра около контейнерите — security заглавки, 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, worker режим за Laravel Octane | Друг модел на изпълнение: в worker режим състоянието живее между заявките, а изтичанията и „мръсните“ singleton-и стават ваш проблем |

За типичен Laravel проект разумното начало е Debian образ с PHP-FPM и nginx, както в тази статия. Alpine има смисъл, когато размерът наистина е важен и сте проверили приложението с musl. Моделите на изпълнение на PHP — FPM, worker-и, 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. Worker-ът не спира плавно и бива убит след 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` се копират преди кода; използват се cache mount-овете на BuildKit.
4. Зависимостите се инсталират със същия PHP и със същите разширения като в runtime, без `--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, worker-и и планировчик гарантира, че всички процеси работят с един и същ код.

---

<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` worker-ът на опашката се прекъсва по средата на задача. Кое от изброените НЯМА да помогне?
- А) `exec "$@"` в entrypoint и exec формата на `CMD`, за да получава `SIGTERM` PHP.
- Б) Инсталирано разширение pcntl и `stop_grace_period`, по-голям от най-дългата задача.
- В) Да увеличите `memory_limit` в `php.ini`.

<details>
<summary>Покажи правилния отговор</summary>

**Правилен отговор: В**
Плавното спиране зависи от доставянето на сигнала до PHP процеса, обработката на `SIGTERM` (pcntl) и времето, което Docker дава до `SIGKILL`. Лимитът на паметта няма отношение към това.
</details>