Docker 指南

OxPHP 以容器方式运行。本指南介绍如何用 Docker 构建、配置并运行它,从最小的单阶段镜像到带有独立开发和生产目标的多阶段配置。

最小 Dockerfile

将应用容器化最简单的方式:

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

对于真实场景的应用,请使用带有独立 devprod 目标的多阶段 Dockerfile。dev 目标包含 PHP CLI、Composer 和 Xdebug。prod 目标在最小的 OxPHP 镜像之上构建,只包含生产环境所需的内容。

Tip

这份 Dockerfile 的开箱即用版本位于仓库的 examples/dockerfile/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"]

构建每个目标:

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

dev 目标基于 php:8.4-zts-alpine(把 8.4 换成 8.5 以匹配某个 OxPHP :*-php8.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 工具链(phpdocker-php-ext-installphpize),并且设置 USER 指令。下游 Dockerfile 可以直接安装 PHP 扩展,无需切换 USER

快速上手模式(单阶段)

最短的实用示例:

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

COPY 上的 --chown=www-data:www-data 很重要:镜像内文件归 www-data(uid 82)所有,因此在编排层执行 --user www-data 降权后,落在一个非特权进程可读、并且(在需要时)可写的 webroot 上。

容器以 root 身份启动,随后 oxphp serve 会在处理任何流量之前默认降权到 www-data(参见下文的安全说明)。如果你想指定某个特定的 uid 或非 www-data 用户,请在编排层显式固定身份。

最佳实践模式(两阶段,镜像更小)

要得到尽可能小的最终镜像,请在专门的构建阶段编译扩展,只把编译好的 .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 及其他维护命令,让它们不出现在服务路径中。

Note

上面的讲解仍然展示了显式的 USER root / USER www-data 切换,以实现纵深防御。在 v0.3.0 中它们是可选的,因为基础镜像不再设置 USER

运行 CLI 工具和迁移

同一个 prod 镜像可以运行 php CLI 命令来执行迁移、Composer 或临时排查。docker run 会用你传入的命令替换默认的 CMD:容器运行该命令后退出,并不会同时启动 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> 也能在运行中的容器上生效,无需任何额外参数——用于调试正在运行的容器时很方便。对于生产环境,请在 Dockerfile 中固化该扩展,让它在重启后依然存在。

安全说明

prod 镜像没有 USER 指令,所以容器 root 身份启动(与 nginx:alpine / php:*-fpm-alpine / frankenphp:alpine 的约定一致)。以 root 启动让 OxPHP 能绑定特权端口,但这已经不再意味着流量是 root 身份被服务的:oxphp serveoxphp run 默认降权到 www-data,先以 root 绑定,然后在处理任何请求或运行任何 PHP 工作进程之前永久降权。在官方镜像上(它自带 www-data 账户),这一切开箱即用,无需任何编排配置。

你仍然可以在编排层显式固定运行时身份,当你想要特定 uid、额外的纵深防御或非 www-data 用户时推荐这样做:

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

当你以这种方式以非 root 启动容器时,OxPHP 本身已经是非特权的,默认的自降权成为空操作,但此时进程无法绑定 1024 以下的端口(参见在 80 端口上以非 root 运行,以同时保留特权绑定非 root 服务)。若要刻意继续以 root 身份服务,请传入 oxphp serve --user=root

www-data 用户(uid 82、gid 82)由基础镜像预先创建,/var/www/html 在构建时已 chown 给它,因此上述任何一种降权路径——包括默认的自降权——都会落在一个可读的 webroot 上。

Note

类似 docker run … php artisan migrate 的 CLI 调用直接运行 php 二进制文件,而非 oxphp serve/run,因此它们不会自降权;它们以容器的启动用户身份运行(默认是 root)。对于这些命令,请使用 Docker 的 --user,如上所示。

在 80 端口上以非 root 运行(serve --user

在编排层降权(如上)有一个局限:以 www-data 启动的进程无法绑定特权端口(1024 以下)。要在 :80/:443 上提供服务,否则你就需要 CAP_NET_BIND_SERVICE、一个 su-exec 风格的入口脚本,或者一个高端口(例如 :8080)配合端口映射。

OxPHP 把这一切收拢到单个进程里:它以 root 绑定监听器,然后在接受任何连接或运行任何 PHP 工作进程之前永久降权。你无需额外能力即可同时获得特权端口非 root 的请求处理。默认降权到的用户是 www-data,因此在官方镜像上,只要以 root 启动容器,就已经能得到一个由 www-data 提供服务的特权绑定——无需任何标志。只有当你想降权到不同的用户时才使用 --user=<spec>;用 --user=root 来保持 root。

以 root 启动容器——不要再设置 user:,因为绑定需要 root。下面的示例显式传入 --user=www-data 以做到自我说明,但它与默认值一致:

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> 接受用户名、name:group、数字 uiduid:gid。降权过程执行 initgroups → setgid → setuid,会验证无法重新获得 root,且不可逆;在 Linux 上还会设置 no_new_privs显式的 --user 采用快速失败策略:如果进程不是以 root 启动的,serve --user 会以错误退出,而不是悄悄地继续以 root 身份运行。(默认降权则是尽力而为——以非 root 启动时它会直接跳过,因为没有什么可降的。)

Warning

只选一种模型,不要两种都用。当高端口或外部负载均衡器终结 :80 时,使用编排层降权(user: / runAsUser)。当你希望 OxPHP 自己拥有特权绑定时,使用 serve --user。同时设置 user: serve --user 会导致绑定失败:因为容器已不再是 root。

降权后用户的文件权限检查清单。 降权之后,OxPHP 在运行时接触的一切都必须能被 <spec> 访问:

资源 要求
DOCUMENT_ROOT 可读。/var/www/html 在镜像构建时已 chown 给 www-data,因此默认已满足。
会话保存路径(session.save_path,默认 /tmp 可写。
上传临时目录(upload_tmp_dir 使用文件上传时须可写。
OPcache 文件缓存(opcache.file_cache 启用次级文件缓存时须可写。
基于文件的访问日志 日志写入文件(而非 stdout)时须可写。
TLS 私钥(TLS_KEY 降权后的用户须可读——需按组或全局可读,而非仅 root 可读的 0600。私钥在降权之后才被读取,因此仅 root 可读的私钥会导致 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

卷挂载

主机路径 容器路径 用途
./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
Note

内部服务器默认关闭。设置 INTERNAL_ADDR 即可启用。在生产环境中,请让内部端口只对你的编排系统或监控系统可达;不要将其公开暴露。

PHP 配置

通过创建一个 custom.ini 文件并将其挂载进容器来自定义 PHP 设置。这是配置 OPcache、JIT、会话及其他 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

不要在此文件中添加 zend_extension=opcache。OxPHP 所用的 PHP ZTS 镜像已内置 OPcache。添加 zend_extension 行会在每次请求启动时产生一条警告。

在开发环境中,设置 opcache.validate_timestamps = 1opcache.revalidate_freq = 0,这样 PHP 无需重启容器即可拾取文件改动。

关于推荐设置和 JIT 配置,请参见 OPcache

健康检查

添加一个 Docker 健康检查,让 Docker 或你的编排系统监控容器健康状况。这需要设置 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

/health 端点在服务器健康时返回 200,在降级时返回 503。响应 JSON 包含运行时长、请求总数和活动连接数。在 Kubernetes 中,可将同一端点同时用作存活探针和就绪探针。

下一步

  • 配置 — 完整的环境变量参考
  • 路由 — 传统、框架、SPA 和工作进程路由模式
  • 工作进程模式 — 面向框架应用的持久 PHP 进程
  • TLS — 内置 TLS 终结的 HTTPS
  • 健康检查 — 健康端点细节与 Kubernetes 集成
  • 优雅关闭 — 排空行为与关闭时序