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 :

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

Cela 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.

Tip

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.

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"]

Construisez chaque cible :

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

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 :

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

Note

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.

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

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

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.

Note

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 :

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> 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.)

Warning

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

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

Montages 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
Note

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.

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

N'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.

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

L'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