FrankenPHP Docker 镜像完全指南:构建、配置、扩展与安全加固
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
本篇技术指南围绕 FrankenPHP 官方 Docker 镜像展开,覆盖从快速启动、标签选择、定制 Caddyfile、安装 PHP 扩展与 Caddy 模块,到 worker 模式默认化、非 root 运行、distroless 安全加固与开发版本使用等完整实战路径。读完本文,你将能够基于dunglas/frankenphp镜像构建生产可用、可扩展、可加固的自定义 PHP 应用镜像,并理解镜像内部的目录布局与构建原理。
镜像基础与标签体系
FrankenPHP 的 Docker 镜像基于官方 PHP 镜像构建,同时提供 Debian 与 Alpine Linux 两个系列,并覆盖多种流行 CPU 架构。官方文档明确建议优先使用 Debian 系列镜像——它的基础工具链更完整,调试与排障更加方便。
镜像同时提供 PHP 8.2、8.3、8.4 和 8.5 四个大版本。这一点在仓库的 docker-bake.hcl 中可以得到印证:构建矩阵的PHP_VERSION默认值为8.2,8.3,8.4,8.5,并且默认 PHP 大版本(DEFAULT_PHP_VERSION)为8.5,即不带 PHP 后缀的latest标签指向 8.5。
标签遵循如下统一模式:
dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os><frankenphp-version>与<php-version>分别是 FrankenPHP 和 PHP 的版本号,粒度从主版本(如1)、次版本(如1.2)一直到补丁版本(如1.2.3)都会提供;<os>取值有三种:trixie(Debian Trixie)、bookworm(Debian Bookworm)或alpine(Alpine 的最新稳定版)。
关于标签的生成逻辑,docker-bake.hcl 中的tag()函数给出了精确规则:当 PHP 版本等于默认版本(8.5)且操作系统为trixie时,会额外生成不带-phpX.Y-os后缀的简写标签;默认版本下还会生成仅带-os后缀的标签。也就是说,dunglas/frankenphp:latest、dunglas/frankenphp:latest-trixie等便捷标签是实际存在的。仓库根目录的 Dockerfile(Debian 系)与 alpine.Dockerfile(Alpine 系)分别负责两个系列的镜像构建,builder与runner两个目标阶段在 docker-bake.hcl 的矩阵中被同时构建与推送。
快速开始:构建并运行你的第一个 PHP 应用
在你项目的根目录创建一个Dockerfile:
FROM dunglas/frankenphp COPY . /app/public然后执行构建与运行:
docker build -t my-php-app . docker run -it --rm --name my-running-app my-php-app镜像默认的工作目录是/app,站点根目录为/app/public。仓库中的 package/content/index.php 就是镜像内预置的欢迎页(“Your FrankenPHP server is up and running.”),当你尚未把自己的代码复制进容器时,访问站点会看到这个页面,用于验证服务是否正常启动。
从 Dockerfile 可以看到镜像默认行为的关键细节:
- 默认启动命令为
CMD ["--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"],即直接以 Caddyfile 适配器运行 FrankenPHP; - 内置健康检查
HEALTHCHECK CMD curl -f http://localhost:2019/metrics || exit 1,通过 Caddy 管理接口的 metrics 端点探活; - 声明暴露
80、443、443/udp(HTTP/3)以及2019(Caddy admin API)四个端口; - 预置环境变量
XDG_CONFIG_HOME=/config与XDG_DATA_HOME=/data,遵循 Caddy 的配置/数据目录约定。
调整 FrankenPHP 的 Docker 配置
为了方便使用者,镜像内置了一份包含常用环境变量的默认 Caddyfile。这份文件被复制到/etc/caddy/Caddyfile,并通过软链接同时指向/etc/frankenphp/Caddyfile(详见 Dockerfile)。
这份默认 Caddyfile 的全部可注入变量如下,它们让你无需修改文件本身即可完成大部分配置:
| 环境变量 | 作用 |
|---|---|
SERVER_NAME | 站点监听地址与域名,默认localhost,同时决定自动签发 TLS 证书的主机名 |
SERVER_ROOT | 站点根目录,默认public/ |
FRANKENPHP_CONFIG | 注入frankenphp指令块内的配置(如启用 worker) |
CADDY_GLOBAL_OPTIONS | 注入 Caddy 全局选项(如debug、servers { enable_full_duplex }) |
CADDY_EXTRA_CONFIG | 追加额外的全局配置片段 |
CADDY_SERVER_EXTRA_DIRECTIVES | 在站点块内追加额外指令 |
MERCURE_PUBLISHER_JWT_KEY/MERCURE_SUBSCRIBER_JWT_KEY/MERCURE_* | Mercure 模块的 JWT 密钥与算法等(默认注释关闭) |
默认 Caddyfile 的站点块启用了encode zstd br gzip压缩与php_server指令,且文件末尾通过import Caddyfile.d/*.caddyfile支持把额外的.caddyfile文件放入Caddyfile.d目录实现自动加载——这与 docs/config.md 中描述的 Docker 配置目录约定一致(主配置/etc/frankenphp/Caddyfile,附加配置/etc/frankenphp/Caddyfile.d/*.caddyfile)。镜像内的官方构建入口 main.go 显示,官方二进制默认就内置了 Mercure 与 Vulcain 模块,因此默认 Caddyfile 中这两段的注释只是“开箱即用”的开关而非需要重新编译的功能。
安装更多 PHP 扩展
基础镜像中内置了install-php-extensions脚本(在 Dockerfile 构建阶段从上游下载并安装到/usr/local/bin/),它可以自动处理扩展的编译、依赖安装与配置。添加扩展非常简单:
FROM dunglas/frankenphp # add additional extensions here: RUN install-php-extensions \ pdo_mysql \ gd \ intl \ zip \ opcache需要说明的是,FrankenPHP 官方镜像内置的 PHP 是 ZTS(线程安全)版本,扩展目录遵循 PHP 官方镜像的布局,位于/usr/local/lib/php/extensions/no-debug-zts-<日期>/;install-php-extensions会自动识别正确的架构与线程模式进行安装。如果你需要在容器中同时调整 PHP 运行时配置,可以参考 docs/config.md:镜像不提供默认php.ini,可通过以下方式复制官方模板:
FROM dunglas/frankenphp # Production: RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini # Or development: RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini附加的.ini文件则放入/usr/local/etc/php/conf.d/。
安装更多 Caddy 模块
FrankenPHP 构建在 Caddy 之上,因此所有 Caddy 模块都能与之协同工作。安装自定义 Caddy 模块最便捷的方式是使用xcaddy重新编译二进制,官方为此提供了专用的builder镜像。
FROM dunglas/frankenphp:builder AS builder # Copy xcaddy in the builder image COPY --from=caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy # CGO must be enabled to build FrankenPHP RUN CGO_ENABLED=1 \ XCADDY_SETCAP=1 \ XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ CGO_CFLAGS=$(php-config --includes) \ CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ xcaddy build \ --output /usr/local/bin/frankenphp \ --with github.com/dunglas/frankenphp=./ \ --with github.com/dunglas/frankenphp/caddy=./caddy/ \ --with github.com/dunglas/caddy-cbrotli \ # Mercure and Vulcain are included in the official build, but feel free to remove them --with github.com/dunglas/mercure/caddy \ --with github.com/dunglas/vulcain/caddy # Add extra Caddy modules here FROM dunglas/frankenphp AS runner # Replace the official binary with the one containing your custom modules COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp其中builder镜像包含预编译好的libphp,因此无需在容器内再次编译 PHP;所有 FrankenPHP 与 PHP 版本、Debian 与 Alpine 系列都提供了对应的 builder 标签。
几点关键细节值得注意:
CGO_ENABLED=1必须开启,因为 FrankenPHP 需要把 PHP 作为 C 共享库链接进二进制;CGO_CFLAGS与CGO_LDFLAGS通过php-config动态获取,确保链接器能找到 PHP 的头文件与库;XCADDY_SETCAP=1让编译产物自动获得绑定特权端口的 capability(见下文非 root 部分);--tags=nobadger,nomysql,nopgx用于去掉官方构建中与 Caddy 无关的存储后端,缩小体积。
这种基于 xcaddy 的编译方式与本地源码编译(见 docs/compile.md)本质一致:compile.md中同样提供了CGO_ENABLED=1、XCADDY_GO_BUILD_FLAGS等环境变量的完整说明,并提醒在 Alpine(musl libc)下若运行 Symfony 出现Maximum call stack size ... reached during compilation之类的错误,需要通过-Wl,-z,stack-size=0x80000增大默认栈大小。有趣的是,alpine.Dockerfile 中官方已经默认给 Alpine 构建加入了-extldflags '-Wl,-z,stack-size=0x80000'参数,因此 Alpine 镜像内置二进制无需担心该问题;但如果你在自定义编译中复刻上述 Dockerfile,请保留这一参数。
默认启用 worker 模式
worker 模式会把 PHP 应用常驻内存、避免每次请求都重新引导框架,从而把请求处理时间降到毫秒级。在 Docker 中启用它只需要设置FRANKENPHP_CONFIG环境变量:
FROM dunglas/frankenphp # ... ENV FRANKENPHP_CONFIG="worker ./public/index.php"默认 Caddyfile 中的{$FRANKENPHP_CONFIG}占位符会把该值注入frankenphp全局指令块。关于 worker 的数量,docs/worker.md 说明默认按“每个 CPU 2 个 worker”启动,也可以在配置中显式指定数量,例如worker ./public/index.php 42。更详细的 worker 指令(file、num、env、watch、max_consecutive_failures等)可参考 docs/config.md 中的frankenphp全局块配置,这些同样可以通过FRANKENPHP_CONFIG环境变量注入。
开发模式下使用数据卷
开发时最方便的玩法是把宿主机的源码目录直接挂载进容器,实现改代码即时生效:
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app这里-p 443:443/udp是为 HTTP/3(QUIC)预留的 UDP 端口。--tty选项能让 Caddy 输出易读的人类日志而不是 JSON 日志,适合开发调试。
使用 Docker Compose 的完整配置:
# compose.yaml services: php: image: dunglas/frankenphp # uncomment the following line if you want to use a custom Dockerfile #build: . # uncomment the following line if you want to run this in a production environment # restart: always ports: - "80:80" # HTTP - "443:443" # HTTPS - "443:443/udp" # HTTP/3 volumes: - ./:/app/public - caddy_data:/data - caddy_config:/config # comment the following line in production, it provides nice human-readable logs in dev tty: true # Volumes needed for Caddy certificates and configuration volumes: caddy_data: caddy_config:caddy_data与caddy_config两个命名卷分别对应容器内的/data与/config(即XDG_DATA_HOME与XDG_CONFIG_HOME指向的目录),用于持久化 Caddy 自动签发的 TLS 证书与管理状态。如果挂载了自定义 Caddyfile(例如放到/etc/caddy/Caddyfile),记得同时保留/data、/config的持久化卷,避免证书反复重新签发。
以非 root 用户运行
FrankenPHP 支持在 Docker 中以非 root 身份运行。官方推荐的做法是通过setcap把CAP_NET_BIND_SERVICEcapability 赋予二进制,使其在非 root 状态下仍能绑定 80/443 特权端口:
FROM dunglas/frankenphp ARG USER=appuser RUN <<-EOF # Use "adduser -D ${USER}" for alpine based distros useradd ${USER} # Add additional capability to bind to port 80 and 443 setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp # Give write access to /config/caddy and /data/caddy chown -R ${USER}:${USER} /config/caddy /data/caddy EOF USER ${USER}注意 Alpine 系列的基础镜像没有useradd,需要改用adduser -D ${USER}。
零 capability 运行
即便以非 root 运行,只要监听 80/443 特权端口,二进制仍需要CAP_NET_BIND_SERVICE。如果你把服务暴露在 1024 及以上的非特权端口,就可以同时去掉非 root 和 capability:
FROM dunglas/frankenphp ARG USER=appuser RUN <<-EOF # Use "adduser -D ${USER}" for alpine based distros useradd ${USER} # Remove default capability setcap -r /usr/local/bin/frankenphp # Give write access to /config/caddy and /data/caddy chown -R ${USER}:${USER} /config/caddy /data/caddy EOF USER ${USER}随后通过环境变量把监听地址改为非特权端口即可:
SERVER_NAME=:8000这里的原理在 Dockerfile 与 alpine.Dockerfile 中都有体现:官方镜像在构建阶段对二进制执行了setcap cap_net_bind_service=+ep(Debian 系通过libcap2-bin提供setcap,Alpine 系通过libcap提供),这是镜像默认能绑定 80/443 的原因;上面的 Dockerfile 正是对这一默认行为做加减法。此外,/config/caddy与/data/caddy必须对运行用户可写,否则 Caddy 无法落盘证书与状态。
镜像更新机制
官方 Docker 镜像在两种情况下会被重新构建:
- 每次打新的 FrankenPHP 版本发布标签时;
- 每天 UTC 时间凌晨 4 点,且官方 PHP 镜像有新版本可用时。
结合 docker-bake.hcl 的实现来看,这套流水线把 PHP 版本、操作系统与 target(builder/runner)做成了矩阵,并对每个变体记录基础镜像指纹(dev.frankenphp.base.fingerprint标签),从而在官方 PHP 镜像更新时能精准触发对应变体的重建。也就是说,使用dunglas/frankenphp镜像可以获得相对及时的 PHP 安全补丁与版本更新。
加固镜像:distroless 与 Docker hardened
为了进一步缩小攻击面与镜像体积,官方支持在 Google distroless 或 Docker hardened 这类精简基础镜像之上构建 FrankenPHP 镜像。
[!WARNING] 这类极简基础镜像不包含 shell 与包管理器,调试会变得非常困难,因此仅在安全优先级很高的生产环境中才建议使用。
由于精简基础镜像中没有编译工具链,安装 PHP 扩展必须放在一个中间构建阶段完成,并通过libtree递归收集二进制及其所有扩展依赖的共享库:
FROM dunglas/frankenphp AS builder # Add additional PHP extensions here RUN install-php-extensions pdo_mysql pdo_pgsql #... # Copy shared libs of frankenphp and all installed extensions to temporary location # You can also do this step manually by analyzing ldd output of frankenphp binary and each extension .so file RUN <<-EOF apt-get update apt-get install -y --no-install-recommends libtree mkdir -p /tmp/libs for target in $(which frankenphp) \ $(find "$(php -r 'echo ini_get("extension_dir");')" -maxdepth 2 -name "*.so"); do libtree -pv "$target" 2>/dev/null | grep -oP '(?:── )\K/\S+(?= \[)' | while IFS= read -r lib; do [ -f "$lib" ] && cp -n "$lib" /tmp/libs/ done done EOF # Distroless Debian base image, make sure this matches the Debian version of the builder FROM gcr.io/distroless/base-debian13 # Docker hardened image alternative # FROM dhi.io/debian:13 COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions COPY --from=builder /tmp/libs /usr/lib COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d COPY --from=builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini # Config and data dirs must be writable for nonroot, even on a read-only root filesystem ENV XDG_CONFIG_HOME=/config XDG_DATA_HOME=/data COPY --from=builder --chown=nonroot:nonroot /data /data COPY --from=builder --chown=nonroot:nonroot /config /config # Copy your app (kept root-owned) and Caddyfile COPY . /app COPY Caddyfile /etc/caddy/Caddyfile USER nonroot WORKDIR /app ENTRYPOINT ["/usr/local/bin/frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]这个 Dockerfile 的几个关键点:
- 基础版本必须匹配:distroless 的 Debian 大版本(示例中为
debian13,对应 Trixie)必须与 builder 镜像的 Debian 版本一致,否则动态链接库可能出现 ABI 不兼容; - 依赖收集:
libtree会解析frankenphp二进制与所有扩展.so的ldd依赖树,cp -n保证不重复复制;注释也提示了可以手动分析ldd输出替代; - 非 root 与只读文件系统:
XDG_CONFIG_HOME/XDG_DATA_HOME指向的/config、/data必须由nonroot用户拥有(--chown=nonroot:nonroot),应用代码则保持 root 所有、只读挂载; - 最终以
USER nonroot运行,入口通过ENTRYPOINT显式指定配置与适配器。
开发版本(daily builds)
除稳定镜像外,官方还维护dunglas/frankenphp-dev仓库,每当 GitHub 主分支有新提交推送时就会触发一次新构建:
latest*标签指向main分支的最新提交;sha-<git-commit-hash>形式的标签对应特定提交,便于复现某个开发快照。
这与 docker-bake.hcl 中的标签逻辑一致:当VERSION为dev时,会生成sha-前缀的标签,而正式发布版本则按 semver 规则生成主/次/补丁标签。开发版本适合提前验证新功能,但不应直接用于生产环境。
小结
围绕dunglas/frankenphp官方镜像,本文覆盖了从开箱即用的docker run、标签体系解析、默认 Caddyfile 的环境变量注入,到install-php-extensions安装 PHP 扩展、xcaddy重新编译以加入 Caddy 模块、FRANKENPHP_CONFIG一键启用 worker、开发数据卷、非 root/capability 最小化、distroless 加固以及 dev 每日构建的完整链路。这些能力在仓库的 Dockerfile、alpine.Dockerfile、默认 Caddyfile 与 docker-bake.hcl 中均有对应的实现依据,你也可以进一步阅读 docs/config.md(容器内配置路径与 php.ini 布局)与 docs/worker.md(worker 模式细节)获得更深入的知识。
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考