CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
PHP Web 入门/课时/Docker 运行 PHP
RawBlame
课时目录12 节

PHP Web 入门

  1. 01PHP 是什么
  2. 02基础语法
  3. 03函数与命名空间
  4. 04面向对象
  5. 05Composer 依赖
  6. 06Web 请求处理
  7. 07PDO 与 MySQL
  8. 08REST API 风格
  9. 09PHP 8 新特性
  10. 10Docker 运行 PHP
  11. 11PHP vs Java 选型
  12. 12综合练习

Docker 运行 PHP

php-fpm + nginx 容器组合。

25 分钟

Docker 运行 PHP

这一课你将能做到

  1. 独立搭建 php:8.2-fpm + nginx 两个容器,用 docker-compose 一键起停
  2. 用官方镜像的 docker-php-ext-install 机制补装 pdo_mysql 等扩展
  3. 看懂 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 连数据卷一起删)

讲清楚要点

  1. cgi → fastcgi → php-fpm 演进:CGI 每请求 fork 进程;FastCGI 进程常驻循环处理;php-fpm 是 FastCGI 的 PHP 进程管理器(主进程 + 可配置的 worker 池,pm=dynamic 按负载伸缩)。容器里一个容器跑一个角色,职责与 Java 的「一个 jar 一个容器」一致。
  2. FastCGI 三要素:协议端口(9000)、fastcgi_pass 指向 fpm 服务名、SCRIPT_FILENAME 告诉 fpm 执行哪个文件。nginx 与 fpm 不共享内存,只共享这份「菜单」——所以代码目录的双边挂载路径必须逐字符一致。
  3. 扩展安装走官方机制:docker-php-ext-install(编译核心/捆绑扩展)、docker-php-ext-enable(启用 pecl 装的扩展)、pecl install(第三方,如 redis)。别去改 php.ini 加 extension=xxx——镜像里 .so 根本没编译。
  4. 开发与生产的差异:开发用 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

卡点急救

  1. 页面 502:docker compose ps 确认 php 容器 Up;logs php 看是否因扩展/配置崩溃;核对 fastcgi_pass php:9000 服务名拼写。
  2. File not found(nginx 404 但文件存在):fpm 收到 SCRIPT_FILENAME 后在自己容器里找文件——检查两个容器的挂载路径是否一致,docker compose exec php ls /var/www/html 直接验证。
  3. 连接数据库报 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。构建产物进镜像层,实现环境固化。

自测三问

  1. fastcgi_pass php:9000 里的 php 是什么?由 compose 的哪个机制解析?
  2. fpm 容器返回 File not found 时,第一步检查什么?
  3. 开发环境代码热更新与生产不可变镜像,volume 策略分别怎么定?

小结一句话

PHP 容器化的标准形态是 nginx(接待)+ php-fpm(后厨)双容器经 FastCGI 协作、volume 共享代码、官方助手补扩展——一份 compose 文件固化整个环境。

延伸

  • PHP 官方 Docker 镜像文档
  • 下一课:PHP vs Java 选型——两门语言的全维度专业对比。
加载本节练习…
← 上一课PHP 8 新特性下一课 →PHP vs Java 选型