适用人群 :已完成 Docker 基础安装与单容器运行,准备深入理解生产级容器化实践的开发者
目标 :掌握多阶段构建优化镜像体积、Volume 数据持久化、Nginx 反向代理、Docker Compose 多服务编排
前置要求 :已完成 M2 前半部分(Docker 安装、单阶段 Dockerfile、基础 Compose)
阅读时长:约 25 分钟(实操约 3-4 小时,建议分两天完成)
目录
- 为什么需要这些"遗漏项"?
- [多阶段构建:从 800MB 到 100MB 的优化艺术](#多阶段构建:从 800MB 到 100MB 的优化艺术 "#%E4%BA%8C%E5%A4%9A%E9%98%B6%E6%AE%B5%E6%9E%84%E5%BB%BA%E4%BB%8E-800mb-%E5%88%B0-100mb-%E7%9A%84%E4%BC%98%E5%8C%96%E8%89%BA%E6%9C%AF")
- [Volume 数据持久化:容器销毁,数据不丢](#Volume 数据持久化:容器销毁,数据不丢 "#%E4%B8%89volume-%E6%95%B0%E6%8D%AE%E6%8C%81%E4%B9%85%E5%8C%96%E5%AE%B9%E5%99%A8%E9%94%80%E6%AF%81%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%A2")
- [Docker 网络:容器之间如何"打电话"](#Docker 网络:容器之间如何"打电话" "#%E5%9B%9Bdocker-%E7%BD%91%E7%BB%9C%E5%AE%B9%E5%99%A8%E4%B9%8B%E9%97%B4%E5%A6%82%E4%BD%95%E6%89%93%E7%94%B5%E8%AF%9D")
- [Day 1 实战:多阶段构建 + Volume(后端容器化)](#Day 1 实战:多阶段构建 + Volume(后端容器化) "#%E4%BA%94day-1-%E5%AE%9E%E6%88%98%E5%A4%9A%E9%98%B6%E6%AE%B5%E6%9E%84%E5%BB%BA--volume%E5%90%8E%E7%AB%AF%E5%AE%B9%E5%99%A8%E5%8C%96")
- [Day 2 实战:前端容器化 + Nginx 反向代理(前后端分离)](#Day 2 实战:前端容器化 + Nginx 反向代理(前后端分离) "#%E5%85%ADday-2-%E5%AE%9E%E6%88%98%E5%89%8D%E7%AB%AF%E5%AE%B9%E5%99%A8%E5%8C%96--nginx-%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E5%89%8D%E5%90%8E%E7%AB%AF%E5%88%86%E7%A6%BB")
- 核心概念图解
- 常见问题排查(FAQ)
- 完成检查清单
一、为什么需要这些"遗漏项"?
在前半部分,你已经学会了安装 Docker、运行 Nginx 容器、编写单阶段 Dockerfile 和使用基础 Compose。但这些距离生产环境部署还有关键差距:
| 遗漏项 | 重要性 | 不掌握的后果 |
|---|---|---|
| 多阶段构建 | 🔴 核心 | 镜像体积 800MB+,上传/下载慢,攻击面大 |
| Volume 卷 | 🔴 核心 | 容器删除后数据全丢,数据库、日志无法持久化 |
| Network 网络 | 🟡 重要 | 容器间无法通信,微服务架构无法实现 |
| 前端 Dockerfile | 🟡 重要 | 只能部署后端,完整项目需要前后端分离 |
| Nginx 反向代理 | 🟡 重要 | 暴露后端端口不安全,前端无法直接调用 API |
depends_on |
🟡 重要 | 服务启动顺序混乱,前端先启而后端未就绪 |
docker exec |
🟢 常用 | 无法进入容器内部调试,排错困难 |
docker system prune |
🟢 常用 | 磁盘被无用镜像/容器占满 |
二、多阶段构建:从 800MB 到 100MB 的优化艺术
2.1 问题:单阶段镜像太臃肿
如果你用 SDK 镜像直接运行应用:
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0
COPY . .
RUN dotnet publish -c Release -o /app
ENTRYPOINT ["dotnet", "/app/MyApp.dll"]
镜像大小 :约 800MB+。因为 SDK 包含:
- 编译器(Roslyn)
- 调试工具
- 大量开发依赖(MSBuild、NuGet 缓存等)
生产环境不需要这些。
2.2 解决:多阶段构建(Multi-stage Build)
Docker 允许在一个 Dockerfile 中定义多个阶段,最终只保留最后一个阶段作为镜像:
dockerfile
# ============================================
# 阶段 1:编译(Build Stage)------ 用完即弃
# ============================================
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# ============================================
# 阶段 2:运行(Runtime Stage)------ 最终交付
# ============================================
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
效果对比:
| 指标 | 单阶段(SDK) | 多阶段(Runtime) |
|---|---|---|
| 镜像大小 | ~800MB | ~100MB |
| 启动速度 | 慢 | 快 |
| 攻击面 | 大(含编译工具) | 小(仅运行环境) |
| 网络传输 | 上传/下载慢 | 适合 CI/CD 快速分发 |
2.3 关键指令解析
| 指令 | 作用 |
|---|---|
FROM ... AS build |
给阶段命名,后续可通过 --from=build 引用 |
COPY --from=build |
从指定阶段复制文件到当前阶段 |
RUN dotnet restore |
下载 NuGet 依赖包 |
RUN dotnet publish |
编译并输出发布文件(DLL) |
💡 原理 :每个
FROM开始新阶段,前面阶段的内容不会进入最终镜像,除非显式COPY --from。这类似于编程中的"临时变量",计算完结果后只保留结果。
三、Volume 数据持久化:容器销毁,数据不丢
3.1 容器的临时性
Docker 容器是无状态的。删除容器后:
- 容器内的文件系统全部清空
- 数据库数据、日志文件、用户上传的图片都会丢失
Volume(数据卷) 解决:将宿主机目录或 Docker 管理的存储挂载到容器内,实现数据持久化。
3.2 两种 Volume 类型
| 类型 | 命令示例 | 适用场景 |
|---|---|---|
| Bind Mount | -v ~/data:/app/data |
开发环境,直接挂载宿主机路径,方便查看/编辑 |
| Named Volume | volumes: app-data: |
生产环境,由 Docker 管理,可跨容器共享 |
3.3 工作原理
bash
宿主机 Docker 容器
┌─────────────┐ ┌─────────────┐
│ ~/data/ │ ←──────────→ │ /app/data/ │
│ test.txt │ Volume │ test.txt │
│ (持久化) │ 挂载 │ (容器内可见) │
└─────────────┘ └─────────────┘
↑ ↓
└──────── 删容器后文件还在 ──────┘
四、Docker 网络:容器之间如何"打电话"
4.1 默认网络模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Bridge | 默认模式,容器通过虚拟网桥通信 | 单机多容器 |
| Host | 容器共享宿主机网络栈 | 性能要求高,但端口冲突风险 |
| None | 无网络 | 隔离环境 |
| Overlay | 跨主机通信 | Docker Swarm / Kubernetes |
4.2 Compose 自动网络
当你使用 docker-compose.yml 时,Compose 自动创建一个桥接网络,所有服务加入该网络。
核心特性 :容器间可以通过服务名作为主机名互相访问。
yaml
services:
backend:
# 服务名 = backend
frontend:
# 可以通过 http://backend:8080 访问后端
Docker 内置 DNS 服务器会自动将 backend 解析为对应容器的 IP 地址。
五、Day 1 实战:多阶段构建 + Volume(后端容器化)
5.1 准备源代码
确保你的 .NET 源代码(不是已发布的 DLL)在 Linux 上可用:
bash
mkdir -p ~/projects/day1/backend
cd ~/projects/day1/backend
# 通过 WinSCP 上传 .cs、.csproj、appsettings.json 等源代码文件
# 或从之前目录复制
cp -r ~/projects/test-source/* ./
5.2 编写多阶段 Dockerfile
bash
nano Dockerfile
dockerfile
# 阶段1:编译(SDK 镜像,体积大,仅用于编译)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# 阶段2:运行(Runtime 镜像,体积小,仅保留运行所需)
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "你的项目名.dll"]
📝 替换
你的项目名.dll为实际文件名(与.csproj中的<AssemblyName>一致,默认为项目名)。
5.3 构建并对比镜像大小
bash
# 构建多阶段镜像
docker build -t api-multi .
# 查看镜像
docker images
预期结果:
REPOSITORY TAG SIZE
api-multi latest ~100-150MB
5.4 Volume 持久化实战
bash
# 1. 创建宿主机数据目录
mkdir -p ~/projects/day1/data
# 2. 运行带 Volume 的容器(Bind Mount)
docker run -d \
--name api-with-volume \
-p 5001:8080 \
-v ~/projects/day1/data:/app/data \
api-multi
# 3. 进入容器创建文件
docker exec -it api-with-volume bash
echo "hello from container" > /app/data/test.txt
exit
# 4. 宿主机查看文件
cat ~/projects/day1/data/test.txt
# 输出:hello from container
# 5. 删除容器
docker rm -f api-with-volume
# 6. 验证数据是否持久化
cat ~/projects/day1/data/test.txt
# 输出:hello from container(还在!)
5.5 测试 API
bash
# 重新运行容器
docker run -d --name api-multi -p 5002:8080 api-multi
# 测试
curl http://localhost:5002/WeatherForecast
# 查看日志
docker logs api-multi
六、Day 2 实战:前端容器化 + Nginx 反向代理(前后端分离)
6.1 为什么需要 Nginx 反向代理?
在前后端分离架构中:
| 问题 | 解决方案 |
|---|---|
浏览器无法直接访问容器名 backend |
Nginx 在服务端转发,浏览器无感知 |
| 暴露后端端口(5000)不安全 | 只暴露 80 端口,/api 内部路由 |
| 前端代码写死 IP 地址,部署不灵活 | 前端只写 /api/xxx,Nginx 负责转发 |
架构图:
ruby
浏览器 (Windows)
│
▼
http://192.168.1.10/ ───┐
├──→ Nginx 容器 (80端口)
http://192.168.1.10/api/ ─┘ │
前端页面 ←───────────────────┤
/api/WeatherForecast ←─────┘──→ 后端容器 (8080端口)
(Docker 内部网络)
6.2 目录结构
bash
mkdir -p ~/projects/day2/{backend,frontend}
cd ~/projects/day2
最终结构:
bash
~/projects/day2/
├── backend/ # .NET 项目 + 多阶段 Dockerfile
│ ├── Dockerfile
│ ├── Program.cs
│ ├── 你的项目.csproj
│ └── ...
├── frontend/ # 前端静态文件 + Nginx 配置
│ ├── Dockerfile
│ ├── index.html
│ └── nginx.conf
└── docker-compose.yml
6.3 后端 Dockerfile(同 Day 1)
bash
cd ~/projects/day2/backend
# 放入源代码后创建 Dockerfile
nano Dockerfile
dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "你的项目名.dll"]
6.4 前端静态页面
bash
cd ~/projects/day2/frontend
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Docker 前后端分离</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; background: #007acc; color: white; border: none; border-radius: 4px; }
#result { margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 4px; min-height: 50px; }
.success { color: green; }
.error { color: red; }
</style>
</head>
<body>
<h1>🐳 Docker 前后端分离测试</h1>
<p>前端:Nginx 容器 | 后端:.NET 容器</p>
<button onclick="fetchApi()">调用 /api/WeatherForecast</button>
<div id="result">点击按钮测试...</div>
<script>
async function fetchApi() {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = '加载中...';
try {
// 关键:不写死 IP,只写 /api/ 路径
const res = await fetch('/api/WeatherForecast');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
resultDiv.innerHTML = '<span class="success">✅ 成功!</span><pre>' + JSON.stringify(data, null, 2) + '</pre>';
} catch (err) {
resultDiv.innerHTML = '<span class="error">❌ 错误:' + err.message + '</span>';
}
}
</script>
</body>
</html>
EOF
💡 关键设计 :前端代码使用
fetch('/api/WeatherForecast')而非fetch('http://192.168.1.10:5000/api/WeatherForecast')。浏览器请求的是相对路径,由 Nginx 负责转发到后端。
6.5 Nginx 反向代理配置
bash
nano nginx.conf
nginx
server {
listen 80;
server_name localhost;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# API 请求转发到后端容器
# 关键:backend 是 docker-compose 中的服务名
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 健康检查端点
location /health {
access_log off;
return 200 "nginx ok\n";
add_header Content-Type text/plain;
}
}
6.6 前端 Dockerfile
bash
nano Dockerfile
dockerfile
# 阶段1:构建(如果有 npm 构建步骤,如 Vue/React)
FROM node:20-alpine AS build
WORKDIR /app
COPY . .
# 阶段2:Nginx 托管静态文件
FROM nginx:alpine
COPY --from=build /app/index.html /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
6.7 完整的 docker-compose.yml
bash
cd ~/projects/day2
nano docker-compose.yml
yaml
version: '3.8'
services:
backend:
build: ./backend
container_name: day2-api
ports:
- "5000:8080" # 宿主机 5000 映射到容器 8080(调试用)
volumes:
- app-data:/app/data # Named Volume 持久化
networks:
- app-network
restart: unless-stopped
environment:
- ASPNETCORE_ENVIRONMENT=Production
frontend:
build: ./frontend
container_name: day2-nginx
ports:
- "80:80"
depends_on:
- backend # 确保后端先启动
networks:
- app-network
restart: unless-stopped
networks:
app-network:
driver: bridge
volumes:
app-data:
6.8 构建、启动与验证
bash
# 1. 构建并后台启动
cd ~/projects/day2
docker compose up -d --build
# 2. 查看状态
docker compose ps
# 3. 查看日志
docker compose logs -f
# 4. 测试清单
curl http://localhost:5000/WeatherForecast # 直接访问后端
curl http://localhost/api/WeatherForecast # 通过 Nginx 转发
curl http://localhost/health # Nginx 健康检查
curl http://localhost/ # 前端页面
# 5. 容器间通信测试
docker exec day2-api curl http://frontend/health # 后端容器访问前端
docker exec day2-nginx curl http://backend:8080/WeatherForecast # 前端容器访问后端
Windows 浏览器验证:
- 访问
http://192.168.1.10/→ 看到前端页面 - 点击 "调用 /api/WeatherForecast" 按钮 → 显示绿色成功 + JSON 数据
6.9 热更新与调试
bash
# 修改后端代码后,只重建后端服务
docker compose up -d --build backend
# 进入容器调试
docker exec -it day2-api bash
ps aux
cat /etc/hosts
curl http://frontend/health
exit
# 进入 Nginx 容器查看配置
docker exec -it day2-nginx sh
cat /etc/nginx/conf.d/default.conf
exit
6.10 清理资源
bash
# 停止并删除容器、网络
docker compose down
# 同时删除镜像和命名卷(彻底清理)
docker compose down --rmi all --volumes
# 系统级清理无用资源
docker system prune -a
七、核心概念图解
7.1 多阶段构建流程
ini
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 源代码 │────→│ 阶段1:build │────→│ 阶段2:runtime │
│ .cs, .csproj │ │ SDK 镜像 │ │ Runtime 镜像 │
└─────────────────┘ │ 800MB │ │ 100MB │
│ │ │ │
│ dotnet restore │ │ COPY --from=build
│ dotnet publish │ │ /app/publish │
│ │ │ │
└─────────────────┘ └─────────────────┘
↑ │
└──────────────────────┘
最终镜像
(仅保留 100MB)
7.2 前后端分离网络架构
arduino
┌─────────────────────────────────────────────────────────────┐
│ 宿主机 (Ubuntu) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Docker Bridge Network │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ frontend │ ←─────→ │ backend │ │ │
│ │ │ (Nginx) │ 内部通信 │ (.NET API) │ │ │
│ │ │ :80 │ │ :8080 │ │ │
│ │ └──────┬───────┘ └──────────────┘ │ │
│ │ │ │ │
│ └─────────┼───────────────────────────────────────────┘ │
│ │ │
│ 端口映射 │ 80:80 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Windows 浏览器 │ │
│ │ http://192.168.1.10/ │ │
│ │ http://192.168.1.10/api/WeatherForecast│ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
八、常见问题排查(FAQ)
Q1:多阶段构建后镜像仍然很大
- 检查 :确认第二阶段使用的是
aspnet而非sdk。 - 检查 :
.dockerignore是否排除了bin/、obj/、.git/。 - 优化 :使用 Alpine 版本的基础镜像(如
aspnet:8.0-alpine)。
Q2:Volume 挂载后容器内看不到文件
- 检查 :宿主机路径是否绝对路径(
~/data需展开为/home/dev/data)。 - 检查:SELinux 或 AppArmor 是否阻止访问(Ubuntu 通常无此问题)。
- 解决 :使用
$(pwd)/data确保路径正确。
Q3:Nginx 转发 API 返回 502 Bad Gateway
- 检查 :
docker compose ps确认backend状态为Up。 - 检查 :后端是否监听
0.0.0.0:8080(而非127.0.0.1:8080)。 - 检查 :
proxy_pass末尾的/是否遗漏(http://backend:8080/)。
Q4:depends_on 设置了但后端还没准备好,前端就启动了
- 说明 :
depends_on只控制启动顺序 ,不等待服务就绪。 - 解决 :生产环境使用
healthcheck或wait-for-it脚本确保后端就绪。
Q5:前端点击按钮报错,提示 CORS
- 原因 :前端直接请求了
http://192.168.1.10:5000(跨域)。 - 解决 :确保前端代码写的是
/api/WeatherForecast,由 Nginx 同域转发。
Q6:docker exec 进入容器后无法使用 bash
- 原因 :某些精简镜像(如 Alpine)没有
bash,只有sh。 - 解决 :
docker exec -it 容器名 sh。
九、完成检查清单
| 检查项 | 验证命令 | 状态 |
|---|---|---|
| 源代码已上传到 Linux | ls ~/projects/day1/backend/*.csproj |
☐ |
| 多阶段 Dockerfile 创建 | cat ~/projects/day1/backend/Dockerfile |
☐ |
| 镜像构建成功 | docker build -t api-multi . |
☐ |
| 镜像体积显著减小 | docker images(对比 SDK 镜像) |
☐ |
| API 运行正常 | curl http://localhost:5002/WeatherForecast |
☐ |
| Volume 持久化有效 | 删容器后 cat ~/projects/day1/data/test.txt |
☐ |
| 目录结构正确 | ls -R ~/projects/day2 |
☐ |
| 后端多阶段构建成功 | docker compose build backend |
☐ |
| 前端 index.html 创建 | cat ~/projects/day2/frontend/index.html |
☐ |
| nginx.conf 配置正确 | cat ~/projects/day2/frontend/nginx.conf |
☐ |
| docker-compose 启动成功 | docker compose ps(两个 Up) |
☐ |
| 前端页面可访问 | 浏览器 http://192.168.1.10/ |
☐ |
| API 转发正常 | curl http://192.168.1.10/api/WeatherForecast |
☐ |
| 前端按钮调用成功 | 点击按钮显示绿色 JSON | ☐ |
| 容器间网络互通 | docker exec day2-api curl http://frontend/health |
☐ |
| depends_on 生效 | docker compose logs frontend 显示等待 backend |
☐ |
结语
至此,你已经掌握了 Docker 生产环境部署的核心技能:
- 多阶段构建:将镜像体积从 800MB 压缩到 100MB,提升分发效率
- Volume 持久化:确保容器销毁后数据不丢失
- 自定义网络:理解容器间通过服务名通信的原理
- Nginx 反向代理:实现前后端分离的标准部署架构
- Compose 编排:一条命令启动完整应用栈
这些知识是后续学习 Docker Swarm 、Kubernetes 、CI/CD 流水线 的坚实基础。