Przewodnik po Dockerze

OxPHP działa jako kontener. Ten przewodnik obejmuje budowanie, konfigurowanie i uruchamianie go w Dockerze — od minimalnego, jednoetapowego obrazu po konfigurację wieloetapową z osobnymi celami dla środowiska deweloperskiego i produkcyjnego.

Minimalny Dockerfile

Najprostszy sposób na skonteneryzowanie aplikacji:

Dockerfile
FROM ghcr.io/oxphp/oxphp:0.10.0 COPY --chown=www-data:www-data . /var/www/html/public

To kopiuje aplikację do kontenera. Domyślnym DOCUMENT_ROOT jest /var/www/html/public. Dla Laravela, Symfony lub dowolnego projektu, który już zawiera podkatalog public/, użyj zamiast tego COPY --chown=www-data:www-data . /var/www/html, aby własny katalog public/ frameworka pokrywał się z domyślnym. Serwer domyślnie nasłuchuje na porcie 80.

Wieloetapowy Dockerfile

W rzeczywistych aplikacjach użyj wieloetapowego pliku Dockerfile z osobnymi celami dev i prod. Cel dev zawiera PHP CLI, Composera i Xdebug. Cel prod bazuje na minimalnym obrazie OxPHP i zawiera tylko to, co niezbędne na produkcji.

Tip

Gotowa do użycia wersja tego pliku Dockerfile znajduje się w repozytorium pod adresem examples/dockerfile/Dockerfile. Skopiuj ją do swojego projektu i dostosuj rozszerzenia do swoich potrzeb.

Dockerfile
# ── Stage: php-base — shared PHP extensions ────────────────── FROM php:8.4-zts-alpine3.23 AS php-base RUN apk add --no-cache \ icu-dev \ icu-libs \ postgresql-dev \ libpq \ && docker-php-ext-install \ pdo \ pdo_mysql \ pdo_pgsql \ intl \ && apk del icu-dev postgresql-dev # ── Stage: php-dev — add Xdebug on top of base ─────────────── FROM php-base AS php-dev RUN apk add --no-cache $PHPIZE_DEPS linux-headers \ && pecl install xdebug \ && docker-php-ext-enable xdebug \ && apk del $PHPIZE_DEPS linux-headers # ── Stage: composer ─────────────────────────────────────────── FROM composer:2 AS composer # ── Stage: oxphp — pull OxPHP artifacts ────────────────────── FROM ghcr.io/oxphp/oxphp:0.10.0 AS oxphp # ── Target: dev ────────────────────────────────────────────── # Includes: PHP CLI, Composer, Xdebug, OxPHP binary + extension FROM php-dev AS dev RUN apk add --no-cache libgcc # Composer COPY --from=composer /usr/bin/composer /usr/local/bin/composer # OxPHP binary COPY --from=oxphp /usr/local/bin/oxphp /usr/local/bin/oxphp # Bridge library COPY --from=oxphp /usr/local/lib/liboxphp_bridge.so /usr/local/lib/ # OxPHP PHP extension RUN EXT_DIR=$(php -r 'echo ini_get("extension_dir");') && \ echo "$EXT_DIR" > /tmp/ext_dir COPY --from=oxphp /usr/local/lib/php/extensions/ /tmp/oxphp-ext/ RUN cp /tmp/oxphp-ext/*/oxphp_sapi.so "$(cat /tmp/ext_dir)/" && \ rm -rf /tmp/oxphp-ext /tmp/ext_dir # PHP config RUN echo "extension=oxphp_sapi.so" > /usr/local/etc/php/conf.d/oxphp-ext.ini # Dev-friendly OPcache (validates timestamps) RUN { \ echo "[opcache]"; \ echo "opcache.enable=1"; \ echo "opcache.enable_cli=1"; \ echo "opcache.validate_timestamps=1"; \ echo "opcache.revalidate_freq=0"; \ } > /usr/local/etc/php/conf.d/opcache-dev.ini # Xdebug — connect back to host RUN { \ echo "[xdebug]"; \ echo "xdebug.mode=debug"; \ echo "xdebug.start_with_request=trigger"; \ echo "xdebug.client_host=host.docker.internal"; \ echo "xdebug.client_port=9003"; \ } > /usr/local/etc/php/conf.d/xdebug-config.ini RUN adduser -D -H -u 82 -G www-data -s /sbin/nologin www-data 2>/dev/null || true RUN mkdir -p /var/www/html/public && chown -R www-data:www-data /var/www/html ENV LD_LIBRARY_PATH=/usr/local/lib # Framework layout: project ships a public/ subdir (Laravel/Symfony/Slim). # For a bare index.php at the project root, copy into /var/www/html/public instead. COPY --chown=www-data:www-data . /var/www/html EXPOSE 80 443 CMD ["oxphp"] # ── Stage: prod-extensions — compile extensions for prod ───── FROM php-base AS prod-extensions RUN EXT_DIR=$(php -r 'echo ini_get("extension_dir");') && \ mkdir -p /ext-out && \ cp "$EXT_DIR"/pdo.so \ "$EXT_DIR"/pdo_mysql.so \ "$EXT_DIR"/pdo_pgsql.so \ "$EXT_DIR"/intl.so \ /ext-out/ # ── Target: prod — minimal, based on OxPHP image ───────────── FROM oxphp AS prod USER root RUN apk add --no-cache icu-libs libpq COPY --from=prod-extensions /ext-out/*.so /usr/local/lib/php/extensions/no-debug-zts-20240924/ RUN { \ echo "extension=pdo_mysql.so"; \ echo "extension=pdo_pgsql.so"; \ echo "extension=intl.so"; \ } > /usr/local/etc/php/conf.d/app-extensions.ini # Framework layout: project ships a public/ subdir (Laravel/Symfony/Slim). # For a bare index.php at the project root, copy into /var/www/html/public instead. COPY --chown=www-data:www-data . /var/www/html USER www-data EXPOSE 80 443 CMD ["oxphp"]

Zbuduj każdy cel:

bash
# Development image (includes PHP CLI, Composer, Xdebug) docker build --target dev -t myapp:dev . # Production image (minimal) docker build --target prod -t myapp:prod .
Note

Cel dev bazuje na php:8.4-zts-alpine (zamień 8.4 na 8.5, aby dopasować się do tagu OxPHP :*-php8.5*) z dokopiowanym OxPHP, dzięki czemu masz pełny dostęp do PHP CLI i Composera. Cel prod bazuje bezpośrednio na obrazie OxPHP, co utrzymuje obraz produkcyjny w niewielkim rozmiarze.

Instalowanie rozszerzeń PHP na produkcji

Począwszy od OxPHP 0.3.0 obraz produkcyjny zawiera pełny zestaw narzędzi PHP (php, docker-php-ext-install, phpize) odziedziczony z php:8.4-zts-alpine (lub php:8.5-zts-alpine dla wariantów :*-php8.5*) i nie ustawia dyrektywy USER. Pochodne pliki Dockerfile mogą instalować rozszerzenia PHP bezpośrednio. Nie jest potrzebne żadne przełączanie USER.

Wzorzec na szybki start (jeden etap)

Najkrótszy użyteczny przykład:

Dockerfile
FROM ghcr.io/oxphp/oxphp:0.10.0 RUN docker-php-ext-install mysqli pdo_mysql COPY --chown=www-data:www-data . /var/www/html/public CMD ["oxphp"]

--chown=www-data:www-data przy instrukcji COPY jest ważne: wewnątrz obrazu pliki należą do www-data (uid 82), więc zrzucenie uprawnień na poziomie orkiestratora poprzez --user www-data trafia na webroot, który proces bez uprawnień może odczytać, a tam gdzie to konieczne — także zapisać.

Kontener startuje jako root, a oxphp serve domyślnie zrzuca uprawnienia do www-data, zanim zacznie obsługiwać jakikolwiek ruch (zobacz uwagę dotyczącą bezpieczeństwa poniżej). Ustaw tożsamość jawnie na poziomie orkiestratora, jeśli chcesz konkretny uid lub użytkownika innego niż www-data.

Wzorzec zgodny z dobrymi praktykami (dwa etapy, mniejszy obraz)

Aby uzyskać możliwie najmniejszy obraz końcowy, skompiluj rozszerzenia w dedykowanym etapie budowania i skopiuj do etapu uruchomieniowego tylko skompilowane pliki .so. Opisany powyżej przewodnik Wieloetapowy Dockerfile używa FROM ghcr.io/oxphp/oxphp:0.10.0 AS prod — proste, przenośne i zalecane jako punkt wyjścia.

Plik examples/dockerfile/Dockerfile w repozytorium idzie dalej: jego cel prod bazuje na gołym obrazie alpine z jawną listą zależności apk, kopiując jedynie binarkę oxphp, libphp.so, skompilowane rozszerzenia PHP oraz wymagane biblioteki współdzielone. Zmniejsza to obraz bazowy z ~188 MB do ~76 MB (redukcja o ~60%, nie licząc kodu Twojej aplikacji) kosztem konieczności śledzenia zmian wersji PHP/Alpine na liście apk. Ten sam plik dostarcza również cel prod-cli — krótko żyjący obraz do php artisan migrate, Composera i innych poleceń konserwacyjnych, które powinny pozostać poza ścieżką obsługi żądań.

Note

Powyższy przewodnik nadal pokazuje jawne przełączanie USER root / USER www-data w ramach ochrony warstwowej (defense-in-depth). Od wersji v0.3.0 jest ono opcjonalne, ponieważ obraz bazowy nie ustawia już USER.

Uruchamianie narzędzi CLI i migracji

Ten sam obraz prod może uruchamiać polecenia php CLI na potrzeby migracji, Composera lub doraźnej inspekcji. docker run zastępuje domyślny CMD poleceniem, które przekażesz: kontener wykonuje polecenie i kończy pracę, nie uruchamia przy tym również serwera OxPHP.

bash
# Run Laravel migrations against the prod image. # Container runs as root by default — the CLI has write access to # root-owned mounted volumes. docker run --rm \ -v "$(pwd):/var/www/html" \ ghcr.io/oxphp/oxphp:0.10.0 \ php artisan migrate # If the mounted volume is owned by www-data, pass Docker's --user: docker run --rm --user www-data \ -v "$(pwd):/var/www/html" \ ghcr.io/oxphp/oxphp:0.10.0 \ php artisan migrate

docker exec <container> docker-php-ext-install <ext> działa również na uruchomionym kontenerze bez żadnych dodatkowych flag — przydatne przy debugowaniu działającego kontenera. Na produkcji utrwal rozszerzenie w swoim pliku Dockerfile, aby przetrwało restarty.

Uwaga dotycząca bezpieczeństwa

Obraz prod nie ma dyrektywy USER, więc kontener startuje jako root (zgodnie z konwencjami nginx:alpine / php:*-fpm-alpine / frankenphp:alpine). Start jako root pozwala OxPHP powiązać się z portami uprzywilejowanymi, ale nie oznacza już, że ruch jest obsługiwany jako root: oxphp serve i oxphp run domyślnie zrzucają uprawnienia do www-data, wiążąc się jako root, a następnie trwale zrzucając uprawnienia, zanim zostanie obsłużone jakiekolwiek żądanie czy uruchomiony jakikolwiek worker PHP. W oficjalnym obrazie (który zawiera konto www-data) dzieje się to od razu, bez żadnej konfiguracji orkiestratora.

Nadal możesz jawnie ustawić tożsamość uruchomieniową na poziomie orkiestratora — zalecane, gdy potrzebujesz konkretnego uid, dodatkowej ochrony warstwowej lub użytkownika innego niż www-data:

bash
docker run --user www-data ghcr.io/oxphp/oxphp:0.10.0

Gdy uruchomisz kontener w ten sposób jako użytkownik inny niż root, OxPHP jest już nieuprzywilejowany, a domyślne samodzielne zrzucenie uprawnień jest operacją bez efektu, ale proces nie może wtedy powiązać się z portami poniżej 1024 (zobacz Uruchamianie jako użytkownik inny niż root na porcie 80, aby zachować powiązanie z portem uprzywilejowanym oraz obsługę bez uprawnień roota). Aby celowo utrzymać obsługę jako root, przekaż oxphp serve --user=root.

Użytkownik www-data (uid 82, gid 82) jest wstępnie utworzony przez obraz bazowy, a /var/www/html jest do niego chownowany w czasie budowania, więc każda z tych ścieżek zrzucenia uprawnień — łącznie z domyślnym samodzielnym zrzuceniem — trafia na czytelny webroot.

Note

Wywołania CLI, takie jak docker run … php artisan migrate, uruchamiają binarkę php bezpośrednio, a nie oxphp serve/run, więc nie dokonują samodzielnego zrzucenia uprawnień; działają jako użytkownik startowy kontenera (domyślnie root). Użyj dla nich Dockerowego --user, jak pokazano powyżej.

Uruchamianie jako użytkownik inny niż root na porcie 80 (serve --user)

Zrzucanie uprawnień na poziomie orkiestratora (powyżej) ma jedno ograniczenie: proces, który startuje jako www-data, nie może powiązać się z portem uprzywilejowanym (poniżej 1024). Aby obsługiwać ruch na :80/:443, w przeciwnym razie potrzebowałbyś CAP_NET_BIND_SERVICE, entrypointu w stylu su-exec albo wysokiego portu (np. :8080) za mapowaniem portów.

OxPHP sprowadza to do jednego procesu: wiąże listenery jako root, a następnie trwale zrzuca uprawnienia zanim zostanie zaakceptowane jakiekolwiek połączenie czy uruchomiony jakikolwiek worker PHP. Otrzymujesz port uprzywilejowany oraz obsługę żądań bez uprawnień roota, bez dodatkowych uprawnień systemowych. Domyślnie użytkownikiem, do którego zrzucane są uprawnienia, jest www-data, więc w oficjalnym obrazie samo uruchomienie kontenera jako root już daje Ci powiązanie z portem uprzywilejowanym obsługiwane przez www-data — bez żadnej flagi. Użyj --user=<spec> tylko po to, aby zrzucić uprawnienia do innego użytkownika; użyj --user=root, aby zachować root.

Uruchom kontener jako root — nie ustawiaj przy tym user:, ponieważ powiązanie wymaga roota. Poniższy przykład jawnie przekazuje --user=www-data, aby był samoopisujący, ale pokrywa się z wartością domyślną:

compose.yaml
services: oxphp: image: ghcr.io/oxphp/oxphp:0.10.0 command: ["oxphp", "serve", "--user=www-data"] ports: - "80:80" - "443:443" environment: - LISTEN_ADDR=0.0.0.0:80

<spec> przyjmuje nazwę użytkownika, name:group, numeryczny uid lub uid:gid. Zrzucenie uprawnień wykonuje initgroups → setgid → setuid, sprawdza, że nie da się odzyskać roota, i jest nieodwracalne; na Linuksie ustawia dodatkowo no_new_privs. Jawne --user działa według zasady fail-fast: jeśli proces nie został uruchomiony jako root, serve --user kończy pracę z błędem, zamiast po cichu kontynuować jako root. (Domyślne zrzucenie działa natomiast według zasady best-effort — uruchomione bez uprawnień roota po prostu je pomija, bo nie ma czego zrzucać.)

Warning

Wybierz jeden model, nie oba. Użyj zrzucenia uprawnień na poziomie orkiestratora (user: / runAsUser), gdy :80 obsługuje wysoki port lub zewnętrzny load balancer. Użyj serve --user, gdy chcesz, aby OxPHP sam był właścicielem powiązania z portem uprzywilejowanym. Ustawienie user: oraz serve --user powoduje niepowodzenie powiązania: kontener nie jest już rootem.

Lista kontrolna uprawnień do plików dla użytkownika po zrzuceniu uprawnień. Po zrzuceniu wszystko, czego OxPHP dotyka w czasie działania, musi być dostępne dla <spec>:

Zasób Wymaganie
DOCUMENT_ROOT Do odczytu. /var/www/html jest chownowany do www-data w czasie budowania obrazu, więc jest to spełnione domyślnie.
Ścieżka zapisu sesji (session.save_path, domyślnie /tmp) Do zapisu.
Katalog tymczasowy uploadów (upload_tmp_dir) Do zapisu, gdy używane są przesyłane pliki.
Plikowa pamięć podręczna OPcache (opcache.file_cache) Do zapisu, gdy włączona jest dodatkowa plikowa pamięć podręczna.
Plikowy log dostępu Do zapisu, gdy logowanie odbywa się do pliku, a nie na stdout.
Klucz prywatny TLS (TLS_KEY) Do odczytu przez użytkownika po zrzuceniu uprawnień — czytelny dla grupy lub wszystkich, a nie tylko dla roota z uprawnieniami 0600. Klucz jest odczytywany po zrzuceniu uprawnień, więc klucz dostępny tylko dla roota spowoduje niepowodzenie startu TLS.

Docker Compose

compose.yaml
services: oxphp: build: context: . target: prod ports: - "80:80" - "443:443" - "9090:9090" environment: - LISTEN_ADDR=0.0.0.0:80 - DOCUMENT_ROOT=/var/www/html/public - ENTRY_FILE=index.php - INTERNAL_ADDR=0.0.0.0:9090 - LOG_LEVEL=info - ACCESS_LOG=error - PHP_WORKERS=4 - DRAIN_TIMEOUT_SECONDS=25 - COMPRESSION_LEVEL=4 restart: unless-stopped

Montowanie wolumenów

Ścieżka hosta Ścieżka w kontenerze Cel
./src /var/www/html Pliki aplikacji (skrypty PHP, zasoby statyczne). Użyj :ro na produkcji
./custom.ini /usr/local/etc/php/conf.d/custom.ini Konfiguracja uruchomieniowa PHP (OPcache, sesje, JIT). Użyj :ro
./certs /etc/ssl/oxphp Certyfikat TLS i klucz prywatny. Użyj :ro

Zestawienie portów

Port Zmienna środowiskowa Cel
80 LISTEN_ADDR Główny serwer HTTP
443 LISTEN_ADDR Główny serwer HTTPS (gdy skonfigurowany jest TLS)
9090 INTERNAL_ADDR Serwer wewnętrzny: /health, /metrics, /config
Note

Serwer wewnętrzny jest domyślnie wyłączony. Ustaw INTERNAL_ADDR, aby go włączyć. Na produkcji utrzymuj port wewnętrzny osiągalny wyłącznie dla Twojego orkiestratora lub systemu monitorującego; nie wystawiaj go publicznie.

Konfiguracja PHP

Dostosuj ustawienia PHP, tworząc plik custom.ini i montując go do kontenera. To zalecany sposób konfigurowania OPcache, JIT, sesji i innych uruchomieniowych ustawień PHP.

custom.ini
; Do NOT add zend_extension=opcache — OPcache is already compiled into the ; PHP ZTS base image. The [opcache] section below configures it directly. [opcache] opcache.enable = 1 opcache.enable_cli = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.validate_timestamps = 0 opcache.jit_buffer_size = 64M opcache.jit = tracing [Session] session.save_path = /tmp session.use_cookies = 1 session.use_only_cookies = 1
Note

Nie dodawaj zend_extension=opcache do tego pliku. OPcache jest już wbudowany w obraz PHP ZTS używany przez OxPHP. Dodanie linii zend_extension spowoduje ostrzeżenie przy starcie każdego żądania.

W środowisku deweloperskim ustaw opcache.validate_timestamps = 1 oraz opcache.revalidate_freq = 0, aby PHP wykrywało zmiany w plikach bez restartu kontenera.

Zobacz OPcache, gdzie znajdziesz zalecane ustawienia i konfigurację JIT.

Kontrole stanu

Dodaj Dockerową kontrolę stanu, aby Docker lub Twój orkiestrator mógł monitorować kondycję kontenera. Wymaga to ustawienia INTERNAL_ADDR.

compose.yaml
services: oxphp: environment: - INTERNAL_ADDR=0.0.0.0:9090 healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9090/health"] interval: 10s timeout: 5s retries: 3 start_period: 5s

Endpoint /health zwraca 200, gdy serwer jest sprawny, oraz 503, gdy jest w stanie pogorszonym. JSON odpowiedzi zawiera czas działania, całkowitą liczbę żądań oraz liczbę aktywnych połączeń. W Kubernetes użyj tego samego endpointu zarówno jako sondy liveness, jak i readiness.

Co dalej

  • Konfiguracja — pełne zestawienie zmiennych środowiskowych
  • Routing — tryby routingu Traditional, Framework, SPA i Worker
  • Tryb worker — trwałe procesy PHP dla aplikacji frameworkowych
  • TLS — HTTPS z wbudowaną terminacją TLS
  • Kontrole stanu — szczegóły endpointu stanu i integracja z Kubernetes
  • Łagodne zamknięcie — zachowanie przy opróżnianiu i sekwencja zamknięcia