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: кантэйнеры не выправяць небяспечны код.
Чэкліст гатоўнасці да прадакшна
- Шматэтапная выява толькі з прадакшн-залежнасцямі, зафіксаваная базавая выява, карыстач без 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 у кантэйнеры і неабнаўляльнай базавай выявы.
Бясплатны званок для выяўлення
Прынясіце бязладны стэк ці завіслую інтэграцыю - Integrono намячае самы разумны першы крок.
Вы атрымліваеце экспертную ацэнку бюджэтных пастак, залежнасцей і таго, ці варта купляць, склейваць ці будаваць - да пачатку платнай працы. CEO застаецца ў кантуры.