Docker: Phase 1 Mini Project
最后更新:2026-08-26
现在把前 5 课的知识串起来——从零部署一个完整的 LEMP 栈(Linux + Nginx + MySQL + PHP),这是 Phase 1 的综合验收项目。
1. 你将学到
- 多容器协作架构设计
- 数据卷持久化策略
- 容器间网络通信
- 环境变量配置管理
- 容器故障排查与修复
2. 一个后端开发者的真实故事
(1) 痛点:手动搭建开发环境耗时半天
Alice 要为公司的内部管理系统搭建开发环境:Nginx 提供静态服务、PHP-FPM 处理动态请求、MySQL 存储数据。手动安装需要:config Nginx 虚拟主机、安装 PHP-FPM 和扩展、安装并初始化 MySQL、配置三者之间的通信路径。每一步都可能踩坑,同事用了一天才搭好。
(2) Docker 多容器部署的解法
Alice 用 Docker 在 10 分钟内搭建了完整的 LEMP 栈——三个容器、一个网络、两个数据卷。
BASH
# Architecture overview: Nginx → PHP-FPM → MySQL
docker network create lemp-net
docker volume create mysql-data
(3) 收益:10 分钟 vs 半天
手动部署半天(且不可复用),Docker 部署 10 分钟(脚本可复用,团队共享)。新成员入职从 1 天环境搭建缩短到 15 分钟。
3. LEMP 架构设计
(1) 架构总览
graph TB
USER["User Browser<br/>:8080"] --> NGX["Nginx Container<br/>:80<br/>Static files + PHP proxy"]
NGX -->|"FastCGI :9000"| PHP["PHP-FPM Container<br/>:9000<br/>PHP processing"]
PHP -->|"MySQL :3306"| DB["MySQL Container<br/>:3306<br/>Database"]
DB --> VOL["Named Volume<br/>mysql-data<br/>Persistent storage"]
PHP --> PHPV["Bind Mount<br/>./src → /var/www/html<br/>PHP source code"]
NGX --> NGXV["Bind Mount<br/>./nginx.conf<br/>Nginx config"]
(2) 手动部署 vs Docker 部署对比
| 维度 | 手动部署 | Docker 部署 |
|---|---|---|
| 耗时 | 4-8 小时 | 10 分钟 |
| 可重复性 | 低(依赖操作者经验) | 高(脚本/命令可复用) |
| 可移植性 | 低(绑定特定 OS) | 高(镜像跨平台一致) |
| 环境隔离 | 无(共享系统环境) | 完全隔离 |
| 回滚 | 困难(卸载重装) | 简单(删除容器重建) |
| 团队共享 | 写文档+照做 | 共享命令/脚本 |
4. 准备工作:网络与卷
(1) 创建自定义网络
容器间通信需要一个自定义 bridge 网络——默认 bridge 网络不支持容器名 DNS 解析。
BASH
# Create a custom bridge network for LEMP stack
docker network create lemp-net
# Verify
docker network ls | grep lemp-net
(2) 创建数据卷
MySQL 数据需要持久化——容器删除后数据不能丢失。
BASH
# Create a named volume for MySQL data
docker volume create mysql-data
# Verify
docker volume ls | grep mysql-data
(3) 卷挂载策略对比
| 策略 | syntax | 适用场景 | 持久性 |
|---|---|---|---|
| Named Volume | -v mysql-data:/var/lib/mysql |
database、持久化存储 | ✅ 容器删除后保留 |
| Bind Mount | -v ./src:/var/www/html |
开发时代码同步 | ✅ 映射主机目录 |
| tmpfs | --tmpfs /tmp |
临时数据、敏感信息 | ❌ 容器停止后消失 |
5. deploy MySQL 容器
▶ 示例:启动 MySQL 容器(难度⭐⭐)
BASH
# Start MySQL 8.0 with persistent storage
docker run -d \
--name lemp-mysql \
--network lemp-net \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
--restart=unless-stopped \
mysql:8.0
💡 提示: 先启动 MySQL,等它初始化完成(约 30 秒)再启动其他容器。可以用
docker logs -f lemp-mysql 观察初始化进度,看到 "ready for connections" 就说明初始化完成。
(1) 验证 MySQL 运行
BASH
# Check MySQL is running
docker ps --filter name=lemp-mysql
# Connect to MySQL to verify
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e "SELECT 1;"
6. 部署 PHP-FPM 容器
(1) 准备 PHP 代码
先创建一个 PHP 测试文件和数据库连接测试文件。
BASH
# Create project directory
mkdir -p lemp-project/src
# Create PHP info page
cat > lemp-project/src/info.php << 'EOF'
<?php
phpinfo();
?>
EOF
# Create database connection test
cat > lemp-project/src/db-test.php << 'EOF'
<?php
$host = 'lemp-mysql';
$db = 'appdb';
$user = 'appuser';
$pass = 'apppass';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "Connected to MySQL successfully!<br>";
echo "MySQL version: " . $pdo->query("SELECT VERSION()")->fetchColumn();
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
?>
EOF
▶ 示例:启动 PHP-FPM 容器(难度⭐⭐)
BASH
# Start PHP-FPM container
docker run -d \
--name lemp-php \
--network lemp-net \
-v $(pwd)/lemp-project/src:/var/www/html \
--restart=unless-stopped \
php:8.2-fpm
📌 重点:
-v $(pwd)/lemp-project/src:/var/www/html 是 Bind Mount——本机代码目录映射到容器内,修改本机文件后容器内立即生效,适合开发调试。
7. 部署 Nginx 容器
(1) 准备 Nginx 配置
Nginx 需要配置 FastCGI 代理,将 PHP 请求转发给 PHP-FPM 容器。
BASH
# Create Nginx configuration
cat > lemp-project/nginx.conf << 'EOF'
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass lemp-php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
EOF
📌 重点:
fastcgi_pass lemp-php:9000 中的 lemp-php 是 PHP-FPM 容器名。因为它们在同一自定义网络 lemp-net 中,Docker DNS 自动将容器名解析为容器 IP。
▶ 示例:启动 Nginx 容器(难度⭐⭐)
BASH
# Start Nginx container
docker run -d \
--name lemp-nginx \
--network lemp-net \
-p 8080:80 \
-v $(pwd)/lemp-project/src:/var/www/html \
-v $(pwd)/lemp-project/nginx.conf:/etc/nginx/conf.d/default.conf \
--restart=unless-stopped \
nginx:1.25-alpine
8. 验证与测试
▶ 示例:访问 PHP 页面验证(难度⭐)
BASH
# Test PHP info page
curl http://localhost:8080/info.php | head -5
# Test database connection
curl http://localhost:8080/db-test.php
💻 输出:
TEXT
📖 仅展示
# curl http://localhost:8080/db-test.php
Connected to MySQL successfully!
MySQL version: 8.0.35
✅ 检查点: 看到 "Connected to MySQL successfully!" 说明三容器协作成功:Nginx → PHP-FPM → MySQL。
(1) 完整验证清单
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 三个容器都在运行 | docker ps |
3 个容器状态为 Up |
| PHP 页面正常 | curl localhost:8080/info.php |
返回 PHP 配置信息 |
| 数据库连接正常 | curl localhost:8080/db-test.php |
"Connected successfully" |
| 容器间 DNS 解析 | docker exec lemp-nginx ping -c 2 lemp-php |
能 ping 通 |
| 数据卷存在 | docker volume ls |
mysql-data 在列表中 |
9. 数据持久化验证
▶ 示例:模拟 MySQL 容器崩溃(难度⭐⭐⭐)
BASH
# 1. Create a test table with data
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e \
"CREATE TABLE test_items (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100)); INSERT INTO test_items (name) VALUES ('Alice'), ('Bob');"
# 2. Verify data exists
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e \
"SELECT * FROM test_items;"
# 3. Stop and remove the MySQL container
docker stop lemp-mysql && docker rm lemp-mysql
# 4. Recreate MySQL container with the same volume
docker run -d \
--name lemp-mysql \
--network lemp-net \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
--restart=unless-stopped \
mysql:8.0
# 5. Wait for MySQL to start, then verify data is still there
docker exec -it lemp-mysql mysql -uappuser -papppass appdb -e \
"SELECT * FROM test_items;"
💻 输出:
TEXT
📖 仅展示
+----+-------+
| id | name |
+----+-------+
| 1 | Alice |
| 2 | Bob |
+----+-------+
✅ 检查点: 容器删除重建后数据仍然存在——Named Volume 的持久化生效了。
10. 容器排序与依赖策略
| 策略 | method | 可靠性 |
|---|---|---|
| 手动按序启动 | MySQL → PHP → Nginx | 中(依赖经验) |
| 健康检查等待 | 等 MySQL 就绪再启动 PHP | 高(自动化) |
| restart 策略 | --restart=on-failure 让失败的服务自动重试 |
中(不保证顺序) |
| Docker Compose | depends_on + 健康检查(Phase 3 学习) |
高(声明式) |
💡 提示: 本课用手动启动顺序。Phase 3 会学到 Docker Compose 的
depends_on,实现自动化依赖管理。
11. 完整示例:一键部署 LEMP 栈脚本
BASH
# ============================================
# Complete walkthrough: Deploy LEMP stack from scratch
# Covers: network, volume, MySQL, PHP, Nginx
# ============================================
# --- Setup ---
# Create network and volume
docker network create lemp-net
docker volume create mysql-data
# Create project directory and files
mkdir -p lemp-project/src
cat > lemp-project/src/index.php << 'PHPEOF'
<?php
$host = 'lemp-mysql';
$db = 'appdb';
$user = 'appuser';
$pass = 'apppass';
try {
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
echo "<h1>LEMP Stack is working!</h1>";
echo "<p>MySQL version: " . $pdo->query("SELECT VERSION()")->fetchColumn() . "</p>";
} catch (PDOException $e) {
echo "<h1>Database connection failed</h1><p>" . $e->getMessage() . "</p>";
}
?>
PHPEOF
cat > lemp-project/nginx.conf << 'NGINXEOF'
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass lemp-php:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
NGINXEOF
# --- Deploy ---
# 1. Start MySQL (must be first)
docker run -d \
--name lemp-mysql \
--network lemp-net \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=appdb \
-e MYSQL_USER=appuser \
-e MYSQL_PASSWORD=apppass \
-v mysql-data:/var/lib/mysql \
--restart=unless-stopped \
mysql:8.0
# 2. Wait for MySQL to initialize (~30 seconds)
echo "Waiting for MySQL to initialize..."
sleep 30
# 3. Start PHP-FPM
docker run -d \
--name lemp-php \
--network lemp-net \
-v $(pwd)/lemp-project/src:/var/www/html \
--restart=unless-stopped \
php:8.2-fpm
# 4. Start Nginx
docker run -d \
--name lemp-nginx \
--network lemp-net \
-p 8080:80 \
-v $(pwd)/lemp-project/src:/var/www/html \
-v $(pwd)/lemp-project/nginx.conf:/etc/nginx/conf.d/default.conf \
--restart=unless-stopped \
nginx:1.25-alpine
# --- Verify ---
echo "Verifying deployment..."
docker ps
curl -s http://localhost:8080/index.php
💻 输出(节选):
TEXT
📖 仅展示
# docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
c1d2e3f4 nginx:1.25-alpine Up 5 min 0.0.0.0:8080->80/tcp lemp-nginx
b2c3d4e5 php:8.2-fpm Up 5 min 9000/tcp lemp-php
a1b2c3d4 mysql:8.0 Up 5 min 3306/tcp, 33060/tcp lemp-mysql
# curl http://localhost:8080/index.php
<h1>LEMP Stack is working!</h1><p>MySQL version: 8.0.35</p>
❓ 常见问题
Q 为什么分开启动不用 Compose?
A 这是 Phase 1 的综合实践,目的是让你理解每个容器是如何独立创建和连接的。Phase 3 会学到 Docker Compose,用一个 YAML 文件替代这些 docker run 命令。先手动再自动,理解更深。
Q Nginx 怎么连接 PHP-FPM?
A 通过 FastCGI 协议。Nginx 配置中
fastcgi_pass lemp-php:9000 指向 PHP-FPM 容器。lemp-php 是容器名,在同一自定义网络中 Docker DNS 自动解析为容器 IP,无需硬编码 IP 地址。Q 容器重启后数据还在吗?
A 用 Named Volume(
-v mysql-data:/var/lib/mysql)挂载的数据在容器删除后仍然存在。但 Bind Mount 的源代码(-v ./src:/var/www/html)是映射到主机目录的,只要主机目录不删,代码也不会丢。Q 怎么查哪个容器报错了?
A 三步排查法:①
docker ps -a 看哪个容器状态不是 Up;② docker logs <container> 查看错误日志;③ docker inspect <container> --format='{{.State.ExitCode}}' 查看退出码。常见:MySQL 未就绪时 PHP 连接报错,等 MySQL 初始化完成即可。Q 多个容器日志怎么一起看?
A
docker compose logs(Phase 3 学到)可以聚合查看。目前可用:① 分别 docker logs 每个容器;② 用 docker logs -f 开多个终端窗口同时监控;③ 将日志重定向到文件后合并查看。📖 小节
- LEMP 栈 = Linux + Nginx + MySQL + PHP,经典 Web 应用架构
- 自定义网络
lemp-net让容器间通过容器名 DNS 解析通信 - Named Volume 持久化数据库数据,Bind Mount 同步开发代码
- 启动顺序:MySQL(先初始化)→ PHP-FPM → Nginx(最后暴露端口)
fastcgi_pass lemp-php:9000实现容器间 FastCGI 通信- 数据持久化验证:删除 MySQL 容器后重建,Named Volume 中数据仍在
📝 作业
- 基础题(难度⭐):从零部署 LEMP 栈,访问
http://localhost:8080/index.php确认三个容器协作正常。 - 进阶题(难度⭐⭐):模拟 MySQL 容器崩溃(
docker stop lemp-mysql && docker rm lemp-mysql),重建后验证数据库数据是否持久化保留。 - 挑战题(难度⭐⭐⭐):添加 Adminer(数据库管理工具)容器连接到 MySQL,用 Web 界面管理数据库。提示:
docker run -d --name adminer --network lemp-net -p 8081:8080 adminer,访问http://localhost:8081,用 appuser/apppass 登录。