<? phpukraine СПІВБЕСІДИ
Пошук по платформі
DEVOPS · MIDDLE ЧАСТО ПИТАЮТЬ

Як побудувати Docker-образ для PHP-застосунку правильно?

Multi-stage: у білдерах Composer і збірка ассетів, у фінальному образі лише runtime, vendor без dev-залежностей і код; шари впорядковані так, щоб зміна коду не інвалідувала шар із залежностями.

Чому composer install має йти після копіювання лише composer.json і lock?
Навіщо multi-stage build для PHP-проєкту з фронтендом?
Що має бути в продакшн-образі, а чого не має?
Docker шари образу multi-stage

Правильний образ починається з розуміння кешу шарів. 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"]
Що кеш шарів працює зверху вниз: усе після зміненого шару перезбирається, тому залежності ставляться до копіювання коду.
Що multi-stage дає малий фінальний образ без Node, Composer і dev-залежностей, а також єдину точку правди для збірки в CI і локально.
Що продакшн-налаштування PHP задаються в образі: opcache з validate_timestamps=0, preload за потреби, memory_limit, realpath cache, і php.ini-production як база.
Що образ пінується до конкретної версії PHP і базового образу, запускається від non-root, а секрети не потрапляють у шари.
Що для FPM потрібен окремий nginx або єдиний образ на FrankenPHP, і що healthcheck, логи в stdout і graceful shutdown є частиною правильного образу.
Копіювати весь проєкт першим шаром: кожна зміна коду перевстановлює всі залежності.
Тягнути dev-залежності, Node і node_modules у продакшн-образ.
Використовувати тег latest або php:8 без мінорної версії й отримувати несподіване оновлення.
Запускати composer install або npm ci на старті контейнера замість на етапі збірки.
Лишати opcache.validate_timestamps=1 у продакшені й платити stat-викликами на кожен запит.
Запускати процес від root і класти .env з секретами в образ.
ПОРАДА

Додайте, що opcache.validate_timestamps=0 у продакшн-образі дає відчутний приріст, і поясніть, чому це безпечно саме в immutable-образі. Згадайте FrankenPHP як варіант без окремого nginx.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Порядок шарів визначає, що кешується; код змінюється частіше за залежності.