传统 Laravel 零改动迁移到 FrankenPHP 完整流程

> 先把最重要的一句话说在前面,免得你迁移到一半发现被坑:

  ▎ 「零改动」只在「经典模式」下成立。 经典模式 = 把 FrankenPHP 当成一个更快的 Nginx+PHP-FPM 替代品,你的 Laravel
  ▎ 代码一行都不用动。
  ▎
  ▎ 而「Worker 模式」(性能翻几倍那个)不是零改动——它需要装laravel/octane
  ▎ 这个官方包,并且你的代码得满足「无状态」要求(第 10 章讲过的那些泄漏陷阱)。

  所以这篇分两段走:先零改动跑起来(经典模式),再可选地榨性能(worker 模式)。两条路我都给完整流程。

  ---
  第一部分:真·零改动迁移(经典模式)

  步骤 0:迁移前盘点你现在的架构

  传统 Laravel 生产环境通常长这样:

  浏览器 →Nginx(收 HTTP、发静态文件、反代)
             →PHP-FPM(跑 PHP,通过 FastCGI 协议)
                →public/index.php →Laravel

  涉及的配置文件:
  - nginx.conf / sites-available/xxx(Nginx 站点配置)
  - php-fpm.conf / www.conf(FPM 进程池配置)
  - php.ini

  FrankenPHP 把 Nginx + PHP-FPM 两个东西合并成一个进程,所以上面这套全部不再需要,换成一个 Caddyfile(或者干脆零配置)。

  步骤 1:装 FrankenPHP

  FrankenPHP 官方发布的是单个静态二进制(第 12 章讲的静态链接 libphp 那条路的成品),里面已经内置了 PHP
  解释器和常用扩展,你机器上甚至不需要装 PHP。

  方式 A:直接下二进制(最省事)
  # Linux x86_64
  curl https://frankenphp.dev/install.sh | sh
  sudo mv frankenphp /usr/local/bin/

  # 验证:里面这个 PHP 是什么版本、带哪些扩展
  frankenphp php-cli -v
  frankenphp php-cli -m      # 列出内置扩展,先确认你 Laravel 要的扩展都在

  方式 B:Docker(生产推荐,环境最干净)
  # Dockerfile ——这是迁移 Laravel 的标准官方镜像
  FROM dunglas/frankenphp:latest

  # 装 Laravel 常用的 PHP 扩展(官方镜像带了 install-php-extensions 工具)
  RUN install-php-extensions \
      pdo_mysql \
      redis \
      intl \
      gd \
      zip \
      bcmath \
      opcache \
      pcntl

  # 把你的 Laravel 项目拷进镜像默认的网站根目录
  COPY . /app

  WORKDIR /app

  # 装依赖(生产参数)
  RUN composer install --no-dev --optimize-autoloader

  ▎ 关键检查点:迁移前一定先用 frankenphp php-cli -m 或 install-php-extensions 把你 composer.json 里 ext-*
  ▎ 要求的扩展全配齐。Laravel
  ▎ 常见依赖:pdo_mysql/pdo_pgsql、mbstring、openssl、tokenizer、xml、ctype、json、bcmath、fileinfo、redis(如果用
  ▎ Redis)、intl、gd/imagick(如果处理图片)、pcntl(如果用队列 --timeout)。少装扩展是迁移最常见的翻车点。

  步骤 2:让 FrankenPHP 指向 Laravel 的入口

  Laravel 的 HTTP 入口永远是 public/index.php,所有请求都被 rewrite 到它(这就是 Laravel
  的「前端控制器」模式)。FrankenPHP 内置了对这个模式的支持,一个命令零配置就能跑:

  # 进到 Laravel 项目根目录
  cd /path/to/your-laravel

  # 一行启动,--root 指向 public 目录
  frankenphp php-server --root public/

  就这一行。它会:
  -:443(带自动 HTTPS)或开发时 :80 起一个服务器
  - 自动把所有请求按 Laravel 的规则 rewrite 到 public/index.php
  - 静态文件(public/ 下的 css/js/图片)直接由服务器高速返回,不走 PHP

  打开浏览器访问,你的 Laravel 原封不动就跑起来了。代码零改动。

  步骤 3:用 Caddyfile 做正经配置(生产)

  php-server 命令适合快速验证,生产环境用 Caddyfile 更可控。一个标准的 Laravel Caddyfile 长这样,逐行解释:

  # Caddyfile ——放在项目根目录

  {
        # 全局选项块
        frankenphp                      # 启用 FrankenPHP 模块
        # auto_https off                # 本地开发想关 HTTPS 时取消注释
  }

  your-domain.com {                   # 你的域名;Caddy 会自动申请并续签 Let's Encrypt 证书
        root * public/                  # ★网站根目录指向 Laravel 的 public/
        encode zstd br gzip             # 自动压缩响应(zstd/brotli/gzip)

        php_server                      # ★核心:把请求交给内置 PHP 处理,
                                        #   自动处理 index.php rewrite、静态文件分流
  }

  逐行大白话:
  - frankenphp:在全局块里打开 FrankenPHP。
  - your-domain.com { }:一个站点块。Caddy 最大的福利——自动HTTPS:你写个真实域名,它自动去 Let's Encrypt
  申请证书、自动续期,0 配置。传统 Nginx 你得手搓 certbot。
  - root * public/:告诉服务器网站根是 public/(Laravel 的 web 根),其它目录(app/、vendor/.env)天然不暴露,安全。
  - encode ...:响应压缩,替代 Nginx 的 gzip on。
  - php_server:一条指令顶 Nginx 一大段 location ~ \.php$ + fastcgi_pass。它内部等价于:
    - 请求的是存在的静态文件 →直接返回
    - 否则 →rewrite 到 index.php 交给 PHP 跑

  启动:
  frankenphp run                 # 它会自动找当前目录的 Caddyfile
  # 或指定:
  frankenphp run --config /path/to/Caddyfile

  步骤 4:把 PHP-FPM 和 Nginx 的配置「翻译」过来

  迁移时你旧配置里的设置要找到对应位置:

  ┌─────────────────────────────────────┬──────────────────┬────────────────────────────────────────────────────────┐
  │            你原来在哪配             │    原来怎么写    │                  FrankenPHP 里怎么配                   │
  ├─────────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────┤
  │ php.ini 的                          │                  │ 还是 php.ini。用 frankenphp run 时它读环境里的         │
  │ memory_limit、upload_max_filesize   │ php.ini          │ php.ini;Docker 镜像里在                               │
  │ 等                                  │                  │ $PHP_INI_DIR/php.ini。改法不变                         │
  ├─────────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────┤
  │                                     │                  │ Caddyfile 里 request_body { max_size 20MB },或        │
  │ Nginx client_max_body_size 20M      │ nginx.conf       │ php.ini 的                                             │
  │                                     │                  │ upload_max_filesize/post_max_size(两边取小)          │
  ├─────────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────┤
  │ FPM pm.max_children(进程数)       │ http://www.conf  │ Caddyfile 的 frankenphp { num_threads N },控制 PHP    │
  │                                     │                  │ 线程数                                                 │
  ├─────────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────┤
  │ Nginx 静态文件缓存头                │ location ~*      │ Caddyfile 的 header 指令,或交给 php_server 默认处理   │
  │                                     │ \.(jpg|css)$     │                                                        │
  ├─────────────────────────────────────┼──────────────────┼────────────────────────────────────────────────────────┤
  │ Nginx 反代 / 负载均衡               │ upstream         │ Caddy 的 reverse_proxy,但单机场景通常不需要了         │
  └─────────────────────────────────────┴──────────────────┴────────────────────────────────────────────────────────┘

  设置线程数的写法:
  {
        frankenphp {
                num_threads 40      # 类比 FPM 的 pm.max_children,按内存和负载调
        }
  }

  步骤 5:迁移验证清单(逐项打勾)

  零改动不代表零验证。挨个确认:

  # 1) 扩展齐不齐(最常翻车)
  frankenphp php-cli -m | sort
  # 对照 composer.json 里所有 "ext-xxx"

  # 2) Laravel 的缓存先清,避免旧路径残留
  php artisan config:clear && php artisan route:clear && php artisan view:clear

  # 3) 生产优化缓存(这些和 FPM 时一模一样,不变)
  php artisan config:cache
  php artisan route:cache
  php artisan view:cache
  php artisan event:cache

  # 4) 文件权限(storage 和 bootstrap/cache 要可写)
  chmod -R ug+rw storage bootstrap/cache

  功能层面要手动点一遍的:
  - ✅ 普通页面渲染(GET)
  - ✅ 表单提交、文件上传(POST + multipart,验证 read_post 那条链路)
  - ✅ Session / 登录态(Cookie,验证 read_cookies)
  - ✅ 文件下载、大响应(验证 ub_write 流式输出)
  - ✅ php artisan 命令照常能跑(CLI 不受影响)
  - ✅ 队列 worker(php artisan queue:work)和定时任务(schedule:run)——这些是独立进程,迁移不影响它们,照旧用
  supervisor/cron 跑

  步骤 6:队列、定时任务、Horizon 怎么办

  这是迁移时容易忽略的:FrankenPHP 只负责 HTTP 那一半。Laravel 的后台部分是独立的:

  # 定时任务:crontab 不变,照旧
  * * * * * cd /path/to/laravel && php artisan schedule:run >> /dev/null 2>&1

  # 队列:supervisor 配置不变,照旧用 php-cli 跑
  # /etc/supervisor/conf.d/laravel-worker.conf
  [program:laravel-worker]
  command=php /path/to/laravel/artisan queue:work --sleep=3 --tries=3

  注意 php 这里如果系统没装 PHP,用 frankenphp php-cli /path/to/artisan queue:work。FrankenPHP 二进制本身就能当 php
  命令用。

  到这一步,经典模式迁移完成,代码零改动,已经能上生产。性能通常已经比 Nginx+FPM 略好(少了 FastCGI
  协议开销),且运维简化(一个进程、自动 HTTPS)。

  ---
  第二部分:榨性能(Worker 模式,非零改动但收益巨大)

  如果你想要 FrankenPHP 那个「快 3-4 倍」的效果,上 worker 模式。这需要 Laravel Octane(官方包,专门适配
  Swoole/RoadRunner/FrankenPHP)。

  为什么 worker 模式快:回顾第 7 章

  经典模式每个请求都要:require autoload →启动整个 Laravel 框架(注册几百个服务、解析路由、读配置)→处理 →
  全部销毁。Laravel 框架启动本身就要几十毫秒,90% 的时间花在重复启动框架上。

  Worker 模式:框架只启动一次常驻内存,之后每个请求复用,省掉那几十毫秒。

  步骤 1:装 Octane

  composer require laravel/octane

  # 安装并选 FrankenPHP 作为 server
  php artisan octane:install --server=frankenphp

  这会装好 Octane 并配置成用 FrankenPHP 的 worker 模式。

  步骤 2:启动

  # 开发
  php artisan octane:start --server=frankenphp --watch
  #                                              ↑改代码自动重载(第 11.5 节那个热重载)

  # 生产
  php artisan octane:start --server=frankenphp --workers=4 --max-requests=500
  #   --workers      worker 线程数,一般 = CPU 核数
  #   --max-requests 每个 worker 处理 N 个请求后重启,防内存泄漏的安全阀

  --max-requests 很重要:它是「定期重启 worker」的兜底机制,就算你代码有轻微泄漏,跑 500 个请求就重启清零,不至于 OOM。

  步骤 3:代码体检(这是「非零改动」的核心工作)

  Worker 模式下框架常驻,第 10 章讲的所有泄漏陷阱都要排查。针对 Laravel 具体化:

  ①静态属性 / 单例累积
  // ☠️危险
  class SomeService {
      private static array $cache = [];   // worker 模式下永不清空
  }
  排查:grep 你代码里的 static 属性容器、static $x = []。

  ②容器里被你手动绑定的「请求作用域」单例
  // 如果你在 ServiceProvider 里这样绑过单例,且它持有了请求数据:
  $this->app->singleton(CurrentTenant::class, ...);   // 小心:会跨请求残留
  Octane 提供了配置来每个请求后重置指定服务:
  // config/octane.php
  'warm' => [...],     // 每个请求前预热的服务
  'flush' => [...],    // 每个请求后清空的服务  ←把有状态的放这
  'listeners' => [
      RequestReceived::class => [...],
      RequestTerminated::class => [
          FlushTemporaryContainerInstances::class,  // Octane 自带的清理
      ],
  ],

  ③Octane 内存泄漏检测

  Octane 自带工具帮你定位:
  # 它会报告哪些请求导致内存持续增长
  php artisan octane:start --server=frankenphp
  # 配合监控 memory_get_usage,或用 --max-requests 兜底

  ④Laravel 官方已知要注意的点(Octane 文档明确列出):
  - 不要用 $request 的全局 helper 缓存:用 Auth::user() 等没问题(Octane 会重置),但你自己 static 缓存了 request()
  就会串台。
  - 配置/容器单例里别存请求数据。
  - 第三方包:确认你用的包兼容 Octane(大部分主流包已兼容)。

  步骤 4:迁移决策——到底用不用worker 模式

  ┌──────────┬──────────────────────┬──────────────────────────────┐
  │   维度   │       经典模式       │     Worker 模式(Octane)      │
  ├──────────┼──────────────────────┼──────────────────────────────┤
  │ 代码改动 │ 零                   │ 需排查无状态,可能改少量代码 │
  ├──────────┼──────────────────────┼──────────────────────────────┤
  │ 性能     │ 比 FPM 略好          │ 比 FPM 快 3-4 倍             │
  ├──────────┼──────────────────────┼──────────────────────────────┤
  │ 迁移风险 │ 极低                 │ 中(状态泄漏风险)           │
  ├──────────┼──────────────────────┼──────────────────────────────┤
  │ 适合     │ 想平滑替换 Nginx+FPM │ 高并发、追求极致吞吐         │
  └──────────┴──────────────────────┴──────────────────────────────┘

  我的建议:分两步走。先经典模式零改动上生产、稳住(享受简化运维和自动 HTTPS),跑一段时间确认无误;之后再单独立项做
  Octane 改造,在 staging
  环境充分压测和泄漏排查后再切。别一步到位把「换服务器」和「改架构」两件事混在一次上线里——出问题难定位。

  ---
  完整迁移路线图(一张图收尾)

  现状:Nginx + PHP-FPM + Laravel
    │
    ├─【第一阶段:零改动】───────────────────────────
    │   1. 装 FrankenPHP(二进制 or Docker dunglas/frankenphp)
    │   2. 用 install-php-extensions 配齐扩展(对照 composer.json)★最易翻车
    │   3. 写 Caddyfile:root →public/,php_server 一行搞定
    │   4. 把 php.ini / FPM / Nginx 设置翻译过来(见对照表)
    │   5. artisan config:cache / route:cache(和原来一样)
    │   6. 队列/定时任务用 frankenphp php-cli 跑,supervisor/cron 不变
    │   7. 功能清单逐项验证(POST/Session/上传/下载)
    │   →上生产。代码零改动,运维简化,自动 HTTPS,性能略升
    │
    └─【第二阶段:可选提速】──────────────────────────
        1. composer require laravel/octane
        2. artisan octane:install --server=frankenphp
        3. ★体检无状态:static属性、请求作用域单例、连接事务状态
        4. config/octane.php 配 flush 清理有状态服务
        5. octane:start --workers=N --max-requests=500(兜底防泄漏)
        6. staging 压测 + 内存监控,确认无泄漏
        →切生产。性能 3-4 倍

  三句话记住:
  1. 经典模式 = 真零改动,root public/ + php_server 一行,把它当更好用的 Nginx+FPM。
  2. 迁移最大的坑是扩展没配齐,先 frankenphp php-cli -m 对照 composer.json。
  3. 要极致性能上 Octane worker 模式,但那不是零改动——得排查第10 章那些跨请求状态泄漏,用 --max-requests 兜底。

更多推荐