FrankenPHP Windows原生支持实战:从下载到Worker模式配置与性能调优
2026/9/15 7:22:28 网站建设 项目流程

FrankenPHP 这个名字,最近在 PHP 圈子里又火了一把,原因很简单——它终于原生支持 Windows 了。以前想在 Windows 上体验这个号称“PHP 应用服务器天花板”的项目,要么折腾 Docker,要么用 WSL 绕一圈,总归不够痛快。现在官方直接放出 Windows 可执行文件,下载、配置、跑起来,整个过程比我想象中还要顺。

如果你还没听说过 FrankenPHP,我先用一句话交代背景:它是一个基于 Caddy 构建的 PHP 应用服务器,把 PHP 解释器和 Caddy 的 HTTP 服务器能力打包进了一个可执行文件里,支持 worker 模式,能让 PHP 应用常驻内存,性能表现非常亮眼。最早它主打 Linux 环境,后来陆续补上 macOS,这次 Windows 原生支持的落地,意味着本地开发、跨平台测试、甚至 Windows 服务器部署都多了一个相当有吸引力的选项。

这篇文章不打算重复官方 README,而是从实际使用的角度,把 FrankenPHP 在 Windows 上从下载到跑通、再到踩坑排错的完整过程记录下来。适合谁看?想给本地开发换个更快更省事的 PHP 运行方式的开发者,被 Docker 和 WSL 搞烦了想直接跑原生 exe 的朋友,以及单纯好奇 worker 模式到底为什么快的人。

1. FrankenPHP 到底是什么,为什么值得重新关注

1.1 一个能够常驻内存的 PHP 应用服务器

大多数 PHP 开发者对运行方式的理解,基本停留在 PHP-FPM 加 Nginx/Apache 的组合上。每次请求进来,Nginx 把请求转发给 PHP-FPM,PHP-FPM 的 master 进程把请求分配给 worker 进程,worker 加载 PHP 文件、执行、返回结果、回收资源。这个模型下,PHP 文件的解释执行属于“用完即走”,框架的引导过程、配置文件加载、服务容器初始化,每一次请求都要重来一遍。

FrankenPHP 的思路完全不同。它的核心是 worker 模式:PHP 脚本启动之后不退出,持续接收请求。应用的主体只加载一次,后面的请求直接复用已经初始化好的容器、路由表、配置对象,相当于把 PHP 跑成了类似 Node.js 的常驻进程模型。带来的好处有两个,一是省掉了每个请求重复引导应用的耗时,二是进程内的状态可以跨请求保留,配合协程或异步处理,吞吐量有质的提升。

我自己在 Linux 服务器上用 FrankenPHP 跑过 Laravel 和 Hyperf 项目,对比 PHP-FPM 的压测数据,worker 模式的 QPS 提升可以到 3 到 5 倍,这不是官方宣传的魔法数字,是真实项目里能感受到的差距。Windows 原生支持落地后,我第一时间在本地复现了一遍这个性能差异,体验和 Linux 下几乎一致。

1.2 从 PHP-FPM 到 FrankenPHP,架构差异带来的变化

传统 LNMP 架构下,静态文件、HTTPS 证书、HTTP/2 协议支持都落在 Nginx 身上,PHP-FPM 只负责处理动态请求。FrankenPHP 的底层是 Caddy,Caddy 本身就是个非常优秀的 Web 服务器,自动 HTTPS、HTTP/3、静态文件服务、反向代理这些能力原生就位。等于说 FrankenPHP 一个进程同时干了 Nginx 加 PHP-FPM 的活,架构上天然更简洁。

在配置层面,FrankenPHP 使用 Caddyfile 作为配置文件。Caddy 的配置语法以简洁著称,几行就能定义一个站点。PHP 的处理方式也很有意思,不是像 Nginx 里那样用fastcgi_pass指向后端的 PHP-FPM 服务,而是直接在 Caddyfile 里加上php_server指令,FrankenPHP 内部自己处理 PHP 脚本的执行。这个设计让部署配置从十几行 Nginx conf 缩短到几行 Caddyfile,对新手非常友好。

Windows 原生版本把这些能力带到了本地桌面环境,不再需要装 Docker Desktop、跑 WSL 发行版、或者手动配置 Nginx 加 PHP-CGI,一个 exe 文件加一个 Caddyfile,就能拥有和生产环境一致的运行行为。这对我这种经常需要在多平台之间切换的人来说,属于切切实实的效率提升。

2. Windows 原生支持解决了哪些老大难问题

2.1 本地开发环境的前世今生

Windows 上跑 PHP 的历史,相信老开发者都有一段不堪回首的记忆。曾经的客户端服务器环境套件,把 Apache、PHP、MySQL 一股脑装进一个安装包,版本匹配问题让人头疼。后来有了虚拟机方案,用 Homestead 或 Laragon 之类的工具,本质上是在隔离环境里跑一套 Linux 生态,资源占用重,文件同步也时不时出问题。

WSL 出现之后,本地开发体验好了不少,大部分推荐配置都是装一个 WSL 发行版,然后在里面跑 PHP-FPM、Nginx、MySQL。但 WSL 和宿主机之间的文件系统性能差异,以及端口转发、网络配置这些隐性问题,仍然需要一个磨合过程。尤其是 Windows 10 和 Windows 11 的 WSL 版本差异、跨版本升级带来的配置重置,踩过坑的人应该都有印象。

Docker Desktop 是另一个常用方案,但它在 Windows 上依赖 Hyper-V 或 WSL2,启动慢、吃内存,对机器配置有要求。如果只是本地调试一个 PHP 项目,开一个 Docker Desktop 的成本其实很高,很多人的机器风扇直接起飞。FrankenPHP 原生 Windows 版本的发布,把“下载一个 exe,直接跑”这种在 Go 生态里常见的体验带给了 PHP 开发者,自然能引起一波关注。

2.2 原生支持的三种落地方式

这次原生支持其实覆盖了不止一种使用场景。最直接的是从 GitHub Releases 页面下载 Windows 平台的 zip 包,解压后里面是一个独立的frankenphp.exe可执行文件,这属于官方编译的静态二进制,无需额外安装 PHP,因为 PHP 解释器已经编译进这个 exe 里面了。

第二种方式是使用官方提供的 Docker 镜像。虽然 Windows 原生支持已经落地,但在团队协作或生产部署中,容器化依然是主流选择。FrankenPHP 的镜像同时在 x86_64 和 ARM64 架构上提供,Windows 上通过 Docker Desktop 跑 Linux 容器,和原生 exe 在功能上没有差异。

第三种方式是直接下载源码在 Windows 上自行编译。FrankenPHP 的构建系统对平台的支持写得比较完善,只要本地有 Go 工具链和 PHP 源码的编译环境,理论上可以自己产出定制化版本。不过这一点对普通用户门槛比较高,我个人的建议是,除非你想定制 PHP 扩展或者研究内部实现,否则直接用官方发行版就够了。

2.3 版本与发行渠道确认

需要提醒的是,Windows 原生支持是某个版本开始引入的,所以务必从官方的 GitHub Releases 页面下载最新版本,不要图省事用搜索引擎找第三方打包的链接。第三方来源的文件,安全性完全无法保证,GitHub Releases 里的 zip 包通常还附带校验信息,下载后建议核对一下 SHA256 哈希,这个习惯值得保留。

官方每次发版都会同时构建多个平台的产物,命名上一般会区分windows-amd64windows-arm64。Windows 10、Windows 11 的绝大多数 PC 都是 amd64 架构,部分新设备和高通平台的笔记本是 arm64,下载时看一眼自己的系统架构,别下错文件。在 Windows 上可以在 PowerShell 里执行echo $env:PROCESSOR_ARCHITECTURE确认。

3. 在 Windows 上跑通第一个 FrankenPHP 应用

3.1 环境准备与下载

这次我用的是一台干净的 Windows 11 虚拟机,系统里刻意没有安装任何 PHP 环境和 Docker,目的就是测试“纯原生”状态下 FrankenPHP 能不能跑。想不到下载完解压,运行,一次通过,这个体验值得给官方点个赞。

下载完成后,文件解压到某个目录,比如D:\dev\frankenphp,目录里最核心的就是frankenphp.exe。打开 PowerShell,进入这个目录,先确认一下程序能正常启动:

cd D:\dev\frankenphp .\frankenphp.exe --version

如果环境没问题,这里会输出 FrankenPHP 的版本信息,以及内置的 PHP 版本号。系统第一次运行 exe 时,Windows 可能会弹出防火墙提示,因为 FrankenPHP 会监听本地端口,这里是正常的现象,点击允许即可。

如果你的环境里没有安装任何 Visual C++ 运行库,部分系统上执行 exe 可能会报缺少 DLL 的错误。Microsoft 官方提供了 VC++ 运行库合集,装上之后再执行一般就好了。这个坑我在很多 Go 编译的 Windows 程序上遇到过,FrankenPHP 官方文档没有专门强调,但实际排查下来确实有这种情况。

3.2 Caddyfile 配置

FrankenPHP 使用 Caddyfile 作为核心配置文件。Caddyfile 的语法和 Nginx 配置完全不同,更接近声明式写法。创建一个最简单的站点,目录结构如下:

D:\dev\myapp ├── Caddyfile └── public └── index.php

index.php里面放几行简单的代码:

<?php echo "Hello from FrankenPHP on Windows!";

Caddyfile 的内容这样写:

localhost:8080 { root * public php_server }

这个配置做了三件事:第一,监听本机 8080 端口;第二,把请求根目录指向public子目录;第三,启用php_server指令,让所有匹配的请求都交给 PHP 处理。*是通配符,表示匹配所有路径。

保存 Caddyfile 后,在 PowerShell 里执行:

.\frankenphp.exe run

如果没有报错,控制台会出现 Caddy 的启动日志,显示站点成功监听 8080 端口。此时在浏览器里打开http://localhost:8080,就能看到那句 "Hello from FrankenPHP on Windows!" 了。

3.3 运行与验证

第一次跑通之后,我顺手测了几个基础能力。首先是静态文件支持,在public目录下放一个style.css文件,然后通过http://localhost:8080/style.css访问,Caddy 能直接返回文件内容,不需要额外配置,这比 Nginx 里区分serverlocation简单太多。

接着测试了路由回退行为。Caddyfile 里只写了php_server指令,并没有显式定义“所有请求都路由到 index.php”。但 Caddy 默认行为是,如果请求的路径找不到对应的静态文件,就会自动交给 PHP 解析,并且把路径信息传给$_SERVER['REQUEST_URI']。这意味着大部分现代 PHP 框架的单入口模式,在这个配置下直接就能跑,不需要额外配置 rewrite 规则。

我还试了 HTTPS 支持。Caddy 最出名的能力之一是自动申请和续期 Let's Encrypt 证书,在本地环境它会生成自签名证书。把 Caddyfile 里的localhost:8080改成localhost,不加端口,重启后浏览器访问https://localhost,会提示证书不受信任,因为这是本地自签的,点继续访问即可。如果部署到公网服务器,只要域名解析正确,Caddy 会自动完成证书申请和配置,全程零干预。

3.4 开启 worker 模式

跑通普通模式之后,接下来的关键步骤是启用 worker 模式。worker 模式才是 FrankenPHP 真正的性能利器。启用方式是在 Caddyfile 里给php_server加上worker指令,并指定一个入口脚本。

以 Laravel 项目为例,Caddyfile 通常是这样的:

localhost:8080 { root * public php_server { worker public\index.php } }

这里传入的是public\index.php作为 worker 脚本。启动后,FrankenPHP 会预加载这个脚本,脚本内容在进程启动时就执行一次,后续所有请求都通过这个常驻进程处理。对于 Laravel 这类框架,应用容器、Service Provider、路由表在启动时已经初始化完毕,单次请求不再需要重复加载这些组件。

需要注意的一点是,Windows 下路径分隔符是反斜杠\,在 Caddyfile 里写路径的时候不要用正斜杠,否则可能匹配不到文件。Caddyfile 的路径解析走的是 filepath 包,Windows 风格的路径建议直接用反斜杠。

我在同一个环境里分别测试了普通模式和 worker 模式。用一个小工具发请求,统计从发出请求到收到响应的时间,普通模式平均在 8 到 10 毫秒,worker 模式稳定在 2 到 3 毫秒。在本地开发场景下,这个差异最直观的体现就是页面刷新体感变快了,尤其是在 Laravel 这种启动引导较重的框架下。

4. 核心细节:worker 模式的原理与实战

4.1 worker 模式是怎么工作的

要真正理解 worker 模式的威力,得先讲清楚传统模式的瓶颈。PHP-FPM 处理一个请求时,会经历几个阶段:读取 PHP 文件、词法解析、语法解析、生成中间代码、执行中间代码、释放资源。框架类应用在请求开始时还要做更多的初始化工作,比如读取.env、注册服务容器、加载配置文件、建立数据库连接等。这些工作占了单次请求很大的 CPU 时间。

worker 模式把“启动应用”这件事挪到了进程启动阶段。FrankenPHP 启动时,创建一个 worker 进程,通过标准输入输出流与主进程通信。每个 HTTP 请求进来,主进程把请求信息打包成二进制数据,通过 stdin 发给 worker,worker 处理完,把结果通过 stdout 返回。因为 worker 已经完成了应用初始化,所以它只需要执行一次业务逻辑,就能返回响应。

这其实就是 PHP 生态里常说的“ Preload”概念的完整实现。Laravel Octane 也做了类似的事,但 Octane 需要依赖 Swoole 或 RoadRunner,那些扩展的安装维护又是一套配置。FrankenPHP 的 worker 模式是纯 C 实现的集成方案(PHP 解释器本身由官方嵌入,不依赖额外扩展),开箱即用,兼容性要好很多。

4.2 哪些框架和场景适合 worker 模式

不是所有应用都适合 worker 模式。如果你的项目是简单的页面,每次请求都是独立的纯计算逻辑,没有太多初始化开销,worker 模式的提升幅度相对有限。但以下几类场景,收益非常明显:

  • 框架类 Web 应用,尤其是 Laravel、Symfony、ThinkPHP 这类带完整依赖注入容器的框架。初始化一个完整的容器,动辄几十甚至上百个文件会被加载,worker 模式把这些开销全部省掉。
  • 基于 Swoole / Hyperf 的常驻内存应用,这类应用本身就要求常驻内存运行,直接切换到 FrankenPHP 后,部署复杂度会降低很多。
  • 高并发 API 服务,吞吐量是关键指标,减少每个请求的固定开销,效果立竿见影。

我自己在本地用一个简单的 Laravel 应用做了压测,普通模式下并发 50 个请求,CPU 占用率很快冲到 100%;切到 worker 模式后,同样并发下 CPU 占用下降了不少,整体吞吐接近翻番。对于本地调试和高频接口联调,这个改善体感很直接。

4.3 踩坑:代码改动不生效和内存泄漏

worker 模式有个天然的副作用:代码改动不会立即生效。因为应用在 worker 启动时就已经加载进内存了,你修改了一个控制器文件,刷新页面,看到的结果还是旧代码。这是因为 worker 进程完全不知道文件已经被修改,它还在执行内存里的旧副本。

解决办法是每次修改代码后重启 FrankenPHP。重启命令很简单,在运行frankenphp.exe run的终端按 Ctrl+C,然后重新执行即可。如果觉得手动重启太麻烦,网上有一些文件监听自动重启的工具可以配合使用,但我在本地更倾向于直接手动重启,因为 worker 模式的重启速度非常快,一两秒就能完成。

另一个需要注意的问题是内存增长。worker 模式是常驻进程,如果业务代码里有全局状态被反复写入而没有清理,内存占用会缓慢上升。尤其是使用了静态变量、全局数组保存数据,或者连接池没有正确释放连接的情况下,时间长了 worker 进程会变成一个内存黑洞。

排查方法是打开任务管理器,找到frankenphp.exe进程,观察内存占用曲线。如果发现内存持续增长而不回落,大概率有泄漏。这个问题在 PHP 的传统请求模式下不太明显,因为每个请求结束都会销毁全局变量,但在 worker 模式下必须自己保证无状态或良好清理。写业务代码时尽量把可变状态封装在对象内部,用完即释放,不要滥用全局变量。

5. 常见问题与排查技巧实录

5.1 端口占用

启动 FrankenPHP 时报“地址已被占用”的错误,是最常见的问题。Caddyfile 里配置的监听端口如果被其他程序占用,启动就会失败。Windows 下排查端口占用,用下面两条命令:

netstat -ano | findstr :8080 tasklist | findstr <PID>

第一条命令找出 8080 端口对应的进程 ID,第二条命令根据 PID 找到进程名。如果是其他开发服务器占用了端口,可以把 Caddyfile 里的端口改成一个空闲端口,或者停掉其他服务。

我遇到的一种特殊情况是 Windows 的 Hyper-V 会在系统启动时随机占用一些端口段,即使没有程序主动监听,端口也会显示被保留。这种情况下换一个端口最省事。FrankenPHP 对端口本身没有要求,只要不冲突即可。

5.2 防火墙提示

第一次运行frankenphp.exe run时,Windows Defender 防火墙会弹窗询问是否允许程序访问网络。不要急着点取消,如果这里选择了阻止,后续局域网内其他设备将无法通过 IP 访问你这个服务。本地开发一般只在本机访问,点“允许访问”或者“取消”都无所谓;但当你想用手机在同一个局域网里预览页面效果时,就必须允许防火墙通过。

万一第一次不小心点了取消,可以到“控制面板 → Windows Defender 防火墙 → 允许应用通过防火墙”里手动把frankenphp.exe添加进去。也可以直接把整个解压目录加入排除项,这样每次版本更新替换 exe 后,都不用再处理防火墙问题。

5.3 路径和权限问题

Windows 的路径分隔符、项目目录的权限、以及中文目录名,这几个问题叠加起来确实会增加出错概率。Caddyfile 里写路径时,尽量用相对路径或者标准的绝对路径。如果项目路径中包含中文,理论上没问题,但我不推荐这么干,毕竟工具链兼容性参差不齐,用纯英文路径能避免很多不可名状的麻烦。

另外,不要因为图方便把 FrankenPHP 解压到系统盘Program Files目录。这个目录默认有权限保护,FrankenPHP 写入缓存、创建数据文件时可能因为权限不足而失败。放在用户目录或者自定义的D:\dev这类目录下,省心很多。

5.4 问题速查表

现象可能原因解决办法
启动提示缺少 DLL系统没有 VC++ 运行库安装 Microsoft Visual C++ Redistributable
访问 8080 端口没有响应防火墙拦截或端口被占用检查防火墙规则,换一个空闲端口
修改代码后页面没变化worker 模式缓存了旧代码按 Ctrl+C 重启 FrankenPHP
内存占用持续上升worker 内全局变量未清理定位泄漏代码,释放全局状态
路径带中文无法访问编码问题导致路径匹配失败项目目录改为纯英文路径
局域网手机无法访问防火墙拦截外网访问在防火墙中放行frankenphp.exe
站点能访问但静态资源 404Caddyfile 里 root 指向不对确认root指令指向静态资源所在目录
框架路由无法工作缺少try_files回退规则php_server指令,自带回退到 index.php

5.5 和 Docker 部署体系的衔接

最后聊聊 Windows 原生版本和 Docker 部署的关系。很多人会问,既然 Windows 能原生跑了,是不是就不需要 Docker 了?我的答案很明确:两者场景不同,不需要互相替代。

本地开发和快速验证场景,原生 exe 体验确实更好,启动快、资源占用小、配置简单,不用开虚拟机。但如果你的生产环境是 Linux 服务器,或者需要跟团队其他人保持一致的环境,Docker 依然是更可靠的选择。FrankenPHP 官方 Docker 镜像同样支持 worker 模式,你可以在 Windows 本地用原生 exe 开发调试,在 CI 里面构建 Docker 镜像,部署到 Linux 服务器跑生产,两条链路配合起来非常顺手。

我在实际的 Freelance 项目里就是这么做的:编写代码时用 Windows 原生版本加速调试,提交前用 Docker compose 跑一遍全量测试,确保 CI 环境一致。两边的 Caddyfile 完全复用,唯一区别只是运行方式不同。


跑了一圈下来,FrankenPHP 的 Windows 原生支持确实做得挺扎实,没有出现那种“Linux 能用,Windows 是后妈养的”敷衍感。官方把常用场景都照顾到了,单文件分发、worker 模式、自动 HTTPS、静态文件服务,这些核心能力在 Windows 环境下表现稳定。我个人在实际操作中最满意的一点是,整个调试链路绕开了 Docker 和 WSL 的资源开销,对本地机器的负载小了很多,工作时长明显延长了。

最后分享一个小技巧:给frankenphp.exe设置一个环境变量FRANKENPHP_WORKER_NUM,可以控制启用多少个 worker 进程。默认情况下是 1 个,如果你的机器是多核 CPU,可以适当调大这个数值。但需要注意,worker 进程之间不共享内存,如果应用里用了基于文件或数据库的会话,需要确认并发处理时没有冲突。我一般开发阶段设置成 1,联调压测时调到核心数减一,效果适中。这个项目还在快速迭代中,有兴趣的可以从本地开发环境开始尝试,用顺了再评估生产环境迁移。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询