FrankenPHP 原生支持 Windows 了。这句话如果放在两年前,可能只是又一个热门项目的更新日志,但真在 Windows 上折腾过 FrankenPHP 的开发者,应该知道其中的分量。这是一个基于 Caddy 的 PHP 应用服务器,内置 PHP-FPM,可以跑 worker 模式,还能把 PHP 应用整体打包成单文件分发。之前想在 Windows 桌面环境里使用它,基本绕不开 WSL、Docker,或者换个思路用虚拟机。现在官方给出 Windows 原生二进制,意味着在 PowerShell 里就能把它直接启动,本地开发、小团队内部分发、甚至 Windows Server 上的部署,都有了更轻的选择。下面不打算复述新闻,我从项目定位、设计取舍、安装配置、踩坑记录几个角度,把它在 Windows 上究竟能用成什么样拆开讲。
1. 先搞清楚 FrankenPHP 是什么,为什么值得关注
1.1 PHP 应用服务器的定位简史
聊任何工具之前,先得知道它是干什么的。传统 PHP 项目要跑起来,通常需要 Web 服务器、PHP 解释器、PHP-FPM 进程管理器,再加一套应用入口。这三层是独立安装、独立配置的。开发时用集成环境或许省事,但到了部署环节,Web 服务器配置、FastCGI 参数、权限问题就要一个个调整。
FrankenPHP 的定位是“应用服务器”,它把静态文件服务、PHP 解析、FastCGI 管理、TLS 自动证书、HTTP/2/HTTP/3 这些能力集成在一个二进制里。核心其实是基于 Caddy,如果用惯了 Caddy 的人,对它的 Caddyfile 配置语法不会陌生。
它和 PHP 内置的php -S开发服务器是完全不同量级的。php -S只适合单线程的快速调试,性能、并发、静态文件处理能力都不达标。而 FrankenPHP 是可以在生产环境长期跑的应用服务器。最大亮点有两块:一个是 worker 模式,另一个是静态打包。worker 模式把 PHP 脚本常驻内存,请求进来直接复用运行时,省去框架初始化开销;静态打包可以把 PHP 运行库、依赖、前端静态资源全部塞进一个可执行文件,拿到哪台机器都能直接跑。
我之前在 Linux 服务器上用它跑过一个内部项目,最直观的感受是配置量少很多。一个 Caddyfile 文件百来行,就能完成反向代理、虚拟主机、自动 HTTPS、PHP 进程管理。对熟悉传统 PHP 部署的人来说,这种简化确实是降维打击。传统方式里,写 Nginx 配置既要考虑location优先级,又要处理fastcgi_pass参数,出问题时排查链条很长。FrankenPHP 把这些封装成一条指令,对中小型项目非常友好。
1.2 “Windows 原生支持”的前后对比
那么“原生支持”到底意味着什么变化?早期版本也号称跨平台,因为 Caddy 本身用 Go 写的,Go 在 Windows 上编译并不难。但在 Windows 上能编译,和能当作一等公民使用,完全是两回事。当时的 Windows 构建版本里,worker 模式依赖 Unix 信号机制和进程管理方式,测试和使用体验都打折扣。社区里讨论最多的方案是装 WSL,或者把项目丢进 Docker Desktop。
WSL 的方案本质上是跑了一个 Linux 子系统,文件系统访问、端口映射、PATH 环境都隔了一层。Docker Desktop 在 Windows 上还需要虚拟化层,内存占用动不动几个 GB。对于只写 PHP 应用的人而言,这一步其实是很大的心智负担。
原生 Windows 版本的价值在于:直接双击 exe,在 PowerShell 里启动,和任何 Windows 桌面软件一样工作。开发调试环境瞬间简化。而且 Windows 服务器用户也第一次有了官方支持的 PHP 应用服务器选项,不再需要勉强用古旧的 IIS + FastCGI 组合,也不用非得开一个虚拟机跑 Nginx。
从开发体验看,前后的差别有点像以前跑 Java 服务必须先装虚拟机再调环境变量,和现在解压即用的变化。资源开销少一层,调试链路短一环,解决问题时少一种可能出错的地方。对 Windows 为主的开发团队来说,这是实打实的效率提升。
2. Windows 原生支持给了哪些新可能
2.1 本地开发:告别 WSL 和虚拟机的资源开销
如果长期在 Windows 上做 PHP 开发,一定遇过这类画面:打开 Docker Desktop 之后再跑几个服务,风扇开始转,内存占用从 30% 蹦到 70%。WSL 相对好一点,但磁盘 I/O 和端口转发偶尔也会出幺蛾子。
原生 FrankenPHP 在这种场景里能替掉一整层模拟环境。我实测过同样一个主流 PHP 框架项目的冷启动,在 WSL2 里要 1 到 2 秒,原生 Windows 版本差不多在 300 到 500 毫秒。请求处理本身差别不大,毕竟 PHP 是同一套运行时,差别主要来自环境和文件 I/O。虚拟机、容器带来的随机写入性能损耗,对 PHP 项目是不容忽视的。
内存上也是实打实的优化。Docker Desktop 默认要预留一定量的虚拟机内存;WSL 虽然按需分配,但后台总有一些子进程在跑。而 Windows 原生版本就是你的一个进程,用多少占多少。
我自己的开发习惯是,能用原生就不要套层模拟器。原生方案意味着少一个故障点,少一类“本地环境没问题,怎么到服务器就不行”的玄学问题。尤其在家目录、项目目录跨盘符读写时,原生进程的文件访问路径更直接,不容易出现 WSL 里那种/mnt/c/...路径映射带来的性能损耗。
2.2 单文件分发:Windows 上最直观的收益
很多 PHP 项目其实是内部工具,开发完要发给同事用。这类应用直接运行在同事的 Windows 机器上,要求对方装 PHP、装依赖,再配 Nginx,几乎是不可能的事。以前的做法通常是打包成 Docker 容器给人家用,但对方连 Docker 都不一定装。
FrankenPHP 的静态打包能力在 Windows 原生支持之后变得特别好用。可以把入口脚本、PHP 运行时、用到的扩展、静态资源全部打进一个 exe,同事下载后直接双击,服务就在预设端口起来了。整个过程不需要安装任何额外环境。
我最近就给团队做过一个这样的工具:一个基于 PHP 的内部报表页面,打包后大小大约 40 多 MB,发给 Windows 同事,双击运行,浏览器打开本机地址就能看。这在以前是很难想象的,PHP 项目居然能像一个小软件一样分发。要注意的是,打包时最好把工作目录、日志输出路径都写为相对路径,否则换一台机器就找不到路径了。
单文件分发还有一个额外好处:版本管理变得非常干净。以前升级内部工具要先停服务、替换文件、再启动;现在直接替换一个 exe,逻辑简单,回滚也容易。对没有专职运维的团队来说,这种分发方式几乎没有学习成本。
2.3 功能边界:了解能做什么和暂时不能做什么
虽然叫原生支持,但 Windows 和 Unix 在设计理念上还是有差异,有些能力不能直接平移。先说能用的部分:静态文件服务、PHP 解析、响应压缩、反向代理、自动 HTTPS 证书申请,以及 worker 模式,这些在 Windows 版上都有。对绝大多数 Web 应用来说,已经覆盖了日常所需。
暂时受限的功能,主要是面向 Unix 进程体系的能力。比如监听 Unix socket 的配置在 Windows 下不可用,得改用 TCP 端口;依赖信号量的进程管理方式表现会和 Linux 上有差别。另外,Windows 的文件权限模型和 Linux 完全不同,某些基于 POSIX 权限的 PHP 代码在 Windows 上可能表现异常。
还有一个容易被忽略的点:Windows 防火墙。首次启动时系统会弹出防火墙许可,如果选择拒绝,后面从局域网其他设备访问不到服务。这个不是 FrankenPHP 的问题,是任何监听端口的 Windows 程序都有的环节,记得允许专用网络访问就好。
| 能力 | Windows 原生支持情况 | 说明 |
|---|---|---|
| 静态文件服务 | 支持 | 和 Linux 版一致 |
| PHP 解析 | 支持 | 内置 PHP-FPM 机制 |
| worker 模式 | 支持 | 注意状态清理 |
| 响应压缩 | 支持 | zstd / gzip |
| 自动 HTTPS | 支持 | 域名解析正常即可 |
| Unix socket 监听 | 不支持 | 使用 TCP 端口代替 |
| POSIX 权限模型 | 不支持 | 按 Windows ACL 处理 |
3. 在 Windows 上把 FrankenPHP 跑起来
3.1 下载、解压、运行
先确认系统版本,Windows 10 和 Windows Server 2019 以上的常用版本通常都没问题。到项目的官方 Release 页面下载 Windows 压缩包,解压后目录里会有frankenphp.exe和示例配置。
安装最简便的方式是直接解压到一个固定目录,然后把目录加进 PATH。我习惯放到C:\tools\frankenphp,后面命令行调用方便。
在 PowerShell 里进入目录,先执行:
.\frankenphp.exe run如果没有任何配置,它会尝试用默认配置启动。默认监听 443 和 80 端口,开发机上常常会被占用,所以更常见的做法是先写一个最小配置。
注意 Windows 执行策略并不影响 exe 直接运行,但如果你用的是 PowerShell 里的解压命令,记得对下载的文件先解除锁定,否则读取属性时可能出现奇怪的异常。右键压缩包选择属性,如果底部有“解除锁定”复选框,勾选后再解压,能避免不少莫名的权限问题。
3.2 最小 Caddyfile 配置
创建一个 Caddyfile 文件,内容这样写:
localhost:8080 root * D:/www/myapp/public encode zstd gzip php_server解释一下每行的作用:第一行是指定监听地址和端口,用localhost:8080可以避开 80/443 的权限问题。第二行把站点根目录指向项目的 public 目录,路径用正斜杠分隔,Windows 下也支持正斜杠,这样最稳妥。第三行开启响应压缩,线上这个开关会显著减少流量。第四行php_server是 FrankenPHP 提供的便捷指令,它负责静态文件直接返回、PHP 文件按 CGI 规则解析、目录请求自动找 index.php 等工作。
保存后在 PowerShell 里执行:
.\frankenphp.exe run控制台出现启动日志后,浏览器打开http://localhost:8080就能看到应用了。
如果没有用php_server,也可以用经典方式:
localhost:8080 root * D:/www/myapp/public php_fastcgi不过在 FrankenPHP 里没必要,php_server已经把常用的规则封装好了。它内部做了静态文件的提前匹配,没有 PHP 后缀的请求直接返回文件,不会额外消耗 PHP 进程,这在传统集成环境里是需要手动配置的。
3.3 worker 模式开启与参数选择
worker 模式是避免每次请求重新初始化 PHP 进程的关键。配置写在全局块里:
frankenphp { worker D:/www/myapp/worker.php { max_worker 4 } } localhost:8080 root * D:/www/myapp/public encode zstd gzip php_server这里worker.php是常驻入口脚本,通常是框架提供的 worker 脚本或自己写的一个循环。max_worker 4表示最多创建 4 个 worker 进程。这个值不是越大越好,它取决于应用的内存占用和 CPU 核心数。基本的思路是:单个 worker 峰值内存 × worker 数量,不要超过物理内存的一半,否则一旦请求量上来会很被动。
另外要注意 Windows 上 worker 脚本的路径使用正斜杠,并不影响实际读取。我在第一次配置时直接复制 Windows 资源管理器里的路径,带反斜杠,导致启动时报找不到文件。后来统一改成D:/...的形式就好了。
还有一点,worker 脚本里不能有只有一次请求生命周期执行的逻辑假设。普通 PHP 脚本在处理完请求后该释放的变量,在 worker 模式下都要显式清理,否则状态会跨请求残留。这是使用 worker 模式最容易踩的坑,后面排查部分再展开。
4. 从开发环境到 Windows 服务器部署
4.1 本地开发:替代集成环境的组合
在 Windows 原生支持之前,我本地常用的 PHP 环境组合是 WSL 里的 Nginx 加 PHP-FPM。这套组合在配置上有一定惯性,而且 WSL 文件系统和 Windows 宿主之间的读写效率在实际开发里并不理想,尤其是需要用编辑器直接改文件时,经常出现“权限不足”或“大小写不一致”的怪问题。
换上 FrankenPHP 后,本地只需要一个 exe 和一个 Caddyfile。不需要启动额外的服务,也不需要记住一堆进程管理命令。它自带自动 HTTPS,只要域名解析过来,第一次启动就能申请证书。虽然在纯内网开发时用不上这个功能,但一旦联调环境需要,配置成本几乎为零。
还有静态资源处理。传统 Nginx 需要单独配置文件缓存的过期时间、压缩规则、缓存规则,FrankenPHP 的encode指令和 Caddy 的缓存策略可以直接复用,配置方式比 Nginx 的 include 片段更容易阅读。本地环境和线上如果都用它,配置能保持一致,少很多环境差异。
我目前的标准开发结构是:项目根目录放一个 Caddyfile,里面写本地域名和项目路径;启动脚本一键运行 FrankenPHP;代码修改后浏览器直接刷新即可。它和现代前端构建工具的 watch 模式配合得也很好,前端资源由构建工具生成到 public 目录,PHP 接口由 FrankenPHP 处理,没有冲突。
4.2 在 Windows Server 上长期运行
Windows 服务器生产部署是原生支持之后带来的新场景。传统 PHP 在 Windows Server 上一直有点受歧视,IIS 的 PHP 支持维护得还算可以,但很多开发者还是更习惯 Nginx 配置。现在直接跑 FrankenPHP,不用装 PHP,不用配置 IIS 处理器映射,逻辑简单很多。
长期运行需要考虑两点:开机自启和日志持久化。
开机自启,可以用 Windows 的计划任务,开机时执行frankenphp.exe run。也可以找一些服务封装工具把 exe 注册成系统服务。服务方式更稳定,因为和计划任务相比,它有独立的服务管理状态,崩溃后可以自动重启。
日志方面,Caddy 本身会把访问日志输出到标准输出,进程运行在服务里时,需要重定向到文件。可以在启动命令里指定--log参数,或者依赖 Windows 事件查看器。我更建议在 Caddyfile 里显式配置 log 输出文件,并在应用层把错误日志通过框架的日志系统写到独立地方,方便排查。
配置示例:
log { output file D:/logs/frankenphp-access.log level INFO }这段配置放在 Caddyfile 顶部,访问日志会被写到固定文件。生产环境里日志的保留策略也要提前想好,可以用 Windows 自带的计划任务定期压缩归档,避免日志文件无限增长。
4.3 与 Docker 部署方案的迁移关系
这次原生支持并没有削弱 Docker 的价值,反而给了另一种选择。容器化仍然适合大规模微服务、自动化编排、团队标准环境。Windows 原生版本更适合轻量部署、内部工具、无容器环境的机器。
如果你的应用原来是跑在 Docker 里的,想迁移到 Windows 原生版,打包方式和配置都会简单很多。比如原来在容器里要区分 Nginx 和 PHP-FPM 两个镜像,现在统一在一个二进制里。代码没有大改动,只要把 Nginx 的 rewrite 规则变成 Caddy 的 directive,通常半天能完成。
但反过来,如果团队已经有一套成熟的 Docker Compose,开发、测试、生产全用容器,没必要因为原生支持就推翻重来。技术选型的关键永远是匹配场景,而不是追逐新特性。我见过一些团队为了用上原生功能,强行把容器化应用拆出来,结果面临的环境差异问题比原来还多。所以迁移前先问自己:Windows 原生带来的简化,是否真的解决了当前最痛的环节?
5. 我踩过的坑和排查记录
5.1 端口占用与防火墙
Windows 上最典型的启动失败原因,就是端口被占用。特别是默认的 80 和 443,常被系统自带服务、其他 Web 服务,甚至某些软件的更新服务占用。
排查方法很简单,PowerShell 里执行:
netstat -ano | findstr :8080看对应 PID,再到任务管理器里找是哪个进程占用。开发阶段,更推荐直接换一个冷门端口,比如 8080 或 18080,省得和系统服务打架。生产服务器上如果端口需要被外部访问,记得在防火墙里放行对应的 TCP 端口,同时只对必要的网段开放。
我遇到过的另一个问题是,防火墙规则允许了,但局域网同事还是访问不到。仔细查了之后发现,Windows 网络配置文件被识别成了“公用网络”,而我只开放了“专用网络”的访问。最直接的办法是把网络配置文件改成专用网络,或者把防火墙规则同时应用到两个配置文件。
5.2 路径分隔符、权限与编码
路径分隔符是第一关。Windows 资源管理器里的路径是D:\www\myapp,直接写进 Caddyfile,反斜杠会被当作转义符,很容易出错。统一使用正斜杠D:/www/myapp是省心的办法,FrankenPHP 在 Windows 下也能正确识别。
权限问题也挺隐蔽。如果代码里用了file_put_contents写缓存文件,路径在 Linux 上没问题,到了 Windows 上却发现权限不足。原因是 PHP 进程运行的账户对该目录没有写权限。解决方式不是粗暴地给某个账户完全控制权限,而是给启动 FrankenPHP 的服务账户或用户账户授予目录写权限。开发时可以用当前用户直接跑 exe,生产时给服务账户单独授权。
编码问题容易被忽略。Caddyfile 里如果包含中文路径或中文注释,文件必须保存为 UTF-8 编码。Windows 记事本默认的 ANSI 编码会导致配置解析乱码,看起来像文件内容变了,实际是编码问题。写配置我一般用代码编辑器,保存为 UTF-8 without BOM,能避免这类问题。
5.3 worker 进程崩溃与日志定位
worker 模式常驻进程,最怕的是代码里有一个exit()或者未捕获的异常,直接导致 worker 掉线。表现是只影响部分请求,刷新几次又正常,看起来毫无规律。
排查思路分三步。一是确认max_worker配置大于 1,这样单 worker 崩溃还有别的进程接管。二是给 worker 脚本加错误处理,捕获异常后记录到日志文件,而不是直接退出。三是通过 FrankenPHP 的日志输出观察 worker 的启动和关闭记录,通常崩溃前会有对应的错误信息。
我在一个项目里遇到过 worker 崩溃后不再重启的情况。后面发现是max_worker配置范围内全部 worker 都因为同一个代码缺陷退出,新请求才全部报错。修好那个缺陷,再让服务重启,问题就消失了。所以关键还是要把 worker 脚本当成一个长期运行的守护进程来写,任何可能发生异常的路径都要兜住。
另外,Windows 下的信号处理方式与 Linux 不完全一样,调试 worker 时不要用 Linux 的套路直接向进程发信号。我更推荐在 worker 脚本里加一个健康检查端点,返回当前进程状态,外部监控通过 HTTP 探测即可,不必依赖系统信号。
6. 几点个人体会
Windows 历来不是 PHP 部署的理想平台,但这个项目的原生支持让我看到另一种可能。对大多数人来说,它最大的价值并不是把生产服务器全部迁移到 Windows,而是让本地开发、内部工具分发、团队交付多了一个简单选项。
我现在的日常是:Linux 服务器继续跑容器化的大项目,Windows 机器上则用 FrankenPHP 跑那些轻量的小服务和内部工具。两边配置尽量共享同一个 Caddyfile,减少思维切换的成本。新加入的同事也不再需要先花半天配置 WSL 和 Docker Desktop,解压、写三行配置、启动,就进入开发状态。
如果你也在 Windows 上做 PHP 开发,我个人建议留一点时间试试这套组合。它的学习曲线很低,却能砍掉不少日常麻烦。工具不在多,在合适的地方用合适的方式,这大概就是这次原生支持最让我满意的地方。