Docker ガイド
OxPHP はコンテナとして実行します。このガイドでは、最小限のシングルステージイメージから、開発用と本番用のターゲットを分けたマルチステージ構成まで、Docker を使ったビルド・設定・実行の方法を解説します。
最小構成の Dockerfile
アプリケーションをコンテナ化する最もシンプルな方法です。
FROM ghcr.io/oxphp/oxphp:0.10.0
COPY --chown=www-data:www-data . /var/www/html/publicこれでアプリケーションがコンテナにコピーされます。デフォルトの DOCUMENT_ROOT は /var/www/html/public です。Laravel や Symfony、あるいは public/ サブディレクトリをすでに含むプロジェクトの場合は、代わりに COPY --chown=www-data:www-data . /var/www/html を使い、フレームワーク自身の public/ がデフォルトと一致するようにします。サーバーはデフォルトでポート 80 をリッスンします。
マルチステージの Dockerfile
実際のアプリケーションでは、dev と prod のターゲットを分けたマルチステージの Dockerfile を使います。dev ターゲットには PHP CLI、Composer、Xdebug が含まれます。prod ターゲットは最小限の OxPHP イメージの上に、本番で必要なものだけをビルドします。
すぐに使えるこの Dockerfile のバージョンは、リポジトリの examples/dockerfile/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"]各ターゲットをビルドします。
# Development image (includes PHP CLI, Composer, Xdebug)
docker build --target dev -t myapp:dev .
# Production image (minimal)
docker build --target prod -t myapp:prod .dev ターゲットは php:8.4-zts-alpine(OxPHP の :*-php8.5* タグに合わせる場合は 8.4 を 8.5 に置き換えます)をベースに OxPHP をコピーしたもので、PHP CLI と Composer にフルアクセスできます。prod ターゲットは OxPHP イメージを直接ベースにしており、本番イメージを小さく保てます。
本番環境での PHP 拡張機能のインストール
OxPHP 0.3.0 以降、本番イメージには php:8.4-zts-alpine(:*-php8.5* バリアントでは php:8.5-zts-alpine)から継承した完全な PHP ツールチェーン(php、docker-php-ext-install、phpize)が含まれ、USER ディレクティブは設定されません。下流の Dockerfile は PHP 拡張機能を直接インストールできます。USER の切り替えは不要です。
クイックスタートのパターン(シングルステージ)
最も短い実用例です。
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"]COPY に付けた --chown=www-data:www-data は重要です。イメージ内でファイルが www-data(uid 82)の所有になるため、オーケストレーターレベルの --user www-data によるドロップが、非特権プロセスが読み取り(必要に応じて書き込み)できる webroot に着地します。
コンテナは root として起動し、その後 oxphp serve がトラフィックを処理する前にデフォルトで www-data にドロップします(後述のセキュリティに関する注記を参照)。特定の uid や www-data 以外のユーザーを使いたい場合は、オーケストレーターレベルで明示的にアイデンティティを固定してください。
ベストプラクティスのパターン(2 ステージ、より小さいイメージ)
最終イメージを可能な限り小さくするには、専用のビルダーステージで拡張機能をコンパイルし、コンパイル済みの .so ファイルだけをランタイムステージにコピーします。上記のマルチステージの Dockerfile の解説では FROM ghcr.io/oxphp/oxphp:0.10.0 AS prod を使っています。シンプルでポータブルであり、出発点として推奨されます。
リポジトリの examples/dockerfile/Dockerfile はさらに踏み込んでいます。その prod ターゲットは素の alpine をベースに、明示的な apk 依存関係のリストを持ち、oxphp バイナリ、libphp.so、コンパイル済みの PHP 拡張機能、そして必要な共有ライブラリだけをコピーします。これにより、ベースイメージは約 188 MB から約 76 MB へと縮小されます(アプリケーションコードを除いて約 60% の削減)。その代償として、apk リスト内で PHP/Alpine のバージョンアップを追跡する必要があります。同じファイルには prod-cli ターゲットも含まれています。これは php artisan migrate、Composer、その他のメンテナンスコマンドを、配信経路の外に置いておくための短命なイメージです。
上記の解説では、多層防御のために USER root / USER www-data の明示的な切り替えを今も示しています。v0.3.0 以降、ベースイメージが USER を設定しなくなったため、これらはオプションです。
CLI ツールとマイグレーションの実行
同じ prod イメージで、マイグレーションや Composer、その場での調査のために php CLI コマンドを実行できます。docker run はデフォルトの CMD を渡したコマンドに置き換えます。コンテナはそのコマンドを実行して終了し、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> も、追加のフラグなしで実行中のコンテナ上で動作します。ライブコンテナのデバッグに便利です。本番環境では、再起動後も残るように Dockerfile 内で拡張機能を永続化してください。
セキュリティに関する注記
prod イメージには USER ディレクティブがないため、コンテナは root として起動します(nginx:alpine / php:*-fpm-alpine / frankenphp:alpine の慣習に合わせています)。root として起動することで OxPHP は特権ポートをバインドできますが、これはトラフィックが root として配信されることを意味しなくなりました。oxphp serve と oxphp run はデフォルトで www-data にドロップし、root としてバインドした後、リクエストが処理されたり PHP ワーカーが動作したりする前に恒久的にドロップします。公式イメージ(www-data アカウントを含む)では、これがオーケストレーターの設定なしに標準で行われます。
特定の uid、追加の多層防御、または 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 は多層防御です。もし runAsUser が削除されたり 0 に上書きされたりした場合、kubelet は静かに root として実行する代わりに Pod を拒否します。
このように非 root でコンテナを起動すると、OxPHP はすでに非特権であり、デフォルトのセルフドロップは何もしません。ただし、その場合プロセスは 1024 未満のポートをバインドできなくなります(特権バインドと非 root 配信の両方を維持する方法は ポート 80 で非 root として実行する を参照)。意図的に root として配信を続けたい場合は、oxphp serve --user=root を渡します。
www-data ユーザー(uid 82、gid 82)はベースイメージによってあらかじめ作成されており、/var/www/html はビルド時にそのユーザーへ chown されます。そのため、デフォルトのセルフドロップを含むこれらのドロップ経路のいずれも、読み取り可能な webroot に着地します。
docker run … php artisan migrate のような CLI 呼び出しは oxphp serve/run ではなく php バイナリを直接実行するため、セルフドロップは行われません。これらはコンテナの起動ユーザー(デフォルトでは root)として実行されます。それらには上記のように Docker の --user を使ってください。
ポート 80 で非 root として実行する(serve --user)
オーケストレーターレベルでの権限ドロップ(上記)には 1 つの制約があります。www-data として起動したプロセスは特権ポート(1024 未満)をバインドできません。:80/:443 で配信するには、そうでなければ CAP_NET_BIND_SERVICE、su-exec 方式のエントリーポイント、あるいはポートマッピングの背後にある高位ポート(:8080 など)が必要になります。
OxPHP はこれを単一のプロセスに集約します。リスナーを root としてバインドし、その後、接続が受け入れられたり PHP ワーカーが動作したりする前に恒久的にドロップします。追加のケイパビリティなしで、特権ポートと非 root でのリクエスト処理の両方が得られます。デフォルトではドロップ先のユーザーは www-data なので、公式イメージでは単にコンテナを root として起動するだけで、www-data が配信する特権バインドが得られます。フラグは不要です。別のユーザーにドロップする場合にのみ --user=<spec> を使い、root を維持する場合は --user=root を使います。
コンテナは root として起動してください。バインドには root が必要なので、user: を併せて設定しないでください。以下の例では自己文書化のために --user=www-data を明示的に渡していますが、これはデフォルトと同じです。
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> はユーザー名、name:group、数値の uid、または uid:gid を受け付けます。ドロップは initgroups → setgid → setuid を実行し、root を再取得できないことを検証し、不可逆です。Linux では no_new_privs も設定します。明示的な --user はフェイルファストです。プロセスが root として起動されていない場合、serve --user は静かに root として続行するのではなくエラーで終了します。(一方、デフォルトのドロップはベストエフォートです。非 root で起動された場合、ドロップするものがないため単にスキップします。)
どちらか一方のモデルを選び、両方は使わないでください。高位ポートを使う場合、または外部のロードバランサーが :80 を終端する場合は、オーケストレーターのドロップ(user: / runAsUser)を使います。OxPHP 自身に特権バインドを所有させたい場合は serve --user を使います。user: と serve --user を両方設定するとバインドが失敗します。コンテナがもはや root ではなくなるためです。
ドロップ先ユーザーのためのファイル権限チェックリスト。 ドロップ後、OxPHP がランタイムで触れるものすべてが <spec> からアクセス可能でなければなりません。
| リソース | 要件 |
|---|---|
DOCUMENT_ROOT |
読み取り可能であること。/var/www/html はイメージビルド時に www-data へ chown されるため、デフォルトで満たされます。 |
セッション保存パス(session.save_path、デフォルトは /tmp) |
書き込み可能であること。 |
アップロードの一時ディレクトリ(upload_tmp_dir) |
ファイルアップロードを使う場合、書き込み可能であること。 |
OPcache ファイルキャッシュ(opcache.file_cache) |
二次的なファイルキャッシュを有効にする場合、書き込み可能であること。 |
| ファイルベースのアクセスログ | stdout ではなくファイルにログを出力する場合、書き込み可能であること。 |
TLS 秘密鍵(TLS_KEY) |
ドロップ先ユーザーが読み取り可能であること — root 専用の 0600 ではなく、グループまたは全体が読み取り可能であること。鍵はドロップの後に読み取られるため、root 専用の鍵では 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-stoppedソースディレクトリをボリュームとしてマウントすると、リビルドせずにファイルの変更が反映されます。dev ターゲットは OPcache のタイムスタンプ検証が有効になっているため、PHP が変更を自動的に取り込みます。
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=allボリュームマウント
| ホストパス | コンテナパス | 用途 |
|---|---|---|
./src |
/var/www/html |
アプリケーションファイル(PHP スクリプト、静的アセット)。本番では :ro を使用 |
./custom.ini |
/usr/local/etc/php/conf.d/custom.ini |
PHP ランタイム設定(OPcache、セッション、JIT)。:ro を使用 |
./certs |
/etc/ssl/oxphp |
TLS 証明書と秘密鍵。:ro を使用 |
ポートのリファレンス
| ポート | 環境変数 | 用途 |
|---|---|---|
80 |
LISTEN_ADDR |
メインの HTTP サーバー |
443 |
LISTEN_ADDR |
メインの HTTPS サーバー(TLS が設定されている場合) |
9090 |
INTERNAL_ADDR |
内部サーバー: /health、/metrics、/config |
内部サーバーはデフォルトで無効です。有効にするには INTERNAL_ADDR を設定します。本番環境では、内部ポートはオーケストレーターや監視システムからのみ到達可能にしておき、公開しないでください。
PHP の設定
custom.ini ファイルを作成してコンテナにマウントすることで、PHP の設定をカスタマイズできます。これは OPcache、JIT、セッション、その他の 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 = 1このファイルに zend_extension=opcache を追加しないでください。OPcache は OxPHP が使用する PHP ZTS イメージにすでに組み込まれています。zend_extension の行を追加すると、リクエストの起動ごとに警告が発生します。
開発環境では opcache.validate_timestamps = 1 と opcache.revalidate_freq = 0 を設定すると、コンテナを再起動せずに PHP がファイルの変更を取り込みます。
推奨設定と JIT の構成については OPcache を参照してください。
ヘルスチェック
Docker のヘルスチェックを追加すると、Docker やオーケストレーターがコンテナの健全性を監視できます。これには 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 1/health エンドポイントは、サーバーが健全なときに 200 を、劣化しているときに 503 を返します。レスポンスの JSON には稼働時間、総リクエスト数、アクティブな接続数が含まれます。Kubernetes では、同じエンドポイントを liveness プローブと readiness プローブの両方に使ってください。