Docker 多阶段构建
大约 12 分钟约 3512 字
Docker 多阶段构建
简介
Docker 多阶段构建(Multi-Stage Build)是优化容器镜像大小的核心技术。通过在一个 Dockerfile 中定义多个构建阶段,仅在最终镜像中保留运行时所需的文件,大幅减少镜像体积。多阶段构建特别适用于 .NET、Java、Node.js 等需要编译环境的语言,能够将镜像从数 GB 缩减至数十 MB。
特点
.NET 多阶段构建
ASP.NET Core 应用 Dockerfile
# 阶段一:构建
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 先复制项目文件,利用缓存恢复依赖
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
# 复制源码并构建
COPY . .
RUN dotnet publish MyApp.csproj \
-c Release \
-o /app/publish \
--no-restore
# 阶段二:运行时
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
# 安装必要运行时依赖(如 ICU、时区数据)
RUN apk add --no-cache icu-libs tzdata
# 从构建阶段复制发布输出
COPY --from=build /app/publish .
# 配置运行时环境
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
# 以非 root 用户运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
ENTRYPOINT ["dotnet", "MyApp.dll"].NET 解决方案多项目构建
# 阶段一:恢复依赖
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS restore
WORKDIR /src
COPY ["MyApp.sln", "./"]
COPY ["src/MyApp.Api/MyApp.Api.csproj", "src/MyApp.Api/"]
COPY ["src/MyApp.Core/MyApp.Core.csproj", "src/MyApp.Core/"]
COPY ["src/MyApp.Infrastructure/MyApp.Infrastructure.csproj", "src/MyApp.Infrastructure/"]
RUN dotnet restore "src/MyApp.Api/MyApp.Api.csproj"
# 阶段二:构建
FROM restore AS build
COPY . .
RUN dotnet publish "src/MyApp.Api/MyApp.Api.csproj" \
-c Release \
-o /app/publish \
--no-restore
# 阶段三:运行时
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/publish .
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]Node.js 多阶段构建
Node.js 应用 Dockerfile
# 阶段一:安装依赖
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 阶段二:构建(如需要 TypeScript 编译或前端构建)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段三:运行时
FROM node:20-alpine AS runtime
WORKDIR /app
# 仅复制生产依赖和构建产物
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]前端 React/Vue 应用构建
# 阶段一:构建前端
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段二:Nginx 提供静态文件服务
FROM nginx:alpine AS runtime
# 复制构建产物到 Nginx
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制自定义 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]# nginx.conf — 前端 SPA 路由配置
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
}镜像体积优化
对比优化效果
# 查看各阶段镜像大小
docker build --target build -t myapp:build .
docker build --target runtime -t myapp:runtime .
docker images | grep myapp
# 构建结果对比示例
| 阶段 | 基础镜像 | 镜像大小 |
|---|---|---|
| SDK 构建 | mcr.microsoft.com/dotnet/sdk:8.0 | ~1.2 GB |
| 运行时(Debian) | mcr.microsoft.com/dotnet/aspnet:8.0 | ~210 MB |
| 运行时(Alpine) | mcr.microsoft.com/dotnet/aspnet:8.0-alpine | ~80 MB |
| 运行时(Chiseled) | mcr.microsoft.com/dotnet/aspnet:8.0-alpine | ~30 MB |
# 使用 dive 工具分析镜像层
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive myapp:runtime.NET 独立发布与 AOT
# 使用 Native AOT 进一步减小体积
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
COPY . .
# 发布 Native AOT
RUN dotnet publish -c Release -r linux-musl-x64 \
-p:PublishAot=true \
-p:StripSymbols=true \
-o /app/publish
# 最终镜像仅需基础系统
FROM alpine:3.19 AS runtime
WORKDIR /app
COPY --from=build /app/publish/MyApp .
RUN apk add --no-cache libstdc++ zlib
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
ENTRYPOINT ["./MyApp"]构建缓存优化
缓存友好型 Dockerfile
# .NET 缓存优化示例
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# 第一步:复制项目文件(变更频率低)
COPY ["Directory.Build.props", "."]
COPY ["Directory.Packages.props", "."]
COPY ["NuGet.config", "."]
COPY ["src/MyApp.Api/MyApp.Api.csproj", "src/MyApp.Api/"]
COPY ["src/MyApp.Core/MyApp.Core.csproj", "src/MyApp.Core/"]
# 第二步:恢复依赖(缓存层)
RUN dotnet restore "src/MyApp.Api/MyApp.Api.csproj"
# 第三步:复制源码(变更频率高)
COPY . .
# 第四步:构建(仅源码变更时重新执行)
RUN dotnet publish "src/MyApp.Api/MyApp.Api.csproj" \
-c Release -o /app/publish --no-restore使用 BuildKit 缓存挂载
# 语法指令
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
# 挂载缓存目录,加速 NuGet 恢复
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore
COPY . .
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet publish -c Release -o /app/publish --no-restore# 启用 BuildKit 构建
DOCKER_BUILDKIT=1 docker build -t myapp:v1.0 .
# 使用缓存挂载的 Node.js 示例
# syntax=docker/dockerfile:1
# FROM node:20-alpine AS deps
# WORKDIR /app
# COPY package*.json ./
# RUN --mount=type=cache,target=/root/.npm \
# npm ci --only=production生产环境最佳实践
多阶段构建结合安全配置
# 阶段一:构建
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
ARG NUGET_TOKEN
ARG VERSION=1.0.0
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release \
-p:Version=${VERSION} \
-o /app/publish
# 阶段二:运行时
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
# 安装安全更新
RUN apk update && apk upgrade --no-cache && \
apk add --no-cache icu-libs tzdata curl && \
rm -rf /var/cache/apk/*
WORKDIR /app
COPY --from=build /app/publish .
# 安全配置
RUN addgroup -S appgroup && adduser -S appuser -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"].dockerignore 优化
# .dockerignore
bin/
obj/
*.md
.git/
.gitignore
.vs/
.vscode/
docker-compose*.yml
Dockerfile*
*.user
*.suo
TestResults/
node_modules/
dist/
.envJava/Spring Boot 多阶段构建
Maven 项目多阶段构建
# 阶段一:Maven 依赖缓存
FROM maven:3.9-eclipse-temurin-17 AS deps
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 阶段二:编译打包
FROM deps AS build
COPY src ./src
RUN mvn package -DskipTests -B
# 阶段三:运行时
FROM eclipse-temurin:17-jre-alpine AS runtime
WORKDIR /app
# 创建非 root 用户
RUN addgroup -S spring && adduser -S spring -G spring
# 从构建阶段复制 JAR 包
COPY --from=build /build/target/*.jar app.jar
# 优化 JVM 参数
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-Djava.security.egd=file:/dev/./urandom"
USER spring
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Gradle 项目多阶段构建
# 阶段一:Gradle 依赖缓存
FROM gradle:8-jdk17 AS deps
WORKDIR /build
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
RUN gradle dependencies --no-daemon || true
# 阶段二:编译打包
FROM deps AS build
COPY src ./src
RUN gradle bootJar --no-daemon -x test
# 阶段三:运行时
FROM eclipse-temurin:17-jre-alpine AS runtime
WORKDIR /app
COPY --from=build /build/build/libs/*.jar app.jar
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Python 多阶段构建
# 阶段一:安装依赖
FROM python:3.12-slim AS deps
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 阶段二:运行时
FROM python:3.12-slim AS runtime
WORKDIR /app
# 仅复制已安装的依赖
COPY --from=deps /install /usr/local
COPY . .
# 创建非 root 用户
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]镜像安全加固
基础镜像选择策略
# 基础镜像选择优先级(从高到低):
# 1. Distroless(Google 出品,无 shell,攻击面最小)
# 2. Alpine(体积小,约 5MB)
# 3. Debian Slim(兼容性好,约 80MB)
# 4. Ubuntu/Debian(调试方便,约 120MB)
# 5. 完整版(仅限构建阶段)
# 使用 Distroless(适用于 Go、Java、Python 等编译型语言)
# FROM gcr.io/distroless/static-debian12
# FROM gcr.io/distroless/java17-debian12
# Alpine 兼容性注意事项:
# Alpine 使用 musl libc 而非 glibc,部分预编译的二进制可能不兼容
# 解决方案:
# 1. 使用 Alpine 兼容的依赖版本
# 2. 编译时静态链接(如 Go 默认静态编译)
# 3. 使用 glibc 兼容层:apk add gcompat
# 定期检查基础镜像更新
# 使用 Trivy 扫描镜像漏洞
trivy image myapp:runtimeDockerfile 安全最佳实践
# 安全加固示例
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
# 1. 更新系统包并安装安全更新
RUN apk update && apk upgrade --no-cache && \
apk add --no-cache ca-certificates curl tzdata && \
rm -rf /var/cache/apk/*
# 2. 设置时区
ENV TZ=Asia/Shanghai
# 3. 使用非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 4. 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 5. 不暴露调试端口
EXPOSE 8080
# 6. 使用 COPY --chown 设置正确权限
COPY --from=build --chown=appuser:appgroup /app/publish ./app
WORKDIR /app
ENTRYPOINT ["dotnet", "MyApp.dll"]调试多阶段构建
# 调试特定阶段(不构建后续阶段)
docker build --target build -t myapp:debug .
# 进入构建阶段容器检查中间产物
docker build --target build -t myapp:debug .
docker run --rm -it myapp:debug sh
# 查看构建过程的每层详细信息
docker history myapp:runtime
# 使用 BuildKit 查看构建缓存命中情况
DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp:v1 .
# 跳过特定阶段的构建(使用缓存)
# 如果只需要运行时阶段,确保构建阶段已缓存
# 导出构建阶段的文件到本地(调试用)
docker build --target build -t myapp:build .
docker create --name tmp myapp:build
docker cp tmp:/app/publish ./local-publish
docker rm tmp多阶段构建常见问题
Alpine glibc 兼容问题
# 问题现象:在 Alpine 镜像中运行报错 "not found" 或 "libc.so.6: not found"
# 原因:某些预编译的二进制依赖 glibc,而 Alpine 使用 musl libc
# 解决方案 1:安装 glibc 兼容层
FROM alpine:3.19 AS runtime
RUN apk add --no-cache gcompat libstdc++
# 解决方案 2:改用 Debian Slim(兼容性更好,体积稍大)
FROM debian:12-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl && \
rm -rf /var/lib/apt/lists/*
# 解决方案 3:使用静态编译(Go 语言适用)
# CGO_ENABLED=0 go build -o myappCOPY --from 跨阶段引用
# 可以从任意阶段复制文件(不限于前一个阶段)
FROM node:20-alpine AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
FROM python:3.12-slim AS backend
WORKDIR /app
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
# 将前端构建产物复制到后端镜像
COPY --from=frontend /app/dist ./static
# 也可以从外部镜像复制文件
# COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf
CMD ["python", "main.py"]优点
缺点
总结
Docker 多阶段构建是优化容器镜像的核心技术。通过将构建环境和运行环境分离,.NET 应用可将镜像从 1.2 GB 的 SDK 镜像缩减至 80 MB 甚至更小的 Alpine 运行时镜像,Node.js 应用也同理。结合 BuildKit 缓存挂载、.dockerignore 过滤和合理的 Dockerfile 分层策略,能够在保持镜像精简的同时实现高效的构建过程。建议所有生产环境的容器镜像都采用多阶段构建,并定期优化 Dockerfile 以利用最新的基础镜像和安全补丁。
关键知识点
- DevOps 主题的核心是让交付更快、更稳、更可审计。
- 自动化不是把命令脚本化,而是把失败、回滚、权限和观测一起设计进去。
- 生产链路必须明确制品、环境、凭据、配置和责任边界。
- 部署主题通常要同时看镜像、容器、卷、网络和宿主机资源。
项目落地视角
- 把流水线拆成构建、测试、制品、部署、验证和回滚几个阶段。
- 为关键步骤补齐日志、指标、通知和人工兜底点。
- 定期演练扩容、回滚、故障注入和灾备切换。
- 固定镜像标签,记录端口、挂载目录、环境变量和自启动策略。
常见误区
- 只关注部署成功,不关注失败恢复和审计追踪。
- 把环境差异藏在临时脚本或人工操作里。
- 上线频率高了以后,没有标准化制品和配置管理。
- 使用 latest 导致结果不可复现。
进阶路线
- 继续补齐 GitOps、可观测性、平台工程和成本治理。
- 把主题和应用架构、安全、权限、备份恢复联动起来理解。
- 形成团队级平台能力,而不是每个项目重复造轮子。
- 继续补齐 Compose 编排、镜像瘦身、安全扫描和镜像仓库治理。
适用场景
- 当你准备把《Docker 多阶段构建》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合构建自动化交付、基础设施治理、监控告警和生产发布体系。
- 当团队规模扩大、发布频率提升或环境变多时,这类主题会显著影响交付效率。
落地建议
- 所有自动化流程尽量做到幂等、可审计、可回滚。
- 把制品、变量、凭据和执行权限分层管理。
- 定期演练扩容、回滚、密钥轮换和灾备恢复。
排错清单
- 先定位失败发生在代码、构建、制品、环境还是权限层。
- 检查流水线变量、凭据、镜像标签和目标环境配置是否一致。
- 如果问题偶发,重点看并发发布、资源争抢和外部依赖抖动。
复盘问题
- 如果把《Docker 多阶段构建》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Docker 多阶段构建》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Docker 多阶段构建》最大的收益和代价分别是什么?
