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 编排
docker-compose.yml:
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:
db/init.sql:
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:验证
src/index.php:
<?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 redis |
| nginx 与 fpm 挂载了不同路径 | File not found(fpm 找不到脚本) | 双方 root 路径完全一致 |
| 映射 fpm 9000 到宿主机 | PHP 解释器暴露公网 | 仅 compose 内网互通 |
| 生产用 volume 挂源码 + latest 镜像 | 环境漂移,不可回滚 | 代码打进镜像,锁定版本 tag |
docker compose up 后改 Dockerfile 不重建 | 改动无效 | docker compose up -d --build |
| MySQL 数据不挂 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 文件固化整个环境。
延伸
- PHP 官方 Docker 镜像文档
- 下一课:PHP vs Java 选型——两门语言的全维度专业对比。