第8章 8.4 部署:Nginx + PHP-FPM + Docker
🎯 开场:为什么你写的 PHP 代码只能在自己电脑上跑?
上一章我们聊了 XSS、SQL 注入、CSRF 这些安全问题,你已经知道怎么保护你的代码不被黑客攻击了。但现在有个更现实的问题——
你的代码怎么让别人也能访问?
想象一下:你写了一个很棒的博客系统,在自己电脑上跑得飞起。但你想让全国人民都能看你的博客,总不能把电脑寄给他们吧?你需要把它「部署」到一台真正的服务器上,让 24 小时开着机、挂着网。
这就好比你做了一桌好菜(代码),现在要把它端上宴席(服务器),需要考虑:
- 怎么让客人(用户)能点到菜(访问你的网站)?
- 怎么同时招待 1000 个人而不手忙脚乱?
- 怎么让厨房(服务器)保持整洁、不影响做菜效率?
这一章我们就来解决这些问题。学完之后,你就能把自己写的 PHP 代码部署到服务器上,让全世界都能访问你的作品!
🧱 基础:三个工具的分工与合作
Nginx:门口的迎宾员
是什么? Nginx 是一个「反向代理服务器」\n\n
\n\n
\n\n,你可以把它想象成餐厅门口的迎宾员。
为什么要用? 用户的请求先到 Nginx,它来决定:这个请求该发给谁?静态文件(图片/CSS/JS)直接返回,动态请求(需要 PHP 处理的)转发给 PHP-FPM。如果没有 Nginx,用户直接找 PHP,PHP 会忙死。
怎么用? Nginx 的配置看起来是这样的:
server {
listen 80;
server_name example.com;
# 静态文件直接返回,不打扰 PHP
location / {
root /var/www/html;
index index.html;
}
# 动态请求转发给 PHP-FPM
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
include fastcgi_params;
}
}
这段配置的意思是:80 端口收到的请求,如果是 .php 结尾的,就发给 127.0.0.1:9000 的 PHP-FPM 处理,其他文件直接从我存储的地方返回。
PHP-FPM:厨房里的大厨
是什么? PHP-FPM 全称是「PHP FastCGI Process Manager」,你可以把它想象成餐厅里的大厨。
为什么要用? 以前 PHP 是「来人即做,做完即弃」,每次请求都要启动一次,做完就销毁,效率很低。PHP-FPM 相当于养了一批固定的大厨(进程池),随时待命,有请求来了直接上锅,不用再启动厨师,省时省力。
怎么用? PHP-FPM 有一个配置文件 www.conf,里面可以设置:
; 最大几个厨师同时上班
pm.max_children = 5
; 一开始先启动几个厨师
pm.start_servers = 2
; 闲着的时候最少留几个
pm.min_spare_servers = 1
; 忙的时候最多能招临时工
pm.max_spare_servers = 3
这几行配置的意思是:正常情况有 2 个大厨上班,最少留 1 个,最多 5 个同时干活。请求多的时候自动扩展,少的时候自动收缩。
Docker:标准化的集装箱
是什么? Docker 是一个容器化工具,你可以把它想象成标准集装箱。
为什么要用? 以前部署头疼的地方在于:这台服务器能跑,换一台可能就不行了——版本不一样、依赖缺失、各种环境问题。Docker 解决了这个问题,它把代码、环境、依赖全部打包成一个「集装箱」,这个集装箱在哪都能跑,一模一样。
怎么用? 一个简单的 PHP + Nginx + MySQL 部署可以用 docker-compose.yml:
version: '3'
services:
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./html:/var/www/html
depends_on:
- php
php:
image: php:8.2-fpm
volumes:
- ./html:/var/www/html
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: myapp
这段配置定义了三个「集装箱」:Nginx 处理请求,PHP-FPM 处理 PHP 代码,MySQL 存储数据。它们通过网络相互通信。
🔥 实战:用 Python 模拟理解部署流程
什么?不是说 PHP 部署吗,怎么用 Python?
别急,这里我用 Python 写几个小脚本,帮你真正理解 Nginx、PHP-FPM、Docker 之间是怎么配合工作的。代码都可以直接跑。
项目 1:模拟 Nginx 的请求分流
"""
模拟 Nginx 请求分流:
用户发来请求,Nginx 根据 URL 规则决定:静态文件还是动态请求?
"""
def nginx_router(path):
"""模拟 Nginx 的路由规则"""
# 静态文件后缀列表
static_extensions = ('.html', '.css', '.js', '.png', '.jpg', '.gif')
if path.endswith(static_extensions):
return {
'type': 'static',
'handler': 'nginx_direct',
'message': f'静态文件 {path},Nginx 直接返回'
}
elif path.endswith('.php'):
return {
'type': 'dynamic',
'handler': 'php_fpm',
'message': f'动态请求 {path},转发给 PHP-FPM 处理'
}
else:
return {
'type': 'dynamic',
'handler': 'php_fpm',
'message': f'{path},转发给 PHP-FPM 处理'
}
# 测试几个请求
test_paths = ['/index.html', '/style.css', '/article.php', '/api/users']
for path in test_paths:
result = nginx_router(path)
print(f"请求: {path}")
print(f" -> {result['message']}")
print()
预期输出:
请求: /index.html
-> 静态文件 /index.html,Nginx 直接返回
请求: /style.css
-> 静态文件 /style.css,Nginx 直接返回
请求: /article.php
-> 动态请求 /article.php,转发给 PHP-FPM 处理
请求: /api/users
-> /api/users,转发给 PHP-FPM 处理
这个脚本模拟了 Nginx 的核心功能:根据 URL 决定请求该谁处理。静态文件 Nginx 直接返回,PHP 请求转发给 PHP-FPM。
项目 2:模拟 PHP-FPM 进程池管理
"""
模拟 PHP-FPM 进程池:
根据请求量动态调整进程数量,演示进程管理机制
"""
import time
class PHPPool:
def __init__(self, min_servers=2, max_servers=5):
self.min_servers = min_servers
self.max_servers = max_servers
self.active_processes = min_servers
self.total_processed = 0
def status(self):
return f"[状态] 活跃进程: {self.active_processes}/{self.max_servers}, 已处理请求: {self.total_processed}"
def process_request(self, request_id):
"""处理一个请求,根据负载调整进程数"""
# 模拟处理时间
time.sleep(0.1)
self.total_processed += 1
# 负载高时扩展进程
if self.total_processed % 5 == 0 and self.active_processes < self.max_servers:
self.active_processes += 1
print(f" 📈 负载上升!扩展进程数到 {self.active_processes}")
# 负载低时收缩进程
if self.total_processed % 15 == 0 and self.active_processes > self.min_servers:
self.active_processes -= 1
print(f" 📉 负载下降!收缩进程数到 {self.active_processes}")
return f"请求 #{request_id} 处理完成"
# 模拟处理 20 个请求
pool = PHPPool(min_servers=2, max_servers=5)
print("=== PHP-FPM 进程池模拟 ===\n")
print(pool.status())
for i in range(1, 21):
result = pool.process_request(i)
if i % 5 == 0: # 每 5 个请求显示一次状态
print(result)
print(pool.status())
print()
print("\n最终状态:", pool.status())
预期输出:
=== PHP-FPM 进程池模拟 ===
[状态] 活跃进程: 2/5, 已处理请求: 0
请求 #5 处理完成
📈 负载上升!扩展进程数到 3
[状态] 活跃进程: 3/5, 已处理请求: 5
请求 #10 处理完成
📈 负载上升!扩展进程数到 4
[状态] 活跃进程: 4/5, 已处理请求: 10
请求 #15 处理完成
📈 负载上升!扩展进程数到 5
[状态] 活跃进程: 5/5, 已处理请求: 15
请求 #20 处理完成
📉 负载下降!收缩进程数到 4
[状态] 活跃进程: 4/5, 已处理请求: 20
最终状态: [状态] 活跃进程: 4/5, 已处理请求: 20
这个脚本展示了 PHP-FPM 的核心优势:进程池会根据负载自动扩缩,不需要每次请求都启动新进程。
项目 3:模拟 Docker 容器编排
"""
模拟 Docker Compose 容器编排:
定义服务、配置端口映射、模拟容器间通信
"""
import yaml
# 定义 docker-compose.yml 配置
docker_config = {
'version': '3',
'services': {
'nginx': {
'image': 'nginx:latest',
'ports': ['80:80'],
'depends_on': ['php'],
'volumes': ['./html:/var/www/html']
},
'php': {
'image': 'php:8.2-fpm',
'depends_on': ['mysql'],
'volumes': ['./html:/var/www/html']
},
'mysql': {
'image': 'mysql:8.0',
'environment': {
'MYSQL_ROOT_PASSWORD': 'secret',
'MYSQL_DATABASE': 'myapp'
}
}
}
}
class ContainerManager:
def __init__(self, config):
self.config = config
self.running_containers = {}
def deploy(self):
"""部署所有服务"""
print("=== Docker 容器部署模拟 ===\n")
# 按依赖顺序启动(先启动被依赖的)
if 'mysql' in self.config['services']:
self._start_container('mysql')
if 'php' in self.config['services']:
self._start_container('php')
if 'nginx' in self.config['services']:
self._start_container('nginx')
print("\n所有容器启动完成!")
self.show_status()
def _start_container(self, name):
"""启动单个容器"""
service = self.config['services'][name]
self.running_containers[name] = {
'image': service['image'],
'status': 'running',
'ports': service.get('ports', [])
}
print(f"✅ 启动容器: {name} (镜像: {service['image']})")
def show_status(self):
"""显示所有容器状态"""
print("\n--- 容器状态 ---")
for name, info in self.running_containers.items():
ports = ', '.join(info['ports']) if info['ports'] else '无端口映射'
print(f" {name}: {info['status']} | 端口: {ports}")
def simulate_request(self, url):
"""模拟用户请求流程"""
print(f"\n🌐 用户访问: {url}")
print(f" 1. Nginx 接收请求")
if url.endswith('.php'):
print(f" 2. Nginx 转发给 PHP 容器")
print(f" 3. PHP 容器处理请求,连接 MySQL 查询数据")
print(f" 4. 返回响应给 Nginx")
else:
print(f" 2. Nginx 直接从卷挂载目录返回静态文件")
print(f" 5. 用户看到结果")
def cleanup(self):
"""清理所有容器"""
print("\n🧹 停止并清理所有容器...")
for name in self.running_containers:
print(f" 删除容器: {name}")
self.running_containers.clear()
print("清理完成!")
# 运行模拟
manager = ContainerManager(docker_config)
manager.deploy()
# 模拟用户访问
manager.simulate_request('/index.html')
manager.simulate_request('/api/blog/posts.php')
# 清理
manager.cleanup()
预期输出:
=== Docker 容器部署模拟 ===
✅ 启动容器: mysql (镜像: mysql:8.0)
✅ 启动容器: php (镜像: php:8.2-fpm)
✅ 启动容器: nginx (镜像: nginx:latest)
所有容器启动完成!
--- 容器状态 ---
mysql: running | 端口: 无端口映射
php: running | 端口: 无端口映射
nginx: running | 端口: 80:80
🌐 用户访问: /index.html
1. Nginx 接收请求
2. Nginx 直接从卷挂载目录返回静态文件
5. 用户看到结果
🌐 用户访问: /api/blog/posts.php
1. Nginx 接收请求
2. Nginx 转发给 PHP 容器
3. PHP 容器处理请求,连接 MySQL 查询数据
4. 返回响应给 Nginx
5. 用户看到结果
🧹 停止并清理所有容器...
删除容器: mysql
删除容器: php
删除容器: nginx
清理完成!
这个脚本完整模拟了 Docker Compose 的工作流程:定义服务、启动容器、容器间通信、清理资源。
💪 进阶:新手最容易踩的坑
坑 1:Nginx 转发 PHP 请求时路径写错
# ❌ 错误示例:SCRIPT_FILENAME 路径不完整
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name; # 少了 root 路径
include fastcgi_params;
}
# ✅ 正确示例:完整路径
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
include fastcgi_params;
}
区别:Nginx 把请求转发给 PHP-FPM 时,必须告诉 PHP-FPM「文件的完整路径是什么」。少了 /var/www/html 这个根路径,PHP-FPM 就找不到文件,会报 404 或者空白页。
坑 2:PHP-FPM 进程数设太多
# ❌ 错误示例:贪心设太大
pm.max_children = 100 # 假设服务器只有 2G 内存,每个 PHP 进程占用 50M,这里就要 5G
# ✅ 正确示例:根据服务器内存合理估算
# 2G 内存服务器,每个 PHP 进程预留 50M,最多同时跑 30 个
pm.max_children = 30
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
区别:进程数不是越多越好。每个 PHP-FPM 进程都要吃内存。设太多,服务器会 swap 卡死,反而更慢。
坑 3:Docker 容器内文件权限问题
# ❌ 错误示例:构建镜像时用 root 用户
FROM php:8.2-fpm
USER root
RUN apt-get update && apt-get install -y something
# ✅ 正确示例:用 www-data 用户运行
FROM php:8.2-fpm
RUN apt-get update && apt-get install -y something
USER www-data
区别:Docker 容器里默认是 root 用户,但 Nginx 和 PHP-FPM 通常用 www-data 用户运行。如果文件是 root 的,www-data 没权限读写,会导致「上传文件失败」「写入不了缓存」等问题。
坑 4:docker-compose 依赖顺序没生效
# ❌ 错误示例:以为 depends_on 就是「等它完全启动好」
services:
nginx:
depends_on:
- php
php:
depends_on:
- mysql
# ✅ 正确示例:需要加 condition 或健康检查
services:
nginx:
depends_on:
php:
condition: service_healthy
php:
depends_on:
mysql:
condition: service_started # mysql 启动不等于就绪
区别:depends_on 只是「等容器启动」,不等「服务就绪」。MySQL 启动可能要 30 秒,这期间 PHP 连上去会失败。生产环境需要加健康检查。
坑 5:生产环境暴露数据库密码
# ❌ 错误示例:密码写死在配置文件里
environment:
MYSQL_ROOT_PASSWORD: "123456"
MYSQL_PASSWORD: "123456"
# ✅ 正确示例:用 .env 文件或 Docker Secret
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
# .env 文件(不要提交到 Git)
# MYSQL_ROOT_PASSWORD=your_secret_password_here
区别:密码明文写在 docker-compose.yml 里,Git 一提交就泄露了。用环境变量或 Docker Secret 更安全。
调试技巧:查看容器日志
# 查看单个容器日志
docker logs -f container_name
# 查看最近 100 行日志
docker logs --tail 100 container_name
# 同时查看多个容器日志
docker logs -f nginx php mysql
# 在 docker-compose.yml 里加日志配置
services:
nginx:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
使用场景:网站打不开?先看日志。docker logs -f nginx 看看 Nginx 有没有收到请求,docker logs -f php 看看 PHP 有没有报错。
✏️ 练习题
练习 1(3 分钟):添加静态文件类型
在项目 1 的 nginx_router 函数里添加对 .svg 和 .woff2 的支持,这两种也是静态文件。
- 输入:
'/font/icon.woff2' - 预期输出:
静态文件 /font/icon.woff2,Nginx 直接返回 - 提示:修改
static_extensions元组即可
练习 2(3 分钟):判断请求来源
在项目 1 基础上添加一个功能:如果是 /admin 开头的路径,不管什么后缀都算「动态请求」,需要登录验证。
- 输入:
'/admin/settings.php' - 预期输出:
动态请求 /admin/settings.php,转发给 PHP-FPM 处理 - 提示:先判断路径是否以
/admin开头
练习 3(5 分钟):调整 PHP-FPM 进程池参数
修改项目 2 的 PHPPool 类,把 min_servers 改成 3,max_servers 改成 8,然后跑 30 个请求看看进程数怎么变化。
- 预期输出:进程数在 3-8 之间波动
- 提示:记得改构造函数参数
练习 4(7 分钟):添加 Redis 缓存服务
在项目 3 的 docker_config 里添加一个 redis 服务,然后用 ContainerManager 部署 4 个容器。
- redis 配置:
image: redis:7-alpine,端口:6379:6379 - 预期输出:4 个容器都启动成功
- 提示:Redis 不被其他服务依赖,应该最先启动
练习 5(10 分钟):分析报错
下面这段 Nginx 配置有问题,用户访问 .php 文件时一直报 502 Bad Gateway。请找出问题并修复。
server {
listen 80;
server_name example.com;
root /var/www/html;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
- 提示:检查
SCRIPT_FILENAME的路径拼接是否正确 - 预期:修复后
.php请求能正常转发
作业:做一个「一键部署配置生成器」
写一个 Python 脚本,根据用户输入的项目信息,自动生成 Nginx 配置文件和 docker-compose.yml 文件。
- 需求描述:用户输入项目名、域名、PHP 版本,程序生成完整的部署配置
- 功能点:
1) 生成nginx.conf(包含 server 块、location 规则)
2) 生成docker-compose.yml(包含 nginx、php、mysql 三个服务)
3) 生成README.md(包含部署步骤说明) - 加分项:
1) 支持选择是否添加 Redis 缓存
2) 支持选择 MySQL 版本(5.7 或 8.0) - 验收标准:生成的文件内容正确,放入对应目录后能跑起来
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结
本文学到的 3 个核心点:
- Nginx 是迎宾员,负责接收所有请求,静态文件直接返回,动态请求转发给 PHP-FPM
- PHP-FPM 是大厨,用进程池方式处理请求,比传统的「来一个请求起一个进程」高效得多
- Docker 是集装箱,把代码、环境、依赖打包,保证「在我电脑上能跑,在你电脑上也能跑」
延伸学习资源:
- Nginx 官方文档(配置参考最权威)
- Docker 官方教程(互动式学习,从头学起)
- 《高性能 Nginx》- 深入理解 Nginx 架构和性能优化
互动钩子:
你在部署 PHP 项目时遇到过什么奇葩问题?502 Bad Gateway?404 Not Found?还是 MySQL 连接不上?评论区聊聊,老粉优先回复!
📢 下一章预告:Nginx、PHP-FPM、Docker 都会用了,但你可能发现——代码写得不够「优雅」。下一章我们来聊点轻松的:PHP 8 的新特性速览,帮你写出更简洁、更高效的 PHP 代码!

评论(0)