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:
FROM ghcr.io/oxphp/oxphp:0.10.0
COPY --chown=www-data:www-data . /var/www/html/publicTo 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.
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.
# ── 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:
# Development image (includes PHP CLI, Composer, Xdebug)
docker build --target dev -t myapp:dev .
# Production image (minimal)
docker build --target prod -t myapp:prod .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:
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ń.
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.
# 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 migratedocker 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:
docker run --user www-data ghcr.io/oxphp/oxphp:0.10.0services:
oxphp:
image: ghcr.io/oxphp/oxphp:0.10.0
user: www-datasecurityContext:
runAsNonRoot: true
runAsUser: 82
runAsGroup: 82runAsNonRoot: true to ochrona warstwowa: jeśli runAsUser zostanie kiedykolwiek usunięty lub nadpisany na 0, kubelet odrzuci poda zamiast po cichu uruchomić go jako root.
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.
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ą:
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ć.)
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
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-stoppedZamontuj swój katalog źródłowy jako wolumen, aby zmiany w plikach były uwzględniane bez przebudowywania. Cel dev ma włączoną walidację znaczników czasu OPcache, więc PHP automatycznie wykrywa zmiany.
services:
oxphp:
build:
context: .
target: dev
ports:
- "80:80"
- "9090:9090"
volumes:
- ./src:/var/www/html:ro
- ./custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
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=debug
- ACCESS_LOG=allMontowanie 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 |
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.
; 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 = 1Nie 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.
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: 5sHEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=5s \
CMD wget --quiet --tries=1 --spider http://localhost:9090/health || exit 1Endpoint /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