Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

Docker多阶段构建完整教程,从2.1GB到48MB实战总结。Node.js/Python/Go三种语言的多阶段构建模板,拿来即用。
你的Docker镜像,几个G?我的一个Node.js项目,从2.1GB降到48MB。
这不是删文件能做到的。这是一套叫「多阶段构建」的技术带来的结果。
先说一个事实:大多数项目的Docker镜像体积是不正常的。
看几个典型案例。一个基于Create React App的Node.js项目,打出来的镜像通常在800MB-1.2GB之间。一个Python Django项目,镜像轻松超过1.5GB。一个Go服务如果直接用 golang:1.21 作为基础镜像,体积也在900MB以上。
这些体积带来的真实成本是什么?
CI/CD环节:更大的镜像意味着更慢的 docker pull。在网络条件一般的情况下,拉取一个1GB的镜像可能需要5-10分钟。这5-10分钟,每天积累下来就是巨大的效率损耗。
生产环境:每次Pod重启都要重新拉取镜像。在K8s的滚动更新场景下,大镜像意味着更长的更新时间窗口和更高的服务不可用风险。
存储成本:无论你用阿里云ACK还是AWS EKS,镜像仓库的存储都是按GB收费的。100个项目,每个项目节省500MB,一年下来就是600GB的免费存储空间。
问题清楚了。接下来看怎么解决。
多阶段构建是Docker 17.05引入的特性。它的核心思想是:一个Dockerfile可以包含多个 FROM 指令,每个 FROM 开启一个独立的构建阶段。
第一个阶段称为 Build Stage,负责编译、构建、安装依赖——这些操作需要完整的开发工具链。第二个阶段称为 Final Stage,只复制 Build Stage 的产物,配置运行时环境。
Build Stage 产生的所有冗余——编译工具、源码、构建依赖——都不会进入 Final Stage。这是体积减少的根本原因。
看一个最简单的对比。
传统单阶段 Dockerfile(Node.js):
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
问题在哪?node:18 这个基础镜像本身就超过900MB,里面包含了npm、yarn、整个Node.js运行时,以及所有构建工具。而且 npm install 会把开发依赖也装进来,这些依赖在生产环境毫无用处。
多阶段构建版本:
# Build Stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY . .
RUN npm run build
# Final Stage
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["npm", "start"]
node:18-slim 体积约200MB,比 node:18 小70%。更重要的是,生产镜像里只有编译好的 dist 目录和必要的 node_modules,没有源码,没有构建工具链。
多阶段构建中,阶段间的产物传递通过 COPY –from=stage_name 实现。
当你用 AS 关键字给 Build Stage 命名后,Final Stage 可以通过名字引用它:
FROM xxx AS builder
# ... 构建步骤 ...
FROM yyy
COPY --from=builder /app/output ./output
也可以用数字索引引用(从0开始):
COPY --from=0 /app/output ./output
这个设计非常灵活。一个 Dockerfile 可以包含任意多个构建阶段,每个阶段负责特定任务,最终只把需要的东西合并到生产镜像里。
完整的 Node.js 多阶段构建模板:
# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Builder
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Runner
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 appuser
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
三个阶段的职责划分:
实测结果:一个中等规模的 Next.js 项目,镜像体积从 1.1GB 降到 67MB。
Python 项目的多阶段构建稍微复杂一点,因为 Python 的依赖安装和运行是两个不同的环境。
# Stage 1: Builder
FROM python:3.11-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# Stage 2: Final
FROM python:3.11-slim AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --from=builder /app /app
ENV PATH=/home/appuser/.local/bin:$PATH \
PYTHONUNBUFFERED=1
USER appuser
WORKDIR /app
CMD ["python", "-m", "myapp"]
关键点:用 –no-cache-dir 避免pip缓存,用 –user 把包安装到用户目录而非系统目录。Final Stage 只需要运行时的动态库(libpq5),不需要 gcc 等编译工具。
实测结果:一个 Django 项目,镜像体积从 1.4GB 降到 89MB。
Go 项目的多阶段构建是最简单的,因为 Go 是编译型语言,产出物是一个独立的二进制文件。
# Stage 1: Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Stage 2: Final
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
这是最小化的 Go 生产镜像。Alpine Linux 基础镜像只有5MB,加上证书大约8MB。整个镜像可以控制在15MB以内。
如果追求更小的体积,可以用 FROM scratch(完全空白的基础镜像):
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-w -s' -o main .
# -w -s 去掉调试信息和符号表,体积再减30%
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
scratch 镜像是真正意义的零体积——没有操作系统,没有Shell,什么都没有。只有你的二进制文件和它依赖的证书文件。Go静态编译的二进制文件完全可以运行在 scratch 上。
实测结果:一个中等规模的 Go HTTP 服务,镜像体积从 940MB(golang:1.21)降到 12MB(scratch)。
基础镜像是体积的主要来源。优先使用 -alpine、-slim、-distroless 等轻量版本。
常见基础镜像体积对比(Node.js):
| 镜像 | 体积 |
|---|---|
| node:18 | 1.1GB |
| node:18-alpine | 180MB |
| node:18-slim | 200MB |
| node:18-distroless | 140MB |
Alpine 用 apk 包管理器,体积最小但有时会遇到兼容性问题(特别是涉及glibc的场景)。Slim 用apt,兼容性最好。Distroless 是Google维护的超精简镜像,不包含Shell,适合安全要求高的场景。
Docker镜像的每一层都会占用体积。合并多个RUN指令可以减少层数和总体积:
# 差的写法:4层
RUN apt-get update
RUN apt-get install -y nginx
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# 好的写法:1层
RUN apt-get update && \
apt-get install -y nginx curl && \
rm -rf /var/lib/apt/lists/*
每一次 RUN 都会生成一个新的镜像层。用 && 合并操作,层数更少,体积更小。
.dockerignore 决定了哪些文件不会被传送到Docker构建上下文。不配置 .dockerignore,你的 node_modules、.git 目录、本地测试数据都会被传进去,既浪费带宽又增大体积。
node_modules
.git
.env
.env.*
!.env.example
*.log
npm-debug.log*
.DS_Store
dist
coverage
.vscode
.idea
*.md
!README.md
tests
spec
__tests__
关键点:node_modules 一定要排除(重新安装比复制更快),.git 必须排除,*.log 排除避免日志文件进入镜像。
多阶段构建不是什么新技术,但它能带来的改变被严重低估了。
一个从 2.1GB 降到 48MB 的镜像,拉取时间从 8 分钟降到 20 秒,CI/CD 的 docker pull 步骤从瓶颈变成几乎无感。这就是为什么要认真对待 Docker 镜像优化——它直接转化为效率。
现在开始检查你的项目 Dockerfile。三个问题:
如果任何一个问题的答案是「没有」,你现在就知道从哪开始了。