FrankenPHP 开发指南:源码编译、测试、Docker 镜像构建与段错误调试全流程
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
本篇指南基于 FrankenPHP 仓库的官方贡献者文档(docs/es/CONTRIBUTING.md)整理而成,面向希望为 FrankenPHP 贡献代码、修复 Bug 或自行构建定制版本的开发者。你将掌握:如何用 Docker 或本机源码编译 PHP 与 FrankenPHP、如何运行测试套件、如何构建带 Caddy 模块的服务器与最小测试服务器、如何用docker buildx bake构建镜像,以及如何在本地与 GitHub Actions 中借助 GDB 定位 C 与 CGO 层的段错误(Segmentation Fault)。文中所有命令均来自仓库文档,并结合 dev.Dockerfile、docker-bake.hcl、go.sh、internal/testserver/main.go 等源码做了解读。
一、编译 PHP:使用 Docker 开发镜像(Linux)
FrankenPHP 的核心是把 PHP 以嵌入式(embed)方式编译进 Go 程序,因此开发环境首先需要一份带调试符号的 PHP。仓库提供了开箱即用的开发镜像定义 dev.Dockerfile,在 Linux 上可直接构建并进入容器:
docker build -t frankenphp-dev -f dev.Dockerfile . docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 443:443 -p 443:443/udp -v $PWD:/go/src/app -it frankenphp-dev参数说明:
--cap-add=SYS_PTRACE:允许在容器内对进程执行ptrace,这是 GDB 附加进程调试的前提;--security-opt seccomp=unconfined:关闭 seccomp 限制,避免调试器与部分系统调用被拦截;-p 8080:8080 -p 443:443 -p 443:443/udp:映射 HTTP、HTTPS 及 HTTP/3(UDP 443)端口;-v $PWD:/go/src/app:把仓库源码挂载进容器(对应 Dockerfile 中的工作目录/go/src/app),实现容器内外代码同步。
容器内的 PHP 配置与扩展位置
从 dev.Dockerfile 第 56-68 行可以看出,容器内 PHP 使用以下固定的配置布局:
- php.ini 主配置:
/etc/frankenphp/php.ini,默认会提供一份带开发预设的 php.ini(php.ini-development),并追加zend_extension=opcache.so与opcache.enable=1; - 附加配置文件:
/etc/frankenphp/php.d/*.ini(对应--with-config-file-scan-dir); - PHP 扩展目录:
/usr/lib/frankenphp/modules/(对应EXTENSION_DIR环境变量与--with-config-file-path组合配置)。
dev.Dockerfile 源码要点
该镜像基于golang:1.26(第 4 行),并做了以下关键设置:
- 从 PHP 官方仓库克隆
PHP-8.5分支源码(第 53 行); --enable-zts:启用 Zend Thread Safety,这是 FrankenPHP 多线程处理并发请求的基础;--enable-debug与全局CFLAGS="-ggdb3"(第 7 行):编译出带完整调试信息的 PHP,供 GDB/Valgrind 使用;- 同时安装了
gdb、valgrind、neovim、clang、cmake、llvm等开发工具,并配置了 GDB 的auto-load safe-path与不限 core dump 大小; - 额外从
e-dant/watcher源码编译安装文件监听库libwatcher-c.so(第 73-80 行),这是 FrankenPHP 热重载功能所依赖的组件; - 最终通过
../../go.sh build在镜像内直接完成一次 FrankenPHP 构建,保证环境自洽。
Docker 版本低于 23.0 的注意事项
如果 Docker 版本低于 23.0,构建会因.dockerignore的模式匹配问题 只排除了编译产物与日志类文件,需要在其中补上源码目录的例外规则:
!testdata/*.php !testdata/*.txt +!caddy +!internal即把caddy与internal目录从忽略规则中显式放行,确保它们能被复制进构建上下文。
二、不使用 Docker 编译(Linux 与 macOS)
如果不想依赖 Docker,可以按照从源码编译的完整步骤操作,关键是在 PHP 的configure阶段传入--debug标志,以获得带调试符号的构建。此外,仓库根目录的 go.sh 封装了编译 FrankenPHP 所需的 Go 与 cgo 环境变量:
PHP_CONFIG=${PHP_CONFIG:-php-config} GOFLAGS="$GOFLAGS -tags=nobadger,nomysql,nopgx" \ CGO_CFLAGS="$CGO_CFLAGS $(${PHP_CONFIG} --includes) $(sh "$(dirname "$0")/mtls-cflags.sh")" \ CGO_LDFLAGS="$CGO_LDFLAGS $(${PHP_CONFIG} --ldflags) $(${PHP_CONFIG} --libs)" \ go "$@"它通过php-config自动注入 PHP 的头文件路径(--includes)与链接参数(--ldflags/--libs),并默认携带nobadger,nomysql,nopgx构建标签。这也是理解后续所有构建命令的背景:FrankenPHP 通过 cgo 直接嵌入 PHP,编译时必须让 Go 工具链找到 PHP 的头文件与库文件。
三、运行测试套件
编译出带调试符号的 PHP 后,可在仓库根目录运行完整测试:
go test -tags watcher -race -v ./...参数解读:
-tags watcher:启用文件监听相关代码(热重载功能依赖),对应 caddy/frankenphp/hotreload.go 等源文件中的构建标签;-race:开启 Go 的竞态检测器(Race Detector),用于捕捉多线程访问共享状态的隐患——对 FrankenPHP 这种多线程调度 PHP 请求的应用尤为重要;-v:输出每个测试用例的详细结果。
仓库中frankenphp_test.go、caddy_test.go、worker_test.go、scaling_test.go等测试文件覆盖了从请求处理、Worker 模式到并发伸缩的各类场景,运行前请确保当前环境已具备php-config指向的 PHP 安装(Windows 下的测试指引见根目录 CONTRIBUTING.md)。
四、构建并运行 Caddy 模块
构建带 FrankenPHP 模块的 Caddy
cd caddy/frankenphp/ go build -tags watcher,brotli,nobadger,nomysql,nopgx cd ../../构建标签说明(结合 caddy/frankenphp 目录源码结构可推断):
watcher:启用文件监听与热重载;brotli:启用 Brotli 压缩编码支持(对应 caddy/frankenphp/br.go);nobadger、nomysql、nopgx:禁用 Caddy 的 Badger/MySQL/PostgreSQL 存储模块,减小二进制体积。
生成的可执行文件位于caddy/frankenphp/frankenphp。从 caddy/frankenphp/main.go 可以看到,该入口通过空导入注册了四类模块:Caddy 标准模块、FrankenPHP 的 Caddy 模块(github.com/dunglas/frankenphp/caddy)、Mercure 与 Vulcain 模块。
运行与验证
cd testdata/ ../caddy/frankenphp/frankenphp run此时 Caddy 会读取 testdata/Caddyfile 启动,站点块为http://(未指定域名),因此默认监听127.0.0.1:80。该 Caddyfile 中开启了debug日志、frankenphp指令(注释掉的worker配置说明如何切换 Worker 模式)、encode zstd br gzip压缩,以及把*.php请求路由给php处理器的规则。
curl -vk http://127.0.0.1/phpinfo.php-k用于跳过证书校验(首次启动会自动生成自签名证书),-v输出详细请求头。测试目录下的 phpinfo.php 会输出完整的 PHP 环境信息,可据此确认嵌入的 PHP 版本、扩展与配置。
[!NOTE] 如果使用 Docker,需要绑定容器的 80 端口,或直接在容器内部执行上述命令。
五、最小测试服务器
除了完整的 Caddy 集成,仓库还提供一个极简的独立测试服务器,用于隔离验证 FrankenPHP 的 Go 库本身。源码位于 internal/testserver/main.go,核心逻辑只有几十行:
if err := frankenphp.Init(frankenphp.WithContext(ctx), frankenphp.WithLogger(logger)); err != nil { panic(err) } defer frankenphp.Shutdown() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { req, err := frankenphp.NewRequestWithContext(r) // ... if err := frankenphp.ServeHTTP(w, req); err != nil { panic(err) } })它演示了在任意 Go 程序中嵌入 FrankenPHP 的最小三步:frankenphp.Init初始化运行时、NewRequestWithContext包装标准net/http请求、ServeHTTP交给 PHP 执行;监听端口由环境变量PORT控制,默认8080。构建并运行:
cd internal/testserver/ go build cd ../../cd testdata/ ../internal/testserver/testserver该服务器监听127.0.0.1:8080,验证方式与 Caddy 模块类似:
curl -v http://127.0.0.1:8080/phpinfo.php这个最小服务器非常适合在编写新测试或排查请求处理问题时快速复现,也是理解 frankenphp.go 中Init、ServeHTTP等公开 API 的最佳入口。
六、本地构建 Docker 镜像
镜像构建统一由 Docker Buildx Bake 驱动,构建配方定义在 docker-bake.hcl。
先查看构建计划
docker buildx bake -f docker-bake.hcl --print该命令输出完整的构建矩阵。从 docker-bake.hcl 可以看到default目标按操作系统 × PHP 版本 × 目标阶段组合展开:操作系统为trixie、bookworm、alpine,PHP 版本默认覆盖8.2,8.3,8.4,8.5,每个组合又分为builder与runner两个阶段;标签规则、semver 解析与基础镜像指纹标签都在此文件中定义。
构建指定平台镜像
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/amd64"--pull:拉取最新的基础镜像;--load:把构建结果载入本地 Docker 守护进程(否则产物只存在于 Buildx 缓存中);--set "*.platform=...":覆盖所有目标的平台为 amd64。
arm64 同理:
docker buildx bake -f docker-bake.hcl --pull --load --set "*.platform=linux/arm64"全量构建并推送
docker buildx bake -f docker-bake.hcl --pull --no-cache --push--no-cache从零开始构建(用于验证构建链路的可复现性),--push会把 amd64 与 arm64 等多平台镜像一并推送到 Docker Hub。除常规镜像外,配方还定义了static-builder-musl与static-builder-gnu两个静态构建目标(分别对应 static-builder-musl.Dockerfile 与 static-builder-gnu.Dockerfile),用于产出可独立分发、不依赖系统动态库的 FrankenPHP 二进制。
七、静态构建下调试段错误
段错误通常发生在 C 代码或 cgo 边界(如 PHP 内核、扩展或 FrankenPHP 的 C 胶水层 frankenphp.c),用 GDB 附加运行中的进程是定位这类问题的主要手段。官方给出了完整的八步流程:
1. 准备带调试符号的静态二进制
从发布页下载调试版二进制,或自行构建包含调试符号的静态版本:
docker buildx bake \ --load \ --set static-builder.args.DEBUG_SYMBOLS=1 \ --set "static-builder.platform=linux/amd64" \ static-builder docker cp $(docker create --name static-builder-musl dunglas/frankenphp:static-builder-musl):/go/src/app/dist/frankenphp-linux-$(uname -m) frankenphpDEBUG_SYMBOLS=1让静态构建保留调试信息;第二条命令从static-builder-musl镜像中把对应架构(uname -m)的产物复制到当前目录。
2. 替换二进制:用调试版frankenphp替换当前使用的版本(注意先备份原文件)。
3. 正常启动:按常规方式启动 FrankenPHP;也可以直接用 GDB 启动:
gdb --args frankenphp run4. 附加进程:
gdb -p `pidof frankenphp`pidof取得进程 PID,-p让 GDB 附加到已运行进程。结合第一步--cap-add=SYS_PTRACE的用意,此时在容器内同样可以附加。
5. 让进程继续运行:必要时在 GDB shell 中输入continue。
6. 触发崩溃:发起导致段错误的请求,使进程崩溃。
7. 抓取调用栈:在 GDB shell 中输入bt(backtrace),输出完整的 C/C++/Go 混合调用栈。
8. 记录结果:复制bt的输出,作为 Issue 或修复的依据。
八、在 GitHub Actions 中调试段错误
如果段错误只在 CI 环境复现,官方提供了借助 tmate 交互式调试的方案,核心步骤:
1. 打开 CI 工作流.github/workflows/tests.yml。
2. 开启 PHP 调试符号:在shivammathur/setup-php步骤的env中追加debug: true:
- uses: shivammathur/setup-php@v2 # ... env: phpts: ts + debug: true3. 启用 tmate 并安装 GDB:在设置 CGO 标志的步骤后追加:
- name: Set CGO flags run: echo "CGO_CFLAGS=$(php-config --includes)" >> "$GITHUB_ENV" + - run: | + sudo apt install gdb + mkdir -p /home/runner/.config/gdb/ + printf "set auto-load safe-path /\nhandle SIG34 nostop noprint pass" > /home/runner/.config/gdb/gdbinit + - uses: mxschmitt/action-tmate@v3GDB 配置中的两行很关键:set auto-load safe-path /允许加载任意目录下的调试脚本;handle SIG34 nostop noprint pass让 GDB 忽略信号 34(Zend VM 的定时器信号),避免调试被频繁打断。
4. 连接容器:tmate 步骤运行后会打印 SSH 连接信息,通过它进入 CI 容器。
5. 启用cgosymbolizer:打开 frankenphp.go,其第 40 行正有一行被注释掉的导入:
- //_ "github.com/ianlancetaylor/cgosymbolizer" + _ "github.com/ianlancetaylor/cgosymbolizer"该模块能让 Go 的 panic 栈与 GDB 正确解析 cgo 中的 C 符号,是调试 CGO 层崩溃的关键开关。
6. 下载模块:执行go get拉取刚启用的依赖。
7. 编译并调试测试:在容器内可以编译带调试信息的测试二进制并用 GDB 运行:
go test -tags watcher -c -ldflags=-w gdb --args frankenphp.test -test.run ^MyTest$-c只编译不运行,产出frankenphp.test;-ldflags=-w去掉 DWARF 调试表(保留符号表即可满足 GDB 回溯)。随后即可用bt等方式定位崩溃点。
8. 修复并还原:Bug 修复后,务必把上述所有临时改动(debug: true、tmate 步骤、cgosymbolizer 导入等)全部还原,再提交正式代码。
九、有用的调试命令与参考资料
调试进程级问题时,官方还推荐用strace跟踪系统调用(例如在容器内观察 PID 1 的行为,排除 futex、epoll 等高频噪声):
apk add strace util-linux gdb strace -e 'trace=!futex,epoll_ctl,epoll_pwait,tgkill,rt_sigreturn' -p 1-e 'trace=!...'表示排除所列系统调用,只输出与业务相关的调用轨迹。
相关实现参考
理解"如何在宿主语言中嵌入 PHP"这一课题时,官方文档推荐对照以下开源实现:
- PHP 在 uWSGI 中的嵌入插件(php_plugin.c);
- PHP 在 NGINX Unit 中的嵌入(nxt_php_sapi.c);
- Go 语言嵌入 PHP 的两个先例:go-php 与 GoEmPHP;
- C++ 中嵌入 PHP 的示例;
- Sara Golemon 的《Extending and Embedding PHP》及其关于 TSRMLS_CC 的经典博客文章(TSRMLS 是 Zend 线程安全资源管理宏);
- macOS 上嵌入 PHP 的实践示例;
- Go 的 SDL 绑定(其
sdl.Main与 FrankenPHP 的"主线程"调度思路可类比)。
Docker 相关参考
- Docker Bake 的文件定义(Bake file definition)文档;
docker buildx build命令参考。
深入架构
若想进一步了解 FrankenPHP 的线程模型、状态机与 CGO 边界等内部机制,可阅读仓库的 docs/internals.md。
十、翻译文档的贡献流程
除代码贡献外,官方还欢迎文档翻译。贡献者文档(如本指南所在的 docs/es/ 目录)本身就是这套流程的产物,具体步骤:
- 在仓库的
docs/目录下,用语言的两位 ISO 代码新建目录(如es、fr、ja); - 将
docs/根目录下的全部.md文件复制到新目录(翻译始终以英文原版为源,因为它始终最新); - 同时把仓库根目录的
README.md与CONTRIBUTING.md复制到新目录; - 翻译文件内容,但不要改动文件名,也不要翻译以
> [!开头的字符串——那是 GitHub 的特殊标记语法(如> [!NOTE]); - 以 Pull Request 提交翻译;
- 在官方站点仓库中,同步翻译
content/、data/与i18n/目录下的翻译文件; - 翻译新建的 YAML 文件中的键值;
- 在站点仓库提交 Pull Request。
遵循此流程,可以保证各语言文档与英文版保持同步,同时不破坏文件间的相对链接关系。
小结
从 Docker 开发镜像编译 PHP,到运行测试套件、构建 Caddy 模块与最小测试服务器,再到docker buildx bake产出多架构镜像,最后通过 GDB、tmate 与cgosymbolizer定位 cgo 层的段错误——这套流程覆盖了 FrankenPHP 贡献者日常最常遇到的构建与调试场景。所有命令均可直接在仓库中执行验证:配置文件见 dev.Dockerfile 与 docker-bake.hcl,构建辅助脚本见 go.sh,测试入口与 Caddyfile 见 internal/testserver/main.go 与 testdata/Caddyfile。以本指南为起点,你就能独立完成 FrankenPHP 的本地开发闭环,并参与到它的代码与文档贡献中。
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考