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(把 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 工具链(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 用户,请在编排层显式固定身份。
最佳实践模式(两阶段,镜像更小)
要得到尽可能小的最终镜像,请在专门的构建阶段编译扩展,只把编译好的 .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 镜像可以运行 php CLI 命令来执行迁移、Composer 或临时排查。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 会拒绝该 pod,而不是悄悄以 root 身份运行。
当你以这种方式以非 root 启动容器时,OxPHP 本身已经是非特权的,默认的自降权成为空操作,但此时进程无法绑定 1024 以下的端口(参见在 80 端口上以非 root 运行,以同时保留特权绑定和非 root 服务)。若要刻意继续以 root 身份服务,请传入 oxphp serve --user=root。
www-data 用户(uid 82、gid 82)由基础镜像预先创建,/var/www/html 在构建时已 chown 给它,因此上述任何一种降权路径——包括默认的自降权——都会落在一个可读的 webroot 上。
类似 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 以做到自我说明,但它与默认值一致:
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 在镜像构建时已 chown 给 www-data,因此默认已满足。 |
会话保存路径(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。OxPHP 所用的 PHP ZTS 镜像已内置 OPcache。添加 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 中,可将同一端点同时用作存活探针和就绪探针。