多阶段构建、数据持久化与前后端分离部署

适用人群 :已完成 Docker 基础安装与单容器运行,准备深入理解生产级容器化实践的开发者

目标 :掌握多阶段构建优化镜像体积、Volume 数据持久化、Nginx 反向代理、Docker Compose 多服务编排

前置要求 :已完成 M2 前半部分(Docker 安装、单阶段 Dockerfile、基础 Compose)

阅读时长:约 25 分钟(实操约 3-4 小时,建议分两天完成)


目录

  1. 为什么需要这些"遗漏项"?
  2. [多阶段构建:从 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")
  3. [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")
  4. [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")
  5. [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")
  6. [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")
  7. 核心概念图解
  8. 常见问题排查(FAQ)
  9. 完成检查清单

一、为什么需要这些"遗漏项"?

前半部分,你已经学会了安装 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 只控制启动顺序 ,不等待服务就绪
  • 解决 :生产环境使用 healthcheckwait-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 生产环境部署的核心技能:

  1. 多阶段构建:将镜像体积从 800MB 压缩到 100MB,提升分发效率
  2. Volume 持久化:确保容器销毁后数据不丢失
  3. 自定义网络:理解容器间通过服务名通信的原理
  4. Nginx 反向代理:实现前后端分离的标准部署架构
  5. Compose 编排:一条命令启动完整应用栈

这些知识是后续学习 Docker SwarmKubernetesCI/CD 流水线 的坚实基础。

相关推荐
谢文峰2 小时前
ChatGPT/Codex 无法开启远程控制?一次 Clash TUN 与 aTrust 网段冲突排查实录
程序员
SamDeepThinking2 小时前
一次线程池线上故障复盘:四层防线如何避免数据丢失
java·后端·程序员
xyy1232 小时前
Linux 核心命令
程序员
SimonKing3 小时前
Base62编码生成6位不重复短码的背后设计
java·后端·程序员
Hilaku19 小时前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
猿的天空1 天前
GPT-5.6拆成三段,智能变成了可选购配置
人工智能·gpt·计算机·ai·程序员·大模型·智能体
阿里嘎多学长1 天前
2026-07-09 GitHub 热点项目精选
开发语言·程序员·github·代码托管
爱勇宝1 天前
从吐槽 DeepSeek 到被投资人炮轰:比“天才光环”更重要的,是契约与边界
程序员·创业·deepseek
狂师1 天前
端开发必装 Skill 清单:6 款 AI 技能,让你的开发效率原地起飞
前端·程序员·开源