Blog

Infrastruktura 9 min czytania

Docker dla PHP w produkcji: praktyczny przewodnik konfiguracji aplikacji biznesowych

Docker sprawia, że aplikacja PHP działa w ten sam sposób na laptopie, podczas testowania i produkcji, a wydawanie i wycofywanie zmian jest rutynowe, a nie stresujące. Dodaje także ruchome części, a nieostrożna konfiguracja jest gorsza niż zwykły serwer. W tym przewodniku przedstawiono wersję bazową gotową do produkcji z rzeczywistą konfiguracją i momentami, w których nie należy się tym przejmować.

Ilustracja okładkowa: Docker dla PHP w produkcji: praktyczny przewodnik konfiguracji aplikacji biznesowych

W skrócie

  • Docker jest tego wart, jeśli chcesz mieć identyczne środowiska, szybkie wycofywanie zmian i łatwe wdrażanie. W przypadku pojedynczej małej witryny z broszurami na hostingu współdzielonym jest to zwykle niepotrzebne.
  • Zbuduj niezmienny obraz w CI: kompilacja wieloetapowa, tylko zależności produkcyjne, użytkownik inny niż root, włączona OPcache, rejestrowane błędy i nigdy nie wyświetlane.
  • Zwolnij za pomocą tagu obrazu (hash zatwierdzenia Git), sprawdź stan po uruchomieniu i automatycznie przywróć poprzedni tag, jeśli sprawdzenie się nie powiedzie.
  • Kontenery są jednorazowe, dane nie: przechowuj bazę danych i przesyłane dane w woluminach lub usługach zarządzanych i twórz ich kopie zapasowe poza Docker.
  • Najczęstsze błędy produkcyjne: sekrety w obrazie, opublikowane porty bazy danych, nieograniczone logi i limity php-fpm, które nie odpowiadają posiadanej pamięci.

W przypadku wielu zastosowań biznesowych prawdziwym kosztem hostingu nie jest serwer, ale ludzie. Konfiguracja zrozumiała tylko dla jednego programisty, wdrożenie wykonywane ręcznie przez FTP i środowisko testowe, które zachowuje się inaczej niż produkcyjne, są na dłuższą metę kosztowne. Kontenery rozwiązują ten problem, pakując aplikację z jej dokładną wersją PHP i rozszerzeniami. Prowadzimy własne witryny PHP i systemy klienckie na stosie opartym na Dockerze, a ten przewodnik opisuje punkt odniesienia, od którego zaczynamy.

Kiedy Docker jest tego wart, a kiedy nie

  • Warto: kilka usług (sieć, PHP, baza danych, kolejka), więcej niż jeden programista, potrzeba powtarzalnych wydań i szybkich wycofywania zmian lub plan przemieszczania się między dostawcami.
  • Niekonieczne: prosta witryna z treścią, która rzadko zmienia się na hostingu zarządzanym. Dodatkowa złożoność wymaga zapłaty.
  • Nie zastąpi dobrych operacji: nadal potrzebujesz łatania, tworzenia kopii zapasowych i monitorowania. Zobacz nasz przewodnik dotyczący tworzenia kopii zapasowych i monitorowania.

Docelowa architektura

Solidna, prosta konfiguracja aplikacji PHP na jednym serwerze:

  • Odwrotne proxy i TLS na brzegu ( Nginx lub Caddy na hoście lub CDN, taki jak Cloudflare, zobacz nasz przewodnik konfiguracji Cloudflare).
  • Kontener WWW ( Nginx), który obsługuje pliki statyczne i przekazuje dalej żądania PHP.
  • Kontener PHP-FPM z Twoim kodem wbudowany w obraz.
  • Baza danych we własnym kontenerze ze stałym wolumenem lub lepiej zarządzana usługa bazy danych dla krytycznych danych.
  • Woluminy tylko dla danych, które muszą przetrwać: pliki baz danych i przesłane przez użytkowników.

Wieloetapowy plik Dockerfile

Kompilacja wieloetapowa instaluje zależności w jednym etapie i kopiuje tylko wynik do wąskiego obrazu środowiska wykonawczego. Ostateczny obraz nie zawiera Composer, kompilatorów ani narzędzi testowych, co czyni go mniejszym i daje atakującym mniej możliwości użycia:

# ---- 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

Wybierz wersję PHP celowo i sprawdź, czy jest nadal obsługiwana. Nasz przewodnik aktualizacji PHP 8 wyjaśnia, jak to zaplanować. Przypinaj wersje obrazów podstawowych i aktualizuj je zgodnie z harmonogramem, ponieważ obraz, którego nigdy nie odbudujesz, gromadzi znane luki w zabezpieczeniach. Zachowaj plik .dockerignore wykluczający .git, .env, testy i dane lokalne, aby na obrazie nie znalazło się nic wrażliwego.

Produkcja PHP i konfiguracja 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

Ustawienie opcache.validate_timestamps = 0 zapewnia najlepszą wydajność w przypadku niezmiennego obrazu, ale działa tylko wtedy, gdy nigdy nie edytujesz plików w działającym kontenerze: zamiast tego publikujesz nowy obraz. Rozmiar puli na podstawie liczb rzeczywistych: zmierz średnią pamięć jednego pracownika PHP pod obciążeniem, zdecyduj, ile pamięci PHP może używać na serwerze, i podziel. Jeśli pracownik używa 60 MB, a Ty możesz dać PHP 1 GB, wartość pm.max_children wynosi około 16. Ustawienie znacznie wyższej wartości powoduje jedynie zamianę serwera lub procesy zabijania jądra podczas wzrostu ruchu. Zbyt niska i kolejka żądań. Sprawdź dziennik php-fpm pod kątem ostrzeżeń „osiągnięto max_children”.

Plik tworzenia z odpowiednimi ustawieniami domyślnymi

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:

Obraz web to mały obraz Nginx z skopiowaną konfiguracją folderu public/ i witryny, zbudowany w tym samym potoku, co obraz PHP. Kluczowe punkty: powiąż opublikowane porty z 127.0.0.1, nigdy nie publikuj bazy danych, ustaw zasady ponownego uruchamiania, ogranicz pamięć, aby jeden kontener nie mógł wyłączyć hosta, i ogranicz rozmiar dziennika. Pamiętaj, że porty publikowane przez Docker mogą ominąć reguły zapory hosta, jak wyjaśniamy w przewodniku Cloudflare. Nigdy nie montuj gniazda Docker w kontenerze aplikacji, ponieważ daje to kontenerowi kontrolę nad hostem.

Zwolnienia i wycofania

Najlepszą cechą konfiguracji opartej na obrazie jest to, że każde wydanie jest tagiem, a wycofanie polega na „rozpoczęciu poprzedniego tagu”. Oznacz obrazy hashem zatwierdzenia Git, nigdy nie polegaj na latest i spraw, aby skrypt wydania sprawdzał stan:

#!/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

Zmiany w bazie danych wymagają ostrożności: zapisz migracje, aby poprzednia wersja kodu nadal działała z nowym schematem (najpierw dodaj kolumny, usuń je w późniejszej wersji). To właśnie sprawia, że szybkie wycofanie jest bezpieczne. Wykonaj kopię zapasową bazy danych przed wydaniem jakiejkolwiek wersji zmieniającej schemat.

Budowa i wysyłka z CI

Zbuduj obraz raz w CI i wdróż dokładnie ten artefakt. Minimalny przepływ pracy 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 }}"

Uruchom testy i analizę statyczną w tym samym potoku przed zbudowaniem obrazu i użyj klucza wdrażania, który może uruchomić tylko skrypt wydania. Dodaj skanowanie obrazu pod kątem luk w zabezpieczeniach, jeśli wymaga tego Twój profil ryzyka.

Logi, dane i bezpieczeństwo

  • Dzienniki trafiają na stdout i stderr, są obracane przez Docker i, w przypadku ważnych wydarzeń, wysyłane do centralnego miejsca, aby przetrwały w kontenerze.
  • Dane znajdują się poza kontenerem. Wolumin nie jest kopią zapasową. Skorzystaj z metody opisanej w naszym przewodniku po kopiach zapasowych: szyfrowana, przechowywana poza siedzibą firmy i testowana przy przywracaniu.
  • Sekrety nigdy nie są zapisane w obrazach. Przekaż je w czasie wykonywania przez pliki env lub menedżera sekretów i trzymaj je z dala od Git.
  • Uruchom jako użytkownik inny niż root, jeśli to możliwe, użyj systemu plików tylko do odczytu i usuń to, czego nie potrzebujesz.
  • Zachowaj świeżość obrazów. Odbuduj regularnie, aby zastosować poprawki bezpieczeństwa w obrazie podstawowym i PHP.
  • Zastosuj kontrole na poziomie aplikacji z naszej listy kontrolnej bezpieczeństwa PHP, ponieważ kontenery nie naprawiają niezabezpieczonego kodu.

Lista kontrolna gotowości produkcyjnej

  1. Obraz wieloetapowy z zależnościami produkcyjnymi, przypięty obraz podstawowy, użytkownik inny niż root.
  2. OPcache włączone, zarejestrowane błędy nie są wyświetlane, rozmiar php-fpm na podstawie zmierzonej pamięci.
  3. Utwórz plik z zasadami ponownego uruchamiania, kontrolą stanu, limitami pamięci, rotacją dzienników i brakiem publicznego portu bazy danych.
  4. Zwalnia poprzez tag commit-hash z automatyczną kontrolą stanu i wycofywaniem zmian.
  5. Kopie zapasowe i testowane przywracanie bazy danych i przesyłanych plików.
  6. Monitorowanie i alerty dotyczące kondycji, dysku, błędów i certyfikatów.

Jak możemy pomóc

Konteneryzujemy aplikacje PHP, konfigurujemy potoki CI i wydania oraz łączymy je z płatnościami, CRM i przesyłaniem wiadomości. Zobacz naszą usługę integracji lub zarezerwuj rozmowę, a my sprawdzimy Twój obecny proces hostingu i wydania.

Najczęściej zadawane pytania

Czy Docker jest dobry do uruchamiania PHP w środowisku produkcyjnym?

Tak, jeśli jest dobrze używany. Zapewnia identyczne środowiska, powtarzalne wydania i szybkie wycofywanie. Wymaga odpowiedniej konfiguracji: niezmiennych obrazów, kontroli stanu, limitów zasobów, rotacji logów, zarządzania sekretami i tworzenia kopii zapasowych danych poza kontenerem.

Czy baza danych powinna działać w środowisku Docker w środowisku produkcyjnym?

Może być stosowany w małych i średnich systemach, jeśli wykorzystuje stały wolumin, nigdy nie jest ujawniany publicznie i ma przetestowane kopie zapasowe. W przypadku danych krytycznych wiele zespołów preferuje usługę zarządzanej bazy danych, która obsługuje przełączanie awaryjne, aktualizacje i kopie zapasowe za Ciebie.

Jak wycofać wersję Docker ?

Oznacz każdy obraz hashem zatwierdzenia Git i zachowaj poprzedni tag. Aby wycofać, uruchom kontenery z poprzednim tagiem. Zapisz migracje baz danych, aby starszy kod nadal działał z nowszym schematem.

Ilu pracowników php-fpm powinienem skonfigurować?

Zmierz średnią pamięć jednego procesu roboczego pod obciążeniem, zdecyduj, ile pamięci może wykorzystać PHP, i podziel. Ustawienie zbyt dużej wartości powoduje zamianę, natomiast zbyt niska powoduje kolejkowanie żądań. Obejrzyj dziennik php-fpm pod kątem ostrzeżeń max_children.

Jaki jest największy błąd bezpieczeństwa Docker w aplikacjach PHP ?

Umieszczanie sekretów w obrazie lub Git i publikowanie portów baz danych w Internecie. Unikaj także uruchamiania jako root, montowania gniazda Docker w kontenerach i nigdy nie aktualizowania obrazu podstawowego.

Bezpłatna rozmowa odkrywcza

Bałagan w stacku albo zawieszona integracja - Integrono zaplanuje najmądrzejszy pierwszy ruch.

Dostajesz senior-ocenę pułapek budżetowych, zależności i tego, czy kupować, sklejać czy budować - zanim zacznie się płatna praca. CEO zostaje w pętli.