官方 PHP 镜像到底差了什么?
如果你曾经用 Docker 部署过 PHP 应用,大概率经历过这样的场景:拉一个 php:8.3-fpm 官方镜像,写一份几十行的 Dockerfile 装 Composer、装扩展、调权限,再挂一堆配置文件——php.ini、www.conf、nginx.conf——最后祈祷生产环境和本地表现一致。
这不是你一个人的问题。官方 PHP Docker 镜像本质上只是一个"能跑 PHP"的最小运行时:没有 Composer,没有生产级的安全加固,默认以 root 用户运行,FPM 参数是裸的默认值,健康检查需要自己写,OPcache 配置全靠自己摸索。对于本地开发勉强够用,但离"生产就绪"还有相当的距离。
于是大多数团队的做法是:fork 一份官方 Dockerfile,加上自己的补丁,维护一个内部镜像。时间一长,这个镜像就变成了没人敢动的"祖传配置"。
Serversideup/php 就是为了解决这个问题而生的。它不是另起炉灶造一个新镜像,而是在官方 PHP 镜像之上,补全了生产环境真正需要的一切。
项目概况
serversideup/php 由 Server Side Up 团队(Dan Pastori 和 Jay Rogers)维护,是一个完全开源(GPL-3.0)的 PHP Docker 镜像方案。截至目前,项目在 GitHub 上有 2,500+ stars,Docker Hub 累计拉取超过 100 万次,已有 46 位贡献者参与开发,最新版本为 v4.5.1(2026 年 7 月发布)。
它支持的 PHP 版本范围很广:PHP 7.4 到 8.5,同时提供 Debian 和 Alpine 两种基础系统。镜像变体覆盖了几乎所有主流部署场景:
| | |
|---|
cli | | serversideup/php:8.5-cli |
fpm | | serversideup/php:8.5-fpm |
fpm-nginx | | serversideup/php:8.5-fpm-nginx |
fpm-apache | | serversideup/php:8.5-fpm-apache |
frankenphp | | serversideup/php:8.5-frankenphp |
所有镜像同时发布在 Docker Hub 和 GitHub Packages 上,不会因为单一源的故障而拉不到镜像。
核心特性详解
与其罗列所有功能,不如挑几个真正改变开发体验的特性展开聊聊。
环境变量驱动配置
这是 serversideup/php 最核心的设计理念:几乎所有配置项都可以通过环境变量完成,不需要挂载或修改任何配置文件。
一个典型的例子。假设你需要调整 PHP 的内存限制、上传大小、执行时间,并开启 OPcache:
services:php:image:serversideup/php:8.5-fpm-nginxenvironment:PHP_MEMORY_LIMIT:"512M"PHP_UPLOAD_MAX_FILE_SIZE:"200M"PHP_MAX_EXECUTION_TIME:"180"PHP_OPCACHE_ENABLE:"1"
就这样。不需要 COPY php.ini /usr/local/etc/php/conf.d/,不需要在 Dockerfile 里写 RUN sed -i ...。改完重启容器即可生效。
环境变量覆盖了 PHP 本身的几乎所有可调参数,也覆盖了 NGINX/Apache/Caddy 的关键配置。举几个容易忽略但很有用的:
PHP_FPM_PM_MAX_CHILDREN:FPM 最大子进程数,默认 20,低配机器上可能需要调小PHP_OPCACHE_JIT 和 PHP_OPCACHE_JIT_BUFFER_SIZE:PHP 8.x 的 JIT 编译器开关,CPU 密集型场景可以开启试试NGINX_CLIENT_MAX_BODY_SIZE:NGINX 请求体大小限制,默认 100MPHP_FPM_PM_CONTROL:FPM 进程管理策略,fpm-nginx 和 fpm-apache 默认是 ondemand(按需启动),这对低流量站点很友好
我个人的经验是:刚开始用的时候会不习惯"什么都靠环境变量"这种方式,觉得不如直接改配置文件直观。但用了一段时间之后,你会发现这恰恰是容器化应该有的样子——配置跟着部署走,而不是跟着镜像走。
非 root 运行与安全加固
官方 PHP 镜像默认以 root 用户运行。这意味着如果你的应用被攻破,攻击者拿到的就是容器内的 root 权限。在 Kubernetes 等编排平台中,root 容器通常会被安全策略直接拒绝。
serversideup/php 默认以 www-data(UID 33)用户运行,遵循最小权限原则。这在安全层面带来了几个好处:
- 爆炸半径受限:即使应用被入侵,攻击者无法在容器内执行特权操作
- Kubernetes 兼容:很多集群的 PodSecurityPolicy 或 OPA 策略要求非 root 运行
- 符合 CIS/NIST 基准:安全审计时少一项整改
如果需要在构建阶段安装扩展,可以临时切换到 root:
FROM serversideup/php:8.5-cliUSER rootRUN install-php-extensions bcmath imagick redis mongodbUSER www-data
这里用到的 install-php-extensions 也是预装好的工具(来自 mlocati/docker-php-extension-installer),一行命令装扩展,自动处理依赖,不用再操心 apt-get 装一堆 dev 库。
另一个值得注意的安全细节:镜像默认关闭了 server_tokens(不暴露 NGINX 版本号),Session Cookie 默认要求 Secure 连接,SSL 模式支持 off/mixed/full 三种策略。这些都是"不需要你额外操心但确实有用"的加固。
S6 Overlay 进程管理
如果你用过 fpm-nginx 或 fpm-apache 变体,会发现容器里实际上跑着两个进程:PHP-FPM 和 Web Server(NGINX/Apache)。Docker 的哲学是"一个容器一个进程",但现实部署中把 FPM 和 Web Server 放在一起往往更实用——少一层网络跳转,配置也更简单。
问题是:谁来管理这两个进程的生命周期?如果一个挂了怎么办?
serversideup/php 选择了 S6 Overlay 作为 init 系统。S6 是一个轻量级的进程管理方案,专门为容器设计,比 Supervisor 更适合 Docker 场景。它提供了:
- 进程监督:FPM 或 Web Server 异常退出时自动重启
- 优雅关闭:收到 SIGTERM 时按正确顺序停止进程,支持零停机部署
- 统一日志:所有进程的日志都输出到 STDOUT/STDERR,直接对接 Docker 日志驱动
对使用者来说,这些细节是透明的。你只需要知道:容器内的进程管理是可靠的,不需要自己写启动脚本或者额外装 Supervisor。
S6 的行为也可以通过环境变量调整。比如 S6_BEHAVIOUR_IF_STAGE2_FAILS 默认值是 2(服务启动失败时停止容器),确保编排系统能检测到故障并重启。排查问题时可以把 S6_VERBOSITY 从默认的 1 调高,会看到更详细的启动日志。
FrankenPHP 支持
FrankenPHP 是这个项目里最值得关注的变体之一。它不是一个传统的 FPM 进程管理器,而是把 PHP 直接嵌入到 Caddy Web Server 中运行,从根本上消除了 FPM 和 Web Server 之间的 FastCGI 通信开销。
用 FrankenPHP 意味着什么?
- 架构简化:不再需要 FPM + NGINX 双进程,一个 FrankenPHP 进程就搞定了一切
- HTTP/2、HTTP/3 原生支持:Caddy 自带这些能力,不需要额外配置
- Worker 模式:可以把 PHP 应用常驻内存,省去每次请求的框架启动开销(对 Laravel 这类重型框架效果显著)
- 性能提升:社区反馈在多种场景下比传统 FPM 快 2-3 倍
切换方式非常简单:
services:php:image:serversideup/php:8.5-frankenphpports:-"80:8080"volumes:-./:/var/www/html
把 image 从 fpm-nginx 换成 frankenphp,其他配置基本不变。Caddy 相关的配置(端口、日志级别、Server Root 等)通过 CADDY_* 前缀的环境变量控制。
踩坑提醒:FrankenPHP 目前支持 PHP 8.3+,如果你的项目还跑在 8.2 或更低版本,暂时用不了。另外,FrankenPHP 的 Worker 模式需要应用做少量适配(比如正确处理数据库连接的持久化),不是所有项目都能"零改动迁移"。建议先在 staging 环境充分测试。
快速上手
废话少说,直接上手。以下是一个最小可运行的 Laravel 项目部署示例。
1. 创建项目
mkdir -p my-app/publiccd my-app
在 public/ 下创建 index.php:
<?phpphpinfo();
2. 编写 compose.yml
services:php:image:serversideup/php:8.5-fpm-nginxports:-"80:8080"volumes:-./:/var/www/htmlenvironment:PHP_UPLOAD_MAX_FILE_SIZE:"250M"PHP_OPCACHE_ENABLE:"0"# 开发环境关闭,方便调试
注意端口映射:容器内 Web Server 监听 8080(非特权端口),映射到宿主机的 80。这是刻意为之——非 root 用户无法绑定 1024 以下的端口。
3. 启动
docker compose up
启动成功,输出
访问 http://localhost,看到 phpinfo() 页面就算成功。页面上应该能看到 PHP 8.5、upload_max_filesize = 250M、opcache.enable = Off。
4. 构建部署镜像
在生产环境中,为了安全性、可靠性和可移植性,你会直接将代码集成到镜像中
对于一个简单的 PHP 应用程序来说,你的 Dockerfile 可能如下所示
services:php:image:serversideup/php:8.5-frankenphpports:-"80:8080"volumes:-./:/var/www/htmlenvironment:PHP_UPLOAD_MAX_FILE_SIZE:"250M"PHP_OPCACHE_ENABLE:"1"# 生产环境开启
一旦你有了 Dockerfile,就可以使用以下命令构建镜像:
docker build -t docker-php-tinywan-hello:latest .
在部署之前,请先在当地测试你的生产镜像。
compose.prod.yml
services:php:image:docker-php-tinywan-hello:latestports:-"8799:8080"environment:PHP_OPCACHE_ENABLE:"1"PHP_MEMORY_LIMIT:"512M"
运行
dockercompose-fcompose.prod.ymlup
这样你就可以在将镜像部署到实际服务器之前,确认其是否能够正确运行。
性能表现
关于性能,Server Side Up 官方引用了 Rabbit Company CEO Žiga Zajc 的测试数据:在相同条件下,serversideup/php 的 FPM 镜像可以处理约 484 请求/秒,而官方 PHP 镜像只有约 68 请求/秒。
这个差距主要来自几个方面:
- OPcache 预配置:官方镜像不带 OPcache 优化配置,serversideup/php 提供了经过调优的默认参数
- FPM 进程管理:默认使用
ondemand 策略(fpm-nginx/fpm-apache 变体),按需启动 worker,比 dynamic 更省资源 - 安全加固的副作用:非 root 运行、正确的文件权限、关闭不必要的功能,这些都间接减少了开销
当然,基准测试的数据需要结合自己的业务场景来看。但至少可以确认:使用这个镜像不会因为"封装了太多东西"而变慢,恰恰相反,合理的默认配置反而比裸镜像跑得更好。
在生产环境中,还可以通过环境变量进一步调优:
environment:PHP_OPCACHE_ENABLE:"1"PHP_OPCACHE_MEMORY_CONSUMPTION:"256"PHP_OPCACHE_MAX_ACCELERATED_FILES:"20000"PHP_OPCACHE_JIT:"1255"PHP_OPCACHE_JIT_BUFFER_SIZE:"128M"PHP_FPM_PM_MAX_CHILDREN:"50"PHP_FPM_PM_CONTROL:"dynamic"PHP_MEMORY_LIMIT:"512M"
内置健康检查
生产环境中,负载均衡器和编排系统需要知道你的应用是否还活着。serversideup/php 内置了健康检查端点(默认路径 /healthcheck),Web 类变体(fpm-nginx、fpm-apache)会自动响应。
这意味着在 Kubernetes 中配置 livenessProbe 和 readinessProbe 变得极其简单:
livenessProbe:httpGet:path:/healthcheckport:8080initialDelaySeconds:10periodSeconds:15readinessProbe:httpGet:path:/healthcheckport:8080initialDelaySeconds:5periodSeconds:10
健康检查路径可以通过 HEALTHCHECK_PATH 环境变量自定义。注意 cli 和 frankenphp 变体没有内置这个端点——前者因为没有 Web Server,后者因为 FrankenPHP 有自己的机制。
原生 CloudFlare 支持
如果你的站点走 CloudFlare CDN,获取用户真实 IP 是一个常见问题。CloudFlare 的请求经过代理转发后,Web Server 看到的来源 IP 是 CloudFlare 的节点 IP,不是用户真实 IP。
serversideup/php 的 fpm-nginx 和 fpm-apache 变体预配置了 CloudFlare 的可信代理列表,开箱即用。你不需要手动维护 CloudFlare IP 列表,也不用在 NGINX 里写一堆 set_real_ip_from 指令。
适用场景与局限性
适合的场景:
- Laravel、WordPress、Symfony 等主流 PHP 框架的新项目或迁移项目
- Kubernetes、Docker Swarm 等容器编排环境
- 想要快速切换 PHP 版本测试兼容性的 CI/CD 流水线
需要注意的地方:
- 端口差异:默认监听 8080 而非 80/443,已有的反向代理配置可能需要调整
- Webroot 路径:默认文档根目录是
/var/www/html/public,如果你的项目结构不同,需要设置 NGINX_WEBROOT 或 APACHE_DOCUMENT_ROOT - FrankenPHP 成熟度:虽然性能出色,但 Worker 模式需要应用适配,部分老旧项目可能不兼容
- 学习成本:如果你的团队对 Docker 不熟悉,从"直接装 LNMP"切换到容器化部署需要一定的适应期
- 自定义深度:环境变量覆盖了绝大多数场景,但如果你有非常特殊的 NGINX 或 FPM 配置需求,最终还是需要挂载自定义配置文件或基于此镜像二次构建
总结
serversideup/php 解决的不是"能不能用 Docker 跑 PHP"的问题,而是"怎么把 PHP 容器跑好"的问题。它把散落在各种博客、Stack Overflow 回答和团队内部文档里的最佳实践,打包成了一个开箱即用的镜像。
对于 PHP 开发者来说,它的价值在于:你不再需要成为 Docker 和 NGINX 专家才能部署生产级的 PHP 应用。几个环境变量,一个 compose.yml,就能得到一个安全、高性能、易维护的运行环境。
项目完全开源,维护活跃(最近一次更新就在 2026 年 7 月),社区规模也在持续增长。如果你正在为 PHP 容器化部署头疼,或者想给现有的 Docker 方案做一次升级,值得认真试一试。
相关链接:
- 项目官网:https://serversideup.net/open-source/docker-php
- GitHub:https://github.com/serversideup/docker-php
- Docker Hub:https://hub.docker.com/r/serversideup/php
- 文档:https://serversideup.net/open-source/docker-php/docs/getting-started
- 社区 Discord:https://serversideup.net/discord