Multi-stage: у білдерах Composer і збірка ассетів, у фінальному образі лише runtime, vendor без dev-залежностей і код; шари впорядковані так, щоб зміна коду не інвалідувала шар із залежностями.
Правильний образ починається з розуміння кешу шарів. Docker перевикористовує шар, поки його вхідні дані не змінились, і перезбирає все після першого зміненого. Код змінюється щодня, composer.lock рідко, тому спершу копіюють лише composer.json і composer.lock, ставлять залежності, і тільки потім копіюють решту. Так зміна одного файлу не перевстановлює vendor.
Multi-stage розділяє збірку й рантайм. Composer живе у своєму стейджі, Node зі збіркою Vite у своєму, а фінальний образ на php:8.4-fpm-alpine отримує лише результат через COPY --from: vendor без dev-залежностей із оптимізованим автолоадером і зібраний public/build. Фінальний образ менший у рази, не містить інструментів збірки й має меншу поверхню атаки.
Продакшн-налаштування PHP входять в образ: php.ini-production як база, opcache з validate_timestamps=0, бо в immutable-образі файли не змінюються, а деплой це заміна контейнера. Версія базового образу пінується до мінорної, процес запускається від www-data, секрети приходять через змінні середовища, а не через .env у шарах. Healthcheck, логи в stdout і graceful shutdown FPM завершують картину. Для нового проєкту альтернативою FPM з nginx є FrankenPHP: один бінарник із вбудованим сервером і worker mode.
# syntax=docker/dockerfile:1
# 1. Залежності PHP: шар кешується, доки не зміниться composer.lock
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative
# 2. Фронтенд: Node живе лише тут
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources ./resources
COPY vite.config.js ./
RUN npm run build
# 3. Рантайм: пінована версія, без Composer, Node і dev-залежностей
FROM php:8.4-fpm-alpine AS runtime
RUN docker-php-ext-install pdo_pgsql opcache \
&& mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/opcache.ini $PHP_INI_DIR/conf.d/ # validate_timestamps=0, jit, memory
WORKDIR /var/www
COPY --chown=www-data:www-data --from=vendor /app ./
COPY --chown=www-data:www-data --from=assets /app/public/build ./public/build
USER www-data
HEALTHCHECK CMD php-fpm-healthcheck || exit 1
CMD ["php-fpm"]
Додайте, що opcache.validate_timestamps=0 у продакшн-образі дає відчутний приріст, і поясніть, чому це безпечно саме в immutable-образі. Згадайте FrankenPHP як варіант без окремого nginx.