CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
Docker 运行 PHP php-fpm + nginx 容器组合。
Docker 运行 PHP
这一课你将能做到
独立搭建 php:8.2-fpm + nginx 两个容器,用 docker-compose 一键起停
用官方镜像的 docker-php-ext-install 机制补装 pdo_mysql 等扩展
看懂 nginx 与 php-fpm 之间的 FastCGI 握手配置,排查 502/文件找不到类问题
为什么要学
「我这能跑,服务器跑不了」是 PHP 老项目的高频惨剧——本机 PHP 7.4 服务器 8.2、扩展缺一半、php.ini 天差地别。Docker 把 PHP 版本、扩展、配置、nginx 全部固化进镜像,开发/测试/生产三环境一致。而且 php-fpm + nginx 双容器架构是 PHP 生产部署的通用形态,理解它就理解了所有 PHP 部署方案(宝塔、K8s、Swoole 例外)的底层逻辑。
人话:nginx 是「前台接待」,只收 HTTP;php-fpm 是「后厨」,只跑 PHP;两者通过 FastCGI 协议传菜单和菜。Docker 就是给每个角色一个独立的标准厨房。
先建立图像
两个容器的拓扑与端口:
┌─────────────┐ FastCGI(TCP 9000) ┌──────────────────┐
│ nginx:alpine │ ────────────────────► │ php:8.2-fpm │
│ 容器 80 端口 │ fastcgi_pass php:9000│ 容器 9000 端口 │
└──────┬──────┘ │ 执行 .php 脚本 │
│ 映射宿主机 8080 └────────┬─────────┘
▼ │ 同一网络
浏览器 localhost:8080 代码目录(volume 双方共享)
关键认知:两个容器必须共享同一份代码目录 ——nginx 找静态文件,fpm 执行脚本,路径必须一致;fpm 容器的 9000 端口不映射到宿主机,只在 compose 网络内互通。
跟着做
步骤 1:项目结构
php-docker/
├── docker-compose.yml
├── php/
│ └── Dockerfile
├── nginx/
│ └── default.conf
├── db/
│ └── init.sql
└── src/
└── index.php
步骤 2:编写 Dockerfile(补装扩展)
php/Dockerfile:
FROM php:8.2-fpm
# 官方镜像内置的扩展安装助手
RUN docker-php-ext-install pdo pdo_mysql
# 常用扩展备查:
# docker-php-ext-install pdo_mysql opcache bcmath gd zip
# pecl install redis && docker-php-ext-enable redis
# 生产建议的自定义配置(可选)
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
php:8.2-fpm 镜像默认只有核心扩展,pdo_mysql 必须手动装——这是新手第一个坑。
步骤 3:nginx 站点配置
nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html; # 必须与 fpm 容器的代码路径一致
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass php:9000; # 服务名 = compose 里的服务名
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
SCRIPT_FILENAME 是 fpm 定位脚本的唯一依据,$document_root 必须展开成与 fpm 容器内一致的绝对路径——路径不一致就是经典的 File not found。
步骤 4:docker-compose 编排 services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./src:/var/www/html # 共享代码
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
php:
build: ./php # 用 Dockerfile 构建
volumes:
- ./src:/var/www/html # 同一份代码
environment:
DB_HOST: db
DB_NAME: demo
DB_USER: root
DB_PASS: secret
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: demo
volumes:
- db-data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
volumes:
db-data:
CREATE TABLE IF NOT EXISTS tasks (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(50) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
步骤 5:验证 <?php
declare(strict_types=1);
header('Content-Type: text/html; charset=utf-8');
$pdo = new PDO(
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4',
getenv('DB_HOST') ?: 'db', getenv('DB_NAME') ?: 'demo'),
getenv('DB_USER') ?: 'root',
getenv('DB_PASS') ?: 'secret',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false],
);
$pdo->prepare('INSERT INTO tasks (title) VALUES (?)')->execute(['Docker 化成功']);
$rows = $pdo->query('SELECT id, title FROM tasks ORDER BY id DESC LIMIT 5')->fetchAll();
echo '<h1>PHP ' . PHP_VERSION . ' + Nginx + MySQL</h1>';
foreach ($rows as $r) {
echo "<p>#{$r['id']} " . htmlspecialchars($r['title'], ENT_QUOTES) . '</p>';
}
docker compose up -d --build
docker compose ps # 三个容器 Up
# 浏览器打开 http://localhost:8080
docker compose logs php # 排错第一入口
docker compose down # 停止(-v 连数据卷一起删)
讲清楚要点
cgi → fastcgi → php-fpm 演进 :CGI 每请求 fork 进程;FastCGI 进程常驻循环处理;php-fpm 是 FastCGI 的 PHP 进程管理器(主进程 + 可配置的 worker 池,pm=dynamic 按负载伸缩)。容器里一个容器跑一个角色,职责与 Java 的「一个 jar 一个容器」一致。
FastCGI 三要素 :协议端口(9000)、fastcgi_pass 指向 fpm 服务名、SCRIPT_FILENAME 告诉 fpm 执行哪个文件。nginx 与 fpm 不共享内存,只共享这份「菜单」——所以代码目录的双边挂载路径必须逐字符一致。
扩展安装走官方机制 :docker-php-ext-install(编译核心/捆绑扩展)、docker-php-ext-enable(启用 pecl 装的扩展)、pecl install(第三方,如 redis)。别去改 php.ini 加 extension=xxx——镜像里 .so 根本没编译。
开发与生产的差异 :开发用 volume 挂源码即改即生效;生产把代码 COPY 进镜像(不可变产物)+ 只读挂配置,锁定 tag 而非 latest,fpm 配置 pm.max_children 按 内存/单进程开销 计算。
你可能会懵的地方
为什么 PHP 不像 Java 一样一个容器全包?
PHP 的执行模型是「Web 服务器收请求 → 转给 fpm 进程跑脚本」,语言本身不含 HTTP 服务器(内置 php -S 仅开发用),所以 nginx 与 fpm 天然是两个进程,容器化时顺势拆成两个容器。Swoole/RoadRunner 把 HTTP 服务器内置进 PHP 后,才能单容器部署。
fpm 容器的 9000 端口要映射到宿主机吗?
不要。它只与 nginx 在 compose 网络内通信,映射出去等于把 PHP 解释器直接暴露给外网(可被 FastCGI 协议直连攻击)。只在 nginx 需要与 fpm 跨主机部署时才做端口暴露并加 ACL。
502 Bad Gateway 怎么定位?
三个方向:fpm 容器没起来(docker compose ps 看 Up)、fastcgi_pass 服务名/端口写错(网络不通)、fpm pm.max_children 打满。先 docker compose logs php,再 docker compose exec web ping php 验证网络。
改了 PHP 代码不生效?
开发模式 volume 挂载理论上即改即生效,不生效多是 opcache 缓存了脚本——开发环境 php.ini 加 opcache.enable=0,或 docker compose restart php。
容器里 PHP 时区/中文乱码?
镜像默认 UTC。php.ini 设 date.timezone = Asia/Shanghai,Dockerfile 里 RUN apt-get update && apt-get install -y locales 配置 locale;MySQL 连接串带 charset=utf8mb4。
错法对照 错法 问题 正解 直接改 php.ini 加 extension=redis.so .so 没编译,fpm 起不来 pecl install redis && docker-php-ext-enable redisnginx 与 fpm 挂载了不同路径 File not found(fpm 找不到脚本) 双方 root 路径完全一致 映射 fpm 9000 到宿主机 PHP 解释器暴露公网 仅 compose 内网互通 生产用 volume 挂源码 + latest 镜像 环境漂移,不可回滚 代码打进镜像,锁定版本 tag docker compose up 后改 Dockerfile 不重建改动无效 docker compose up -d --buildMySQL 数据不挂 named volume 容器删除数据清零 db-data 卷持久化 /var/lib/mysql
卡点急救
页面 502 :docker compose ps 确认 php 容器 Up;logs php 看是否因扩展/配置崩溃;核对 fastcgi_pass php:9000 服务名拼写。
File not found(nginx 404 但文件存在) :fpm 收到 SCRIPT_FILENAME 后在自己容器里找文件——检查两个容器的挂载路径是否一致,docker compose exec php ls /var/www/html 直接验证。
连接数据库报 Connection refused :host 写 db(服务名)不是 127.0.0.1(指向容器自己);确认 db 容器健康,且应用启动晚于 MySQL 初始化(depends_on + 重试)。
面试常问
CGI、FastCGI、PHP-FPM 的关系?
CGI 是「每请求 fork 一个解释器进程」的古老协议,开销巨大;FastCGI 协议改为进程常驻、循环处理请求;PHP-FPM 是 FastCGI 协议的 PHP 实现,提供主进程管理、worker 进程池(static/dynamic/ondemand)、慢日志、平滑 reload,是 Nginx 之外 PHP 生产化的标准搭档。
为什么 nginx 和 php-fpm 之间常用 TCP 9000 而不是 unix socket?
unix socket 同机性能更高、少一层 TCP 开销;跨机/跨容器(K8s、compose 分容器)必须走 TCP。容器化场景两者网络隔离,TCP 9000 是通用选择,同 Pod 内可用共享 volume 的 unix socket。
Docker 里如何给 PHP 镜像添加扩展?
用官方镜像内置助手:核心与捆绑扩展用 docker-php-ext-install pdo_mysql opcache;需要参数的扩展先 docker-php-ext-configure gd --with-freetype 再 install;第三方扩展走 pecl install xxx && docker-php-ext-enable xxx,最后 docker-php-ext-enable 写入 ini。构建产物进镜像层,实现环境固化。
自测三问
fastcgi_pass php:9000 里的 php 是什么?由 compose 的哪个机制解析?
fpm 容器返回 File not found 时,第一步检查什么?
开发环境代码热更新与生产不可变镜像,volume 策略分别怎么定?
小结一句话 PHP 容器化的标准形态是 nginx(接待)+ php-fpm(后厨)双容器经 FastCGI 协作、volume 共享代码、官方助手补扩展——一份 compose 文件固化整个环境。
延伸