Docker для PHP в продакшне: практическое руководство для бизнес-приложений
Docker позволяет PHP-приложению работать одинаково на ноутбуке, в тестировании и в продакшне и превращает релизы и откаты из стресса в рутину. Но он добавляет подвижных частей, а небрежная настройка хуже обычного сервера. Здесь — готовая к продакшну базовая конфигурация с реальными настройками и случаи, когда он не нужен.
Коротко
- Docker стоит использовать, когда нужны одинаковые окружения, быстрые откаты и простой вход в проект. Для одного небольшого сайта-визитки на виртуальном хостинге он обычно лишний.
- Собирайте неизменяемый образ в CI: многоэтапная сборка, только продакшн-зависимости, пользователь без root, включённый OPcache, ошибки пишутся в лог и никогда не показываются.
- Выпускайте релизы по тегу образа (хеш коммита Git), проверяйте здоровье после старта и автоматически откатывайтесь на предыдущий тег, если проверка не прошла.
- Контейнеры одноразовые, данные — нет: держите базу и загрузки в томах или управляемых сервисах и делайте бэкапы вне Docker.
- Самые частые ошибки: секреты в образе, опубликованные порты базы, неограниченные логи и лимиты php-fpm, не соответствующие имеющейся памяти.
Для многих бизнес-приложений главная стоимость хостинга — не сервер, а люди. Настройка, которую понимает только один разработчик, деплой руками через FTP и тестовое окружение, отличающееся от продакшна, в долгосрочной перспективе обходятся дорого. Контейнеры решают это, упаковывая приложение вместе с точной версией PHP и расширениями. Мы сами запускаем свои PHP-сайты и клиентские системы на стеке с Docker, и это руководство описывает базу, с которой мы начинаем.
Когда Docker нужен, а когда нет
- Нужен: несколько сервисов (веб, PHP, база, очередь), больше одного разработчика, нужны повторяемые релизы и быстрые откаты или планируется переезд между провайдерами.
- Не обязателен: простой контентный сайт, который редко меняется, на управляемом хостинге. Дополнительная сложность должна окупаться.
- Не заменяет нормальную эксплуатацию: по-прежнему нужны обновления, бэкапы и мониторинг. См. наше руководство по бэкапам и мониторингу.
Целевая архитектура
Надёжная и простая схема для PHP-приложения на одном сервере:
- Обратный прокси и TLS на границе (Nginx или Caddy на хосте либо CDN вроде Cloudflare, см. наше руководство по Cloudflare).
- Веб-контейнер (Nginx), который отдаёт статику и передаёт PHP-запросы дальше.
- Контейнер PHP-FPM с вашим кодом, встроенным в образ.
- База данных в отдельном контейнере с постоянным томом или, лучше, управляемый сервис базы для критичных данных.
- Тома только для данных, которые должны пережить пересоздание: файлы базы и загрузки пользователей.
Многоэтапный Dockerfile
Многоэтапная сборка устанавливает зависимости на одном этапе и копирует только результат в компактный рабочий образ. В итоговом образе нет Composer, компиляторов и тестовых инструментов, поэтому он меньше, а у атакующего меньше возможностей:
# ---- этап 1: зависимости PHP ----
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --optimize-autoloader
# ---- этап 2: рабочий образ ----
FROM php:8.3-fpm-alpine
RUN apk add --no-cache icu-libs libzip \
&& apk add --no-cache --virtual .build icu-dev libzip-dev $PHPIZE_DEPS \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql intl zip opcache \
&& apk del .build
COPY docker/php.ini /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/www.conf /usr/local/etc/php-fpm.d/zz-www.conf
WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .
USER www-data
EXPOSE 9000
Выбирайте версию PHP осознанно и проверяйте, что она ещё поддерживается. Как это спланировать, объясняет наше руководство по обновлению до PHP 8. Фиксируйте версии базовых образов и обновляйте их по расписанию: образ, который никогда не пересобирают, накапливает известные уязвимости. Держите .dockerignore, исключающий .git, .env, тесты и локальные данные, чтобы ничего чувствительного не попало в образ.
Продакшн-конфигурация PHP и php-fpm
# docker/php.ini
expose_php = 0
display_errors = 0
log_errors = 1
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0 ; код в образе не меняется, пока образ работает
# docker/www.conf
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no
Параметр opcache.validate_timestamps = 0 даёт лучшую производительность для неизменяемого образа, но работает, только если вы никогда не правите файлы в работающем контейнере: вместо этого выпускается новый образ. Рассчитывайте пул по реальным цифрам: измерьте среднюю память одного процесса PHP под нагрузкой, решите, сколько памяти сервера может занимать PHP, и разделите. Если процесс занимает 60 МБ, а PHP можно отдать 1 ГБ, pm.max_children — около 16. Намного большее значение приводит к свопу или к тому, что ядро убивает процессы при всплеске трафика. Слишком малое — запросы встают в очередь. Следите в логе php-fpm за предупреждениями «max_children reached».
Compose-файл с правильными значениями по умолчанию
x-logging: &default-logging
driver: json-file
options: { max-size: "10m", max-file: "5" } # логи не заполнят диск
services:
web:
image: ghcr.io/your-org/app-web:${TAG}
ports: ["127.0.0.1:8080:80"] # достучаться может только локальный обратный прокси
depends_on: [php]
restart: unless-stopped
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health.php"]
interval: 15s
timeout: 3s
retries: 3
php:
image: ghcr.io/your-org/app:${TAG}
env_file: .env # секреты остаются на сервере, не в образе
restart: unless-stopped
read_only: true
tmpfs: [/tmp]
mem_limit: 512m
volumes: [uploads:/var/www/app/storage/uploads]
logging: *default-logging
db:
image: mysql:8.4
env_file: .env.db
restart: unless-stopped
volumes: [db_data:/var/lib/mysql] # нет "ports:" - база не публична
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 15s
retries: 5
logging: *default-logging
volumes:
uploads:
db_data:
Образ web — небольшой образ Nginx, в который скопированы папка public/ и конфигурация сайта; он собирается в том же конвейере, что и образ PHP. Главное: привязывайте публикуемые порты к 127.0.0.1, никогда не публикуйте базу, задавайте политики перезапуска, ограничивайте память, чтобы один контейнер не положил хост, и ограничивайте размер логов. Помните, что порты, опубликованные Docker, могут обходить правила файервола хоста, как мы объясняем в руководстве по Cloudflare. Никогда не монтируйте сокет Docker в контейнер приложения: это даёт контейнеру контроль над хостом.
Релизы и откаты
Лучшая особенность схемы на образах в том, что каждый релиз — это тег, а откат — «запусти предыдущий тег». Помечайте образы хешем коммита Git, не полагайтесь на latest и добавьте в скрипт релиза проверку здоровья:
#!/usr/bin/env bash
# /srv/app/release.sh <git-sha>
set -euo pipefail
TAG="${1:?usage: release.sh <git-sha>}"
cd /srv/app
PREV="$(cat .current_tag 2>/dev/null || true)"
export TAG
docker compose pull php web
docker compose up -d --no-deps php web
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8080/health.php > /dev/null; then
echo "$TAG" > .current_tag
echo "healthy: $TAG"; exit 0
fi
sleep 2
done
echo "unhealthy after release, rolling back to ${PREV:-none}" >&2
if [ -n "$PREV" ]; then TAG="$PREV" docker compose up -d --no-deps php web; fi
exit 1
Изменения базы требуют аккуратности: пишите миграции так, чтобы предыдущая версия кода работала с новой схемой (сначала добавляйте колонки, удаляйте их в следующем релизе). Именно это делает быстрый откат безопасным. Перед любым релизом, меняющим схему, делайте бэкап базы.
Сборка и доставка из CI
Собирайте образ один раз в CI и разворачивайте именно этот артефакт. Минимальный workflow GitHub Actions:
name: build-and-release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/your-org/app:${{ github.sha }}
- name: Release over SSH
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
install -m 600 /dev/null key && echo "$DEPLOY_KEY" > key
ssh -i key -o StrictHostKeyChecking=accept-new deploy@your-server "/srv/app/release.sh ${{ github.sha }}"
Запускайте тесты и статический анализ в том же конвейере до сборки образа и используйте ключ деплоя, который может запускать только скрипт релиза. Добавьте сканирование образа на уязвимости, если этого требует ваш профиль рисков.
Логи, данные и безопасность
- Логи идут в stdout и stderr, ротируются Docker, а важное отправляется в центральное место, чтобы пережить контейнер.
- Данные живут вне контейнера. Том — это не бэкап. Используйте подход из нашего руководства по бэкапам: шифрование, копия вне площадки, проверка восстановления.
- Секреты никогда не вшиваются в образы. Передавайте их при запуске через env-файлы или менеджер секретов и не храните в Git.
- Запускайте от пользователя без root, по возможности используйте файловую систему только для чтения и убирайте лишнее.
- Обновляйте образы. Пересобирайте регулярно, чтобы получать исправления безопасности базового образа и PHP.
- Применяйте меры уровня приложения из нашего чеклиста безопасности PHP: контейнеры не исправят небезопасный код.
Чеклист готовности к продакшну
- Многоэтапный образ только с продакшн-зависимостями, зафиксированный базовый образ, пользователь без root.
- OPcache включён, ошибки пишутся в лог, а не показываются, php-fpm рассчитан по измеренной памяти.
- Compose-файл с политиками перезапуска, healthcheck, лимитами памяти, ротацией логов и без публичного порта базы.
- Релизы по тегу с хешем коммита, автоматической проверкой здоровья и откатом.
- Бэкапы и проверенное восстановление базы и загрузок.
- Мониторинг и алерты по здоровью, диску, ошибкам и сертификатам.
Чем мы можем помочь
Мы контейнеризуем PHP-приложения, настраиваем CI-конвейеры и релизы и подключаем их к платежам, CRM и мессенджерам. Посмотрите нашу услугу интеграций или запишитесь на звонок, и мы изучим ваш текущий хостинг и процесс релизов.
Частые вопросы
Подходит ли Docker для запуска PHP в продакшне?
Да, если использовать его правильно. Он даёт одинаковые окружения, повторяемые релизы и быстрые откаты. Нужна корректная настройка: неизменяемые образы, healthcheck, лимиты ресурсов, ротация логов, управление секретами и бэкапы данных вне контейнера.
Нужно ли запускать базу данных в Docker в продакшне?
Для небольших и средних систем можно, если используется постоянный том, база не открыта публично и бэкапы проверены. Для критичных данных многие команды предпочитают управляемый сервис базы, который берёт на себя отказоустойчивость, обновления и бэкапы.
Как откатить релиз в Docker?
Помечайте каждый образ хешем коммита Git и храните предыдущий тег. Для отката запустите контейнеры с предыдущим тегом. Пишите миграции базы так, чтобы старый код работал с новой схемой.
Сколько воркеров php-fpm настраивать?
Измерьте среднюю память одного воркера под нагрузкой, решите, сколько памяти может занимать PHP, и разделите. Слишком большое значение вызывает своп, слишком малое — очередь запросов. Следите в логе php-fpm за предупреждениями max_children.
Какая самая большая ошибка безопасности Docker в PHP-приложениях?
Секреты в образе или Git и порты базы, опубликованные в интернет. Также избегайте запуска от root, монтирования сокета Docker в контейнеры и необновляемого базового образа.
Бесплатный discovery-звонок
Хаотичный стек или зависшая интеграция - Integrono наметит самый умный первый шаг.
Вы получаете senior-взгляд на бюджетные ловушки, зависимости и когда покупать, склеивать или писать - до старта оплаты. CEO остаётся в контуре.