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

适用人群 :已完成 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 流水线 的坚实基础。

相关推荐
demo007x13 小时前
让大模型活在你的鼠标旁:我用 Tauri 2 + Rust 打造了一款“反直觉”的 AI 全局划词效率神器
macos·程序员·llm
程序员海军14 小时前
AI 越来越强,为什么打工人反而越来越累、越来越内耗了?
前端·程序员·aigc
badhope19 小时前
同样写 AI,为什么有的文章 549 人看、有的只有 16 人看?——我用 40 篇掘金文章的真实数据复盘
程序员
今朝唯我少年郎2 天前
Codex安全盲区代码漏洞生成实测
python·程序员
kyriewen2 天前
我用 AI 写完一个需求后才发现,最难的不是 prompt,而是验收
前端·程序员·ai编程
酷酷的逗逗乐3 天前
前端转 Agent 开发 · 第六节
前端·程序员
爱勇宝3 天前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
程序员cxuan3 天前
跟 WebUI 说再见了,最强 DeepSeek 桌面端来了!
人工智能·后端·程序员
codigger3 天前
用 AI 写代码,你知道它会把我的代码传去哪?
ai·程序员·编程·数据安全
程序员cxuan3 天前
DeepSeek V4.1 Flash 正式发布!
人工智能·后端·程序员