使用 FrankenPHP 与 Docker 的现代 PHP 开发

我得坦白说——第一次听到 FrankenPHP 这个名字时,我以为它只是 PHP 圈里又一个时髦的名词。结果完全不是这么回事。

在传统 PHP 部署环境里摸爬滚打多年,折腾 Apache 配置、与性能瓶颈拉扯,直到遇见 FrankenPHP,那感觉就像有人告诉你有一条从未公开过的捷径。

FrankenPHP 不只是另一个应用服务器。它把 PHP 带到了现代 Web 开发的语境里:基于久经考验的 Caddy Web 服务器,原生支持 HTTP/2 与 HTTP/3、自动 HTTPS、实时能力,甚至可以把整个 PHP 应用编译成一个独立可执行的二进制文件。

这篇文章会带你从零开始,用 FrankenPHP 将 PHP 应用容器化,并解释为什么这种方式可能会彻底改变你对 PHP 部署的看法。

为什么 FrankenPHP 改变一切

在动手之前,先说说“为什么”。传统的 PHP 部署历来都不算省心。

想让一个最简单的应用跑起来,你需要一个 Web 服务器(Apache 或 Nginx)、PHP-FPM、成堆的配置文件、SSL 证书,以及一整套错综复杂的组件。我已经花了无数时间在调配置、解故障的路上。

FrankenPHP 用一个“全能二进制”把这些复杂度拿掉了。更进一步,它开箱即用地支持现代 Web 标准,提供传统栈难以达到的性能特性,同时在部署形态上既支持容器,也支持“打包成一个可执行文件”的极致简化方案。

搭建开发环境

开始之前,请先安装 Docker。如果还没装,可以去 Docker 官方网站下载。你需要对 PHP 和 Docker 有一点基础认识,但文中我也会边走边解释。

创建你的第一个 FrankenPHP 应用

从最简单的起步更容易理解,先建个基础项目:

bash
mkdir my-frankenphp-app && cd my-frankenphp-app

新建一个 index.php

php
<?php
// index.php
?>
<!DOCTYPE html>
<html>
<head>
  <title>FrankenPHP Demo</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 40px; }
    .container { max-width: 800px; margin: 0 auto; }
    .feature { background: #f4f4f4; padding: 20px; margin: 10px 0; border-radius: 5px; }
  </style>

</head>
<body>
  <div class="container">
    <h1>Hello from FrankenPHP!</h1>
    <div class="feature">
      <h3>Server Information</h3>
      <p><strong>PHP Version:</strong> <?= PHP_VERSION ?></p>
      <p><strong>Server Software:</strong> <?= $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown' ?></p>
      <p><strong>Request Time:</strong> <?= date('Y-m-d H:i:s') ?></p>
    </div>

    <div class="feature">
      <h3>HTTP Protocol</h3>
      <p><strong>Protocol:</strong> <?= $_SERVER['SERVER_PROTOCOL'] ?? 'Unknown' ?></p>
      <p><strong>HTTPS:</strong> <?= isset($_SERVER['HTTPS']) ? 'Yes' : 'No' ?></p>
    </div>
  </div>

</body>
</html>

这段示例比“Hello, world!”更有意思,能直观展示 FrankenPHP 的一些能力。

用 FrankenPHP 容器化

激动人心的部分来了。在项目根目录新建 Dockerfile

Dockerfile
FROM dunglas/frankenphp:latest

# 拷贝应用代码到容器
COPY . /app

# 设置工作目录
WORKDIR /app

# 暴露端口 80
EXPOSE 80

# 启动 FrankenPHP
CMD ["frankenphp", "php-server", "--root=/app", "--listen=:80"]

这个 Dockerfile 的“简洁”会让你直观感受到差别:

  • 不需要配置 Apache 虚拟主机
  • 不需要 PHP-FPM 配置文件
  • 不需要复杂的 Nginx 反代设置

只需拷贝文件,然后跑起来。

构建并运行:

bash
docker build -t my-frankenphp-app .
docker run -p 8080:80 my-frankenphp-app

打开 http://localhost:8080,你就能看到应用在跑了。值得一提的是:在“看似平平无奇”的背后,FrankenPHP 已经为你启用了 HTTP/2、自动压缩,以及一系列通常需要手动配置才能获得的性能优化。

加入真实世界的依赖

大多数项目都会有依赖。若你使用 Composer(大概率会),可以这样优化 Dockerfile:

Dockerfile
FROM dunglas/frankenphp:latest

# 安装 Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# 先拷贝 composer 文件以利用 Docker 层缓存
COPY composer.json composer.lock* /app/

# 设置工作目录
WORKDIR /app

# 安装依赖
RUN composer install --no-dev --optimize-autoloader --no-interaction

# 再拷贝其余应用代码
COPY . /app

# 暴露端口
EXPOSE 80

# 启动服务
CMD ["frankenphp", "php-server", "--root=/app", "--listen=:80"]

这个顺序利用了 Docker 镜像的分层缓存:当 composer.json 没变时,composer install 的结果可以复用缓存;只有当依赖本身变化时才会重装,避免每次源代码改动都重新安装依赖。

解锁高级特性

下面这些,是 FrankenPHP 真正“拉开差距”的地方。在传统栈中要么很费劲,要么几乎做不到。

HTTPS 与 HTTP/2/3 支持

我最喜欢的一点就是 FrankenPHP 处理 HTTPS 的方式。传统配置 SSL 通常需要多步:证书生成、Web 服务器配置、自动续期脚本等。FrankenPHP 要么帮你自动搞定,要么你只需提供证书即可。

开发环境下使用自签证书:

Dockerfile
FROM dunglas/frankenphp:latest

COPY . /app
WORKDIR /app

# 拷贝你准备好的证书
COPY certs/cert.pem certs/key.pem /certs/

EXPOSE 443

CMD ["frankenphp", "php-server", "--root=/app", "--listen=:443", "--tls-cert=/certs/cert.pem", "--tls-key=/certs/key.pem"]

运行:

bash
docker run -p 8443:443 my-frankenphp-app

生产环境要自动 HTTPS(真的很“魔法”):

bash
frankenphp php-server --domain yourdomain.com

就这一个命令。FrankenPHP 会自动申请和续期 Let’s Encrypt 证书。我第一次看它跑起来的时候,直呼“这也太省心了”。

Worker 模式:持久化应用状态

真正有意思的地方在这。传统 PHP 的请求-响应是“短生命周期”的,每次请求都会销毁上下文。而 FrankenPHP 的 Worker 模式会在请求之间保留应用内存,这对 Symfony、Laravel 一类框架能显著提速。

创建一个 worker.php

php
<?php
// worker.php

require_once 'vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

// 下面的代码在 Worker 启动时只执行一次
$app = new MyApplication();
$cache = new SomeExpensiveCache();

// 在循环中处理请求
while ($request = \FrankenPHP\workerRequest()) {
    try {
        // 每个请求都会执行到这里,但 $app 和 $cache 会持久化保留
        $response = $app->handle($request);
        \FrankenPHP\workerResponse($response);
    } catch (\Throwable $e) {
        \FrankenPHP\workerResponse(new Response('Error: ' . $e->getMessage(), 500));
    }
}

更新 Dockerfile:

Dockerfile
CMD ["frankenphp", "php-server", "--root=/app", "--worker=/app/worker.php", "--listen=:80"]

性能影响非常可观。根据我的经验,启用 Worker 模式后,很多应用能获得 2–3 倍的吞吐提升,因为那些昂贵的初始化只需执行一次。

使用 Fibers 的异步 PHP

FrankenPHP 同样支持利用 PHP Fibers 来做“准异步”。尽管 PHP 传统上并不以异步见长,但 FrankenPHP 让它变得可行:

php
<?php
// async-example.php

function asyncOperation($id) {
  return \FrankenPHP\async(function() use ($id) {
    // 模拟一些异步工作
    sleep(2);
    return "Result for operation $id";
  });
}

// 启动多个异步任务
$operations = [];
for ($i = 1; $i <= 5; $i++) {
  $operations[] = asyncOperation($i);
}

// 等待全部完成
foreach ($operations as $operation) {
  echo $operation() . "\n";
}

这为 PHP 打开了不少原本难以企及的场景。

独立二进制:部署方式的“降维打击”

我认为 FrankenPHP 最具革命性的能力,是把你的 PHP 应用(连同 PHP 运行时与所有依赖)编译进一个静态二进制里。

想想这意味着什么——“在我机器上能跑”的老问题不再出现;没有运行时依赖;部署脚本也大幅简化。

生产化准备

构建二进制前,先做一些生产化处理:

bash
# 复制一份干净的构建目录
mkdir build-app
cp -r . build-app/
cd build-app

# 设置生产环境
echo "APP_ENV=prod" > .env.local
echo "APP_DEBUG=false" >> .env.local

# 安装生产依赖
composer install --no-dev --optimize-autoloader --ignore-platform-reqs

# 移除无关文件
rm -rf tests/ .git/ node_modules/ *.md

# 若是 Symfony 应用,预热缓存
php bin/console cache:warmup --env=prod

构建二进制

创建 static-build.Dockerfile

Dockerfile
FROM dunglas/frankenphp:static-builder

# 拷贝准备好的应用
COPY . /go/src/app/dist/app

# 设置工作目录
WORKDIR /go/src/app/

# 将应用嵌入,构建静态二进制
RUN EMBED=dist/app/ ./build-static.sh

构建并导出:

bash
docker build -t my-static-app -f static-build.Dockerfile .
docker create --name temp-container my-static-app
docker cp temp-container:/go/src/app/dist/frankenphp-linux-x86_64 ./my-app
docker rm temp-container

运行独立应用

现在你已经有一个自包含的二进制:

bash
# 赋予执行权限
chmod +x my-app

# 以 Web 服务器模式启动
./my-app php-server

# 或启用自动 HTTPS
./my-app php-server --domain localhost

# 执行 CLI 命令
./my-app php-cli bin/console list

第一次用这种方式部署时,我真有点惊讶。一个 50MB 左右的文件,取代了曾经需要多个服务、配置文件和依赖管理的复杂流程。

真实世界的性能观感

以实战观察,基于 FrankenPHP 的应用通常具有:

  • 50%–70% 的内存占用下降(相较 PHP-FPM 传统部署)
  • 启用 Worker 模式后 2–3 倍的吞吐提升
  • 更快的冷启动(优化过的运行时)
  • 在容器环境里的资源利用更优

需要注意的是:Worker 模式要求你关注内存泄漏与请求间的状态清理。并非所有 PHP 应用的代码都天然适合持久化进程模型。

生产部署策略

在生产环境中,我常使用如下 Docker Compose:

yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "80:80"
      - "443:443"
    environment:
      - FRANKENPHP_CONFIG=
        {
          "apps": {
            "http": {
              "servers": {
                "srv0": {
                  "listen": [":80", ":443"],
                  "routes": [{
                    "handle": [{
                      "handler": "php_server",
                      "root": "/app"
                    }]
                  }]
                }
              }
            }
          }
        }
    volumes:
      - ./certs:/certs
    restart: unless-stopped

对于 Kubernetes,这个“单一二进制”的方案尤其诱人:不再需要 init 容器来拉依赖,也不必搞复杂的卷挂载。

展望与总结

FrankenPHP 代表着我们对 PHP 部署与性能认知的一次“重构”。它让 PHP 以现代 Web 的姿态出现,同时保留了 PHP 一直以来的简洁与易用。

无论你选择容器化方案以获取灵活性,还是选择独立二进制以达成部署极简,FrankenPHP 都提供了传统方案难以匹敌的路径。现代协议栈、性能优化与多样化的交付方式组合起来,使它既适合新项目,也适合为既有项目“现代化”。

站在“传统 PHP 部署”的多年老兵视角,我可以很有把握地说:FrankenPHP 不是小修小补,而是对 PHP 开发可能性的重新想象。

动手试试吧。很可能你会跟我一样,发现回不去了。

—— 2025-08

JaguarJack
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接