
先说结论:Cloudflare Workers 本身不能直接解释执行 PHP,但 Cloudflare Containers 可以运行标准 OCI/Docker 镜像。正确架构是“Worker 接收公网请求并选择容器实例,容器内部运行 PHP Web 服务器”。下面用一个可以直接部署的最小项目,把 Dockerfile、Worker 路由、Wrangler 配置、部署验证、费用和持久化边界一次讲清。
本文配置于 2026 年 10 月 11 日按 Cloudflare 官方文档核对。Cloudflare Containers 仍在快速迭代,复制旧教程前先看配置字段是否已经变化。
什么时候应该把 PHP 放进 Cloudflare Containers
适合的场景是:现有 PHP 应用不方便改写为 Worker、需要 PHP 扩展或系统二进制、需要运行 Laravel/Symfony 的后台任务,或者希望利用 Cloudflare 的入口网络,把动态应用按需启动。它不是“把一个 index.php 上传到 Workers”——Worker 和 PHP 是两个运行层。
| 组成 | 职责 | 本文示例 |
|---|---|---|
| Cloudflare Worker | 接收请求、鉴权、改写路径、选择容器实例 | src/index.ts |
| Container | 运行 Linux 镜像与 PHP Web 服务 | php:8.4-apache |
| Durable Object binding | 给容器实例提供可寻址身份与生命周期管理 | PHP_CONTAINER |
Cloudflare 的官方入门文档明确说明:镜像需要支持 linux/amd64;Worker 通过 Durable Object binding 调用容器;首次部署后,容器资源可能还要等待几分钟才能完成配置。
准备条件
- Cloudflare 账户,并开通 Workers Paid 计划。Containers 的计算用量包含在每月 5 美元的 Workers Paid 计划中,超出后按资源用量收费;具体额度见文末费用说明。
- 本机安装 Node.js、npm 和 Docker,且
docker info能正常返回。 - 已经登录 Wrangler:
npx wrangler login。 - 准备一个用于测试的 Cloudflare Workers 子域名;绑定自定义域名可以部署成功后再做。
如果你还在规划服务器、域名、CDN 与应用部署方式,可以先查看 SHOPAGG 的独立站服务页面,把运行环境和业务需求定清楚,再决定是否需要把传统 PHP 应用迁到容器。
第一步:创建 Containers 项目
先从 Cloudflare 官方模板创建项目:
npm create cloudflare@latest -- --template=cloudflare/templates/containers-template
cd <你的项目目录>
docker info
模板已经安装 Wrangler 和 @cloudflare/containers。接下来把示例 Go 服务替换为 PHP。项目保留下面四个关键文件:
cloudflare-php/
├── Dockerfile
├── public/
│ └── index.php
├── src/
│ └── index.ts
└── wrangler.jsonc
第二步:编写 PHP 程序
新建 public/index.php。这个页面返回 JSON,便于确认 PHP 版本、容器位置和实例身份:
<?php
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode([
'ok' => true,
'message' => 'PHP is running in Cloudflare Containers',
'php_version' => PHP_VERSION,
'region' => getenv('CLOUDFLARE_REGION') ?: null,
'location' => getenv('CLOUDFLARE_LOCATION') ?: null,
'container_id' => getenv('CLOUDFLARE_DURABLE_OBJECT_ID') ?: null,
'request_time' => gmdate('c'),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
CLOUDFLARE_REGION、CLOUDFLARE_LOCATION 和 CLOUDFLARE_DURABLE_OBJECT_ID 是平台自动注入的运行时变量,字段来源可在环境变量文档核对。
第三步:制作 PHP 镜像
在项目根目录创建 Dockerfile:
FROM php:8.4-apache
RUN sed -ri 's!Listen 80!Listen 8080!g' /etc/apache2/ports.conf \
&& sed -ri 's!<VirtualHost \*:80>!<VirtualHost *:8080>!g' \
/etc/apache2/sites-available/000-default.conf \
&& echo 'ServerName localhost' >> /etc/apache2/apache2.conf
COPY public/ /var/www/html/
EXPOSE 8080
这里选择 PHP 8.4 的 Apache 官方镜像,并把 Apache 从 80 端口改到 8080。原因不是 PHP 只能监听 8080,而是后面的 Container 类会把 defaultPort 设为 8080;两个值必须一致,否则 Worker 会一直等待一个并不存在的监听端口。
正式项目不要使用 latest 标签。应固定到经过测试的 PHP 主版本甚至补丁版本,并按PHP 支持周期安排升级。需要 pdo_mysql、intl 或 gd 等扩展时,在 Dockerfile 中显式安装,避免“本地能跑、线上缺扩展”。
第四步:让 Worker 把请求送进 PHP 容器
将 src/index.ts 改为:
import { Container } from "@cloudflare/containers";
export class PhpContainer extends Container {
defaultPort = 8080;
sleepAfter = "10m";
envVars = {
APP_ENV: "production",
};
override onStart() {
console.log("PHP container started");
}
override onError(error: unknown) {
console.error("PHP container error", error);
}
}
interface Env {
PHP_CONTAINER: DurableObjectNamespace<PhpContainer>;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (!url.pathname.startsWith("/php")) {
return new Response("Open /php", { status: 404 });
}
// 单应用示例固定使用同一个实例名。
const container = env.PHP_CONTAINER.getByName("php-app");
// 去掉公开入口 /php,避免 Apache 把它当作真实目录。
url.pathname = url.pathname.replace(/^\/php/, "") || "/";
const upstream = new Request(url.toString(), request);
return container.fetch(upstream);
},
} satisfies ExportedHandler<Env>;
sleepAfter = "10m" 表示实例闲置 10 分钟后可以休眠。后续请求会重新启动它,因此低频应用可以缩到零,但用户可能遇到冷启动。Cloudflare 的FAQ称冷启动通常在 1—3 秒区间,实际时间仍取决于镜像体积和启动逻辑。
第五步:配置 wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "php-on-cloudflare-containers",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"containers": [
{
"class_name": "PhpContainer",
"image": "./Dockerfile",
"instance_type": "lite",
"max_instances": 1
}
],
"durable_objects": {
"bindings": [
{
"name": "PHP_CONTAINER",
"class_name": "PhpContainer"
}
]
},
"exports": {
"PhpContainer": {
"type": "durable-object",
"storage": "sqlite"
}
},
"observability": {
"enabled": true
}
}
class_name 必须与 TypeScript 导出的类同名,binding 名称必须与 env.PHP_CONTAINER 一致。max_instances 是允许同时运行的最大实例数,不是预热数量。本文只有一个固定实例名,因此先设为 1 最容易排查。
lite 当前提供 1/16 vCPU、256 MiB 内存和 2 GB 磁盘,适合这个最小示例。Laravel、图片处理或 Composer 依赖较多的项目,通常应从 basic 起测;完整规格见实例类型与限制。
第六步:本地测试并部署
npx wrangler dev
另开一个终端请求:
curl -i http://localhost:8787/php
看到 HTTP 200,并且 JSON 中有 "ok": true 与 PHP 版本,说明 Worker 到容器的链路正常。随后部署:
npx wrangler deploy
npx wrangler containers list
npx wrangler containers images list
第一次执行 wrangler deploy 会调用本机 Docker 构建镜像、上传镜像并部署 Worker。Cloudflare 提醒首次配置可能需要几分钟;Worker URL 先返回,不代表容器已经可用。不要看到一次 502 就连续改十遍代码,先检查容器状态和日志。
如何验证部署确实成功
- 访问
https://你的-worker.workers.dev/php,确认 HTTP 200。 - 响应中的
php_version应与镜像版本一致。 region、location或container_id至少应出现平台注入的信息。- Cloudflare 控制台进入 Workers & Pages → Containers,查看 Status、Metrics 和 Logs。
- 等待超过
sleepAfter后再次访问,观察冷启动时间是否符合业务接受范围。
数据库、上传文件和 Session 怎么处理
这是把 PHP 放入 Containers 最容易踩的坑:容器磁盘默认是临时的。实例休眠后,下次启动会获得从镜像恢复的全新文件系统。把 SQLite 数据库、用户上传、订单附件或 PHP Session 只写到本地目录,迟早会丢。
- 用户上传:放到 R2 或其他对象存储,不要依赖容器本地目录。
- 业务数据库:使用可从容器网络访问的托管 MySQL/PostgreSQL;凭据通过 Worker Secret 或 Secrets Store 注入。
- Session:使用 Redis、数据库或无状态签名 Cookie。
- 临时缓存:可以写本地盘,但必须允许随时丢失并重建。
Cloudflare 支持快照和 R2 FUSE,但快照是不可变的时间点状态,FUSE 也不应期待本地 SSD 的性能。官方对临时磁盘、快照和 FUSE 的边界说明见持久化 FAQ。
环境变量与密钥不要写进镜像
普通配置可以放在 Container 类的 envVars,数据库密码、API Key 等敏感值应使用 Worker Secrets 或 Secrets Store。不要在 Dockerfile 中写 ENV DB_PASSWORD=...,因为镜像层和构建记录可能泄露它。Cloudflare 提供了环境变量与 Secrets 示例,部署生产应用前应按该模式改造。
费用怎么算,免费计划能不能用
截至 2026 年 10 月 11 日,Containers 计费包含在每月 5 美元的 Workers Paid 计划中,免费计划不含容器资源。官方价格页列出的月度包含量为 25 GiB-hours 内存、375 vCPU-minutes CPU 和 200 GB-hours 磁盘;超出后按 10ms 粒度及对应资源单价计费。容器休眠后停止计算容器资源费用,但请求还会涉及 Workers、Durable Objects、日志和网络出站计费。价格会变动,投产前以Cloudflare Containers Pricing为准。
常见错误与排查顺序
1. Cannot connect to the Docker daemon
本机 Docker 没启动。先让 docker info 成功,再执行 wrangler deploy。
2. Worker 正常,但访问 /php 返回 502 或超时
先确认 Dockerfile 中 Apache 的监听端口与 defaultPort 都是 8080;再等首次容器配置完成,并查看 Containers 日志。端口不一致是最常见原因。
3. 镜像在 Apple Silicon 本机可运行,部署后启动失败
Cloudflare 当前要求 linux/amd64。构建含原生二进制的自定义镜像时,确保依赖和入口程序支持该架构,必要时先执行 docker build --platform linux/amd64 . 验证。
4. 发布新版本后仍像旧代码
先用 npx wrangler containers images list 确认新镜像已上传,再看 rollout 和实例日志。Cloudflare 部署时 Worker 先上线,容器镜像默认渐进更新,不保证两者在同一瞬间全部切换。
5. PHP 上传的文件重启后消失
这不是权限故障,而是临时磁盘的设计结果。把永久数据迁移到对象存储或外部数据库,本地目录只放可重建缓存。
上线前检查清单
- 镜像固定 PHP 版本,支持
linux/amd64,并完成漏洞扫描。 - 容器监听端口与
defaultPort一致。 - 密钥使用 Secrets 注入,不进入代码仓库和镜像层。
- 数据库、上传与 Session 不依赖临时磁盘。
- 测试冷启动、并发、超时、内存上限和回滚流程。
- 开启日志,但避免把支付信息、Cookie 和用户隐私写入日志。
这个最小项目跑通后,再把 public/ 换成真实 PHP 应用,并逐项补齐扩展、Composer 依赖、外部数据库、对象存储和健康检查。Cloudflare Containers 解决的是“PHP 运行环境能否靠近 Cloudflare 网络按需启动”,应用的数据一致性、安全和容量规划仍然要由你负责。
作者:庄朋龙,SHOPAGG 官方博客编辑,长期关注独立站建站、服务器运维与跨境电商技术基础设施。



