Docker多阶段构建优化实践

前言

在容器化时代,Docker镜像的大小和构建效率直接影响着部署速度和资源消耗。一个臃肿的镜像不仅占用存储空间,还会拖慢CI/CD流水线的速度。

多阶段构建(Multi-stage Build)是Docker 17.05引入的特性,它能帮助我们构建出更小、更安全、更高效的镜像。本文将通过实际案例,深入讲解多阶段构建的原理和最佳实践。


一、为什么需要多阶段构建

1.1 传统构建的问题

以一个Java Spring Boot应用为例,传统的Dockerfile:

1
2
3
4
5
6
7
8
9
10
FROM maven:3.8-openjdk-17
WORKDIR /app

COPY pom.xml .
COPY src ./src

RUN mvn clean package -DskipTests

EXPOSE 8080
CMD ["java", "-jar", "target/app.jar"]

问题:

  • 最终镜像包含Maven、源代码等构建工具
  • 镜像大小:约800MB
  • 包含不必要的安全风险

1.2 多阶段构建的优势

1
2
3
4
5
6
7
8
9
10
11
12
13
# 构建阶段
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# 运行阶段
FROM openjdk:17-slim
WORKDIR /app
COPY --from=builder /app/target/app.jar .
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

优势:

  • 最终镜像只包含运行时依赖
  • 镜像大小:约250MB(减少70%)
  • 更安全,不包含构建工具

二、多阶段构建基础

2.1 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
# 阶段1:构建阶段(命名为builder)
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 阶段2:运行阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

关键点:

  • AS <name>:给阶段命名
  • COPY --from=<stage>:从指定阶段复制文件
  • 最后一个FROM决定最终镜像的基础

2.2 从外部镜像复制

1
2
3
4
5
6
7
8
# 从其他镜像复制文件
FROM alpine:latest

# 从nginx镜像复制配置
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

# 从特定镜像复制二进制文件
COPY --from=golang:1.21 /usr/local/go /usr/local/go

三、实战案例

3.1 Go应用优化

优化前:

1
2
3
4
5
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["./main"]

镜像大小:约1GB

优化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /app

# 复制依赖文件
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码并构建
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# 运行阶段
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

镜像大小:约15MB(减少98%!)

3.2 Node.js应用优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app

# 只复制package文件,利用缓存
COPY package*.json ./
RUN npm ci --only=production

# 复制源代码
COPY . .
RUN npm run build

# 运行阶段
FROM node:18-alpine
WORKDIR /app

# 只复制生产依赖
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./

EXPOSE 3000
CMD ["node", "dist/index.js"]

3.3 Python应用优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 构建阶段
FROM python:3.11-slim AS builder
WORKDIR /app

# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*

# 安装Python依赖
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 运行阶段
FROM python:3.11-slim
WORKDIR /app

# 复制Python包
COPY --from=builder /root/.local /root/.local

# 复制应用代码
COPY . .

# 确保脚本在PATH中
ENV PATH=/root/.local/bin:$PATH

EXPOSE 8000
CMD ["python", "app.py"]

3.4 前端应用优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app

# 安装依赖
COPY package*.json ./
RUN npm ci

# 构建应用
COPY . .
RUN npm run build

# 运行阶段
FROM nginx:alpine

# 复制构建产物
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;
server_name localhost;

root /usr/share/nginx/html;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}

# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

四、高级优化技巧

4.1 利用构建缓存

1
2
3
4
5
6
7
8
9
10
11
12
FROM node:18-alpine AS builder
WORKDIR /app

# 先复制依赖文件(不常变化)
COPY package*.json ./
RUN npm ci

# 再复制源代码(经常变化)
COPY . .
RUN npm run build

# 这样,代码变化时,依赖安装层会被缓存

4.2 多个构建阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 依赖下载阶段
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# 测试阶段(可选)
FROM builder AS tester
RUN npm run test

# 运行阶段
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./

EXPOSE 3000
CMD ["node", "dist/index.js"]

构建时可以指定目标阶段:

1
2
3
4
5
# 只构建到测试阶段
docker build --target tester -t myapp:test .

# 构建完整镜像
docker build -t myapp:latest .

4.3 使用Alpine基础镜像

1
2
3
4
5
6
7
8
9
10
11
# Go应用使用distroless
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main .

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/main /
USER nonroot:nonroot
EXPOSE 8080
CMD ["/main"]

镜像大小对比:

  • golang:1.21: 1GB
  • golang:1.21-alpine: 300MB
  • alpine: 5MB
  • distroless/static: 2MB

4.4 清理临时文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM node:18-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci && \
npm cache clean --force && \
rm -rf /tmp/*

COPY . .
RUN npm run build && \
rm -rf src tests *.config.js

EXPOSE 3000
CMD ["node", "dist/index.js"]

五、安全加固

5.1 使用非root用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM node:18-alpine
WORKDIR /app

# 创建非root用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001

COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci

COPY --chown=nodejs:nodejs . .
RUN npm run build

# 切换到非root用户
USER nodejs

EXPOSE 3000
CMD ["node", "dist/index.js"]

5.2 扫描漏洞

1
2
3
4
5
6
# 使用Trivy扫描镜像
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:latest

# 使用Snyk扫描
snyk container test myapp:latest

5.3 最小化攻击面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .

# 使用scratch(空镜像)
FROM scratch

# 只复制必要的文件
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/main /main

EXPOSE 8080
ENTRYPOINT ["/main"]

六、构建性能优化

6.1 使用.dockerignore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
.env.local
dist
build
coverage
.vscode
.idea
*.log

6.2 并行构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:18-alpine AS builder1
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY src/module1 ./src/module1
RUN npm run build:module1

FROM node:18-alpine AS builder2
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY src/module2 ./src/module2
RUN npm run build:module2

FROM node:18-alpine
COPY --from=builder1 /app/dist/module1 ./dist/module1
COPY --from=builder2 /app/dist/module2 ./dist/module2

6.3 使用BuildKit

1
2
3
4
5
6
7
8
# 启用BuildKit
export DOCKER_BUILDKIT=1

# 使用BuildKit特性
docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t myapp .

# 使用缓存
docker build --cache-from myapp:latest -t myapp:new .

七、实战:Java Spring Boot应用

完整的生产级Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 构建阶段
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /app

# 复制Maven配置
COPY pom.xml .
COPY .mvn .mvn

# 下载依赖(利用缓存)
RUN mvn dependency:go-offline

# 复制源代码并构建
COPY src ./src
RUN mvn clean package -DskipTests && \
mkdir -p target/dependency && \
cd target/dependency && \
jar -xf ../*.jar

# 运行阶段
FROM openjdk:17-slim
WORKDIR /app

# 创建非root用户
RUN groupadd -r spring && useradd -r -g spring spring

# 复制依赖层(这些层变化较少)
COPY --from=builder --chown=spring:spring /app/target/dependency/BOOT-INF/lib ./lib
COPY --from=builder --chown=spring:spring /app/target/dependency/META-INF ./META-INF
COPY --from=builder --chown=spring:spring /app/target/dependency/BOOT-INF/classes .

# 切换用户
USER spring:spring

# JVM参数优化
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

EXPOSE 8080

ENTRYPOINT exec java $JAVA_OPTS -cp ".:lib/*" com.example.Application

构建和运行:

1
2
3
4
5
6
7
8
9
10
# 构建镜像
docker build -t spring-app:latest .

# 运行容器
docker run -d \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e JAVA_OPTS="-Xms512m -Xmx1g" \
--name spring-app \
spring-app:latest

八、Docker Compose集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3.8'

services:
app:
build:
context: .
dockerfile: Dockerfile
target: runner # 指定构建目标
cache_from:
- myapp:latest
image: myapp:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped

九、CI/CD集成

9.1 GitLab CI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stages:
- build
- test
- deploy

variables:
DOCKER_DRIVER: overlay2
DOCKER_BUILDKIT: 1

build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build --target builder -t $CI_REGISTRY_IMAGE:builder .
- docker build --cache-from $CI_REGISTRY_IMAGE:builder -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

9.2 GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Build and Push Docker Image

on:
push:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: username/myapp:latest
cache-from: type=registry,ref=username/myapp:buildcache
cache-to: type=registry,ref=username/myapp:buildcache,mode=max

十、监控和优化

10.1 分析镜像层

1
2
3
4
5
6
7
8
# 使用dive分析镜像
dive myapp:latest

# 查看镜像历史
docker history myapp:latest --no-trunc

# 使用docker-slim优化镜像
docker-slim build --target myapp:latest --tag myapp:slim

10.2 镜像大小对比

应用类型 优化前 优化后 减少比例
Java Spring Boot 800MB 250MB 69%
Go应用 1GB 15MB 98%
Node.js应用 600MB 150MB 75%
Python应用 900MB 200MB 78%

总结

Docker多阶段构建的核心价值:

  1. 大幅减小镜像体积:只包含运行时必需的文件
  2. 提升安全性:不包含构建工具和源代码
  3. 提高构建效率:充分利用层缓存
  4. 简化部署流程:单个Dockerfile完成所有步骤

最佳实践:

  • 合理组织构建阶段
  • 利用层缓存机制
  • 使用轻量级基础镜像
  • 实施安全加固措施
  • 配合CI/CD自动化

掌握多阶段构建,是迈向Docker专家的必经之路!