Блог

Інфраструктура 8 хв читання

Docker для PHP у продакшні: практичний посібник для бізнес-додатків

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, компіляторів та тестових інструментів, тому він менший, а у атакуючого менше можливостей:

# ---- stage 1: PHP dependencies ----
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

# ---- stage 2: runtime ----
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   ; the code in an image never changes while it runs

# 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" }     # logs cannot fill the disk

services:
  web:
    image: ghcr.io/your-org/app-web:${TAG}
    ports: ["127.0.0.1:8080:80"]                  # only the local reverse proxy can reach it
    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                                 # secrets stay on the server, not in the image
    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]              # no "ports:" - the database is not public
    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: контейнери не виправлять небезпечний код.

Чекліст готовності до продакшну

  1. Багатоетапний образ лише з продакшн-залежностями, зафіксований базовий образ, користувач без root.
  2. OPcache включений, помилки пишуться в балку, а не показуються, php-fpm розрахований по виміряній пам'яті.
  3. Compose-файл з політиками перезапуску, healthcheck, лімітами пам'яті, ротацією ліг і без публічного порту бази.
  4. Релізи за тегом з хешем комміту, автоматичною перевіркою здоров'я та відкатом.
  5. Бекапи та перевірене відновлення бази та завантажень.
  6. Моніторинг та алерти щодо здоров'я, диску, помилок та сертифікатів.

Чим ми можемо допомогти

Ми контейнеризуємо 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 лишається в контурі.