Guide Docker
OxPHP s'exécute en tant que conteneur. Ce guide couvre sa construction, sa configuration et son exécution avec Docker, d'une image minimale à une seule étape jusqu'à une configuration multi-étapes avec des cibles de développement et de production distinctes.
Dockerfile minimal
La manière la plus simple de conteneuriser votre application :
FROM ghcr.io/oxphp/oxphp:0.10.0
COPY --chown=www-data:www-data . /var/www/html/publicCela copie votre application dans le conteneur. Le DOCUMENT_ROOT par défaut est /var/www/html/public. Pour Laravel, Symfony ou tout projet qui embarque déjà un sous-répertoire public/, utilisez plutôt COPY --chown=www-data:www-data . /var/www/html afin que le public/ propre au framework s'aligne sur la valeur par défaut. Le serveur écoute sur le port 80 par défaut.
Dockerfile multi-étapes
Pour des applications réelles, utilisez un Dockerfile multi-étapes avec des cibles dev et prod distinctes. La cible dev inclut PHP CLI, Composer et Xdebug. La cible prod s'appuie sur l'image OxPHP minimale avec uniquement ce qui est nécessaire en production.
Une version prête à l'emploi de ce Dockerfile se trouve dans examples/dockerfile/Dockerfile dans le dépôt. Copiez-la dans votre projet et ajustez les extensions selon vos besoins.
# ── 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"]Construisez chaque cible :
# Development image (includes PHP CLI, Composer, Xdebug)
docker build --target dev -t myapp:dev .
# Production image (minimal)
docker build --target prod -t myapp:prod .La cible dev est basée sur php:8.4-zts-alpine (remplacez 8.4 par 8.5 pour correspondre à un tag OxPHP :*-php8.5*) avec OxPHP copié dedans, ce qui vous donne un accès complet à PHP CLI et Composer. La cible prod est basée directement sur l'image OxPHP, ce qui maintient l'image de production petite.
Installer des extensions PHP en production
Depuis OxPHP 0.3.0, l'image de production embarque la chaîne d'outils PHP complète (php, docker-php-ext-install, phpize) héritée de php:8.4-zts-alpine (ou php:8.5-zts-alpine pour les variantes :*-php8.5*) et ne définit pas de directive USER. Les Dockerfiles en aval peuvent installer des extensions PHP directement. Aucun basculement de USER requis.
Modèle de démarrage rapide (une seule étape)
L'exemple utile le plus court :
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 sur le COPY est important : dans l'image, les fichiers appartiennent à www-data (uid 82), de sorte qu'un abaissement de privilèges au niveau de l'orchestrateur via --user www-data aboutit sur une racine web que le processus non privilégié peut lire et (au besoin) écrire.
Le conteneur démarre en tant que root, puis oxphp serve s'abaisse à www-data par défaut avant de servir le moindre trafic (voir la note de sécurité ci-dessous). Fixez explicitement l'identité au niveau de l'orchestrateur si vous voulez un uid spécifique ou un utilisateur autre que www-data.
Modèle de bonne pratique (deux étapes, image plus petite)
Pour l'image finale la plus petite possible, compilez les extensions dans une étape de construction dédiée et ne copiez que les fichiers .so compilés dans l'étape d'exécution. La présentation du Dockerfile multi-étapes ci-dessus utilise FROM ghcr.io/oxphp/oxphp:0.10.0 AS prod — simple, portable et recommandé comme point de départ.
examples/dockerfile/Dockerfile dans le dépôt va plus loin : sa cible prod est basée sur alpine nu avec une liste de dépendances apk explicite, ne copiant que le binaire oxphp, libphp.so, les extensions PHP compilées et les bibliothèques partagées requises. Cela réduit l'image de base de ~188 Mo à ~76 Mo (~60 % de réduction, hors code de votre application), au prix du suivi des montées de version PHP/Alpine dans la liste apk. Le même fichier fournit également une cible prod-cli — une image éphémère pour php artisan migrate, Composer et d'autres commandes de maintenance qui doivent rester hors du chemin de service.
La présentation ci-dessus montre encore des basculements explicites USER root / USER www-data pour la défense en profondeur. Avec la v0.3.0, ils sont optionnels, puisque l'image de base ne définit plus de USER.
Exécuter des outils CLI et des migrations
La même image prod peut exécuter des commandes CLI php pour les migrations, Composer ou une inspection ponctuelle. docker run remplace le CMD par défaut par la commande que vous passez : le conteneur exécute la commande puis se termine, il ne démarre pas aussi le serveur 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> fonctionne aussi sur un conteneur en cours d'exécution sans aucun flag supplémentaire — utile pour déboguer un conteneur en direct. Pour la production, persistez l'extension dans votre Dockerfile afin qu'elle survive aux redémarrages.
Note de sécurité
L'image prod n'a pas de directive USER, donc le conteneur démarre en tant que root (conformément aux conventions de nginx:alpine / php:*-fpm-alpine / frankenphp:alpine). Démarrer en tant que root permet à OxPHP de se lier à des ports privilégiés, mais cela ne signifie plus que le trafic est servi en tant que root : oxphp serve et oxphp run s'abaissent à www-data par défaut, se liant en tant que root puis abandonnant définitivement ces privilèges avant qu'une requête ne soit traitée ou qu'un worker PHP ne s'exécute. Sur l'image officielle (qui embarque le compte www-data), cela se produit d'emblée, sans aucune configuration de l'orchestrateur.
Vous pouvez toujours fixer explicitement l'identité d'exécution au niveau de l'orchestrateur, ce qui est recommandé lorsque vous voulez un uid spécifique, une défense en profondeur supplémentaire ou un utilisateur autre que 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 relève de la défense en profondeur : si runAsUser est un jour supprimé ou remplacé par 0, le kubelet rejette le pod au lieu de s'exécuter silencieusement en tant que root.
Lorsque vous démarrez le conteneur en tant que non-root de cette manière, OxPHP est déjà non privilégié et l'auto-abaissement par défaut est sans effet, mais le processus ne peut alors plus se lier à des ports inférieurs à 1024 (voir Exécuter en tant que non-root sur le port 80 pour conserver une liaison privilégiée et un service non-root). Pour continuer délibérément à servir en tant que root, passez oxphp serve --user=root.
L'utilisateur www-data (uid 82, gid 82) est pré-créé par l'image de base, et /var/www/html lui est attribué (chown) au moment de la construction, de sorte que chacun de ces chemins d'abaissement — y compris l'auto-abaissement par défaut — aboutit sur une racine web lisible.
Les invocations CLI comme docker run … php artisan migrate exécutent directement le binaire php, et non oxphp serve/run, donc elles ne s'auto-abaissent pas ; elles s'exécutent avec l'utilisateur de démarrage du conteneur (root par défaut). Utilisez le --user de Docker pour celles-ci, comme montré ci-dessus.
Exécuter en tant que non-root sur le port 80 (serve --user)
L'abaissement des privilèges au niveau de l'orchestrateur (ci-dessus) a une limitation : un processus qui démarre en tant que www-data ne peut pas se lier à un port privilégié (inférieur à 1024). Pour servir sur :80/:443, il vous faudrait sinon CAP_NET_BIND_SERVICE, un point d'entrée de type su-exec, ou un port élevé (tel que :8080) derrière un mappage de ports.
OxPHP condense tout cela en un seul processus : il lie les écouteurs en tant que root, puis abandonne définitivement ces privilèges avant qu'une connexion ne soit acceptée ou qu'un worker PHP ne s'exécute. Vous obtenez un port privilégié et un traitement des requêtes non-root sans capacités supplémentaires. Par défaut, l'utilisateur cible de l'abaissement est www-data, donc sur l'image officielle, il suffit de démarrer le conteneur en tant que root pour obtenir une liaison privilégiée servie par www-data — aucun flag requis. Utilisez --user=<spec> uniquement pour vous abaisser à un utilisateur différent ; utilisez --user=root pour rester root.
Démarrez le conteneur en tant que root — ne définissez pas aussi user:, car la liaison a besoin de root. L'exemple ci-dessous passe --user=www-data explicitement pour être auto-documenté, mais cela correspond à la valeur par défaut :
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> accepte un nom d'utilisateur, name:group, un uid numérique, ou uid:gid. L'abaissement exécute initgroups → setgid → setuid, vérifie que root ne peut pas être récupéré, et est irréversible ; sous Linux, il définit également no_new_privs. Un --user explicite échoue rapidement (fail-fast) : si le processus n'est pas démarré en tant que root, serve --user se termine avec une erreur plutôt que de continuer silencieusement en tant que root. (L'abaissement par défaut, lui, est au mieux (best-effort) — démarré non-root, il est simplement ignoré, puisqu'il n'y a rien à abaisser.)
Choisissez un seul modèle, pas les deux. Utilisez l'abaissement par l'orchestrateur (user: / runAsUser) lorsqu'un port élevé ou un répartiteur de charge externe termine :80. Utilisez serve --user lorsque vous voulez qu'OxPHP lui-même possède la liaison privilégiée. Définir user: et serve --user fait échouer la liaison : le conteneur n'est plus root.
Liste de contrôle des permissions de fichiers pour l'utilisateur cible de l'abaissement. Après l'abaissement, tout ce à quoi OxPHP touche à l'exécution doit être accessible à <spec> :
| Ressource | Exigence |
|---|---|
DOCUMENT_ROOT |
Lisible. /var/www/html est attribué (chown) à www-data lors de la construction de l'image, donc cette condition est satisfaite par défaut. |
Chemin de sauvegarde des sessions (session.save_path, /tmp par défaut) |
Accessible en écriture. |
Répertoire temporaire des téléversements (upload_tmp_dir) |
Accessible en écriture, lorsque les téléversements de fichiers sont utilisés. |
Cache de fichiers OPcache (opcache.file_cache) |
Accessible en écriture, lorsqu'un cache de fichiers secondaire est activé. |
| Journal des accès basé sur fichier | Accessible en écriture, lors de la journalisation vers un fichier plutôt que vers stdout. |
Clé privée TLS (TLS_KEY) |
Lisible par l'utilisateur cible de l'abaissement — lisible par le groupe ou par tous, et non 0600 réservé à root. La clé est lue après l'abaissement, donc une clé réservée à root fait échouer le démarrage de 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-stoppedMontez votre répertoire source en tant que volume afin que les modifications de fichiers soient prises en compte sans reconstruction. La cible dev a la validation des timestamps OPcache activée, donc PHP prend en compte les changements automatiquement.
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=allMontages de volumes
| Chemin hôte | Chemin conteneur | Rôle |
|---|---|---|
./src |
/var/www/html |
Fichiers de l'application (scripts PHP, ressources statiques). Utilisez :ro en production |
./custom.ini |
/usr/local/etc/php/conf.d/custom.ini |
Configuration d'exécution PHP (OPcache, sessions, JIT). Utilisez :ro |
./certs |
/etc/ssl/oxphp |
Certificat TLS et clé privée. Utilisez :ro |
Référence des ports
| Port | Variable d'environnement | Rôle |
|---|---|---|
80 |
LISTEN_ADDR |
Serveur HTTP principal |
443 |
LISTEN_ADDR |
Serveur HTTPS principal (lorsque TLS est configuré) |
9090 |
INTERNAL_ADDR |
Serveur interne : /health, /metrics, /config |
Le serveur interne est désactivé par défaut. Définissez INTERNAL_ADDR pour l'activer. En production, gardez le port interne accessible uniquement par votre orchestrateur ou votre système de surveillance ; ne l'exposez pas publiquement.
Configuration PHP
Personnalisez les paramètres PHP en créant un fichier custom.ini et en le montant dans le conteneur. C'est la manière recommandée de configurer OPcache, JIT, les sessions et d'autres paramètres d'exécution 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 = 1N'ajoutez pas zend_extension=opcache à ce fichier. OPcache est déjà intégré à l'image PHP ZTS utilisée par OxPHP. Ajouter une ligne zend_extension produira un avertissement au démarrage de chaque requête.
En développement, définissez opcache.validate_timestamps = 1 et opcache.revalidate_freq = 0 afin que PHP prenne en compte les modifications de fichiers sans redémarrer le conteneur.
Consultez OPcache pour les paramètres recommandés et la configuration JIT.
Contrôles de santé
Ajoutez un contrôle de santé Docker pour permettre à Docker ou à votre orchestrateur de surveiller la santé du conteneur. Cela nécessite que INTERNAL_ADDR soit défini.
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 1L'endpoint /health renvoie 200 lorsque le serveur est sain et 503 lorsqu'il est dégradé. Le JSON de réponse inclut la durée de fonctionnement, le nombre total de requêtes et le nombre de connexions actives. Pour Kubernetes, utilisez le même endpoint à la fois comme sonde de vivacité (liveness) et de disponibilité (readiness).
Et ensuite
- Configuration — référence complète des variables d'environnement
- Routage — modes de routage Traditional, Framework, SPA et Worker
- Mode worker — processus PHP persistants pour les applications de framework
- TLS — HTTPS avec terminaison TLS intégrée
- Contrôles de santé — détails de l'endpoint de santé et intégration Kubernetes
- Arrêt gracieux — comportement de drainage et séquence d'arrêt