Docker 多阶段构建优化镜像
引言
大家有没有遇到过这种情况:自己写的 Node.js、Go 或者 Python 项目,用 Docker 打包完后,镜像体积大得离谱?我之前做一个 Go 服务,编译出来的二进制才 20MB,结果镜像愣是干到了 1GB 多,里面一堆没用的依赖和构建工具。这不仅占用磁盘空间,部署的时候也特别慢,镜像拉取半天不动弹。
后来我学会了 Docker 的多阶段构建(Multi-stage Build),镜像体积直接砍到只剩几十兆,拉取速度飞起。今天就给大家好好聊聊这个技术,怎么用多阶段构建来优化你的 Docker 镜像。
什么是多阶段构建
多阶段构建是 Docker 17.05 之后引入的特性,简单来说就是在一个 Dockerfile 里定义多个阶段(Stage),每个阶段可以有不同的基础镜像,最后只把需要的内容复制到最终镜像里。
传统的构建方式是这样的:用一个包含完整开发环境的镜像,比如各种语言的基础镜像再加上编译工具,然后在容器里跑完构建,最后直接提交。这个镜像里就会残留很多编译依赖、临时文件之类的垃圾。
多阶段构建的好处就在于,它允许我们“用完就扔”。第一个阶段专门用来构建,把代码编译成可执行文件或者安装依赖;第二个阶段用一个干净的基础镜像,只把构建产物复制过来。这样最终镜像里就不会有那些没用的东西了。
基础用法演示
先来看一个最简单的例子,这是优化前的 Dockerfile:
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
这个镜像体积保守估计在 1GB 左右,因为 node:18 本身就不小,再加上各种依赖。
改成多阶段构建后是这样的:
# 第一阶段:构建阶段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
第二阶段:运行阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
看到区别了吗?第一个阶段用完整的 node:18 来跑构建,第二个阶段用轻量的 node:18-alpine,而且只复制构建产物(dist 目录和 node_modules)。这样镜像体积能少一大截,alpine 镜像本身才几十MB。
Go 项目的优化案例
再给大家看一个 Go 项目的例子,这个更典型,因为 Go 是静态编译的语言,理论上只需要一个二进制就能跑,但很多人不会配置 Dockerfile,导致镜像特别大。
传统写法:
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o main .
EXPOSE 8080
CMD ["./main"]
这个镜像体积轻松超过 1GB,因为 golang:1.21 这个镜像本身就很大,包含了完整的 Go 编译工具链。
优化后的多阶段版本:
# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
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 /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
这里有几个小技巧:
1. CGO_ENABLED=0 禁用 CGO,编译纯静态的二进制
2. GOOS=linux 确保编译产物是 Linux 可执行文件
3. -installsuffix cgo 避免链接额外的 C 库
4. 第二个阶段用 alpine,只加了一个 ca-certificates 用于 HTTPS
最终镜像大概只有 20-30MB,比原来的 1GB 少了 97%!
Python 项目同样适用
Python 项目也能用多阶段构建,不过 Python 一般是解释型语言,不像 Go 那样能直接编译成二进制。但我们可以用这个思路来减少依赖。
# 构建阶段
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
运行阶段
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]
这里利用 --no-cache-dir 安装依赖,然后只把 site-packages 目录复制到最终镜像里,省去了 pip 本身和构建工具。
进阶技巧
除了上面说的基本用法,还有几个可以进一步优化的地方:
1. 减少 COPY 次数Docker 的每一层 COPY 都会增加镜像体积,尽量合并:
# 不好的写法
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
好的写法
COPY package*.json ./
RUN npm install
COPY . .
跟 .gitignore 类似,把不需要的文件排除掉,减少 COPY 进去的内容:
node_modules
.git
*.md
Dockerfile
.dockerignore
dist
alpine 镜像虽然小,但有时候会有兼容性问题,比如 glibc 相关的库。如果遇到问题,可以考虑用 debian:slim 或者直接用官方的基础镜像。
4. 分阶段构建可以多个复杂项目可能需要多个构建阶段,比如先编译前端,再编译后端:
FROM node:18 AS frontend-builder
WORKDIR /frontend
COPY frontend/ .
RUN npm run build
FROM golang:1.21 AS backend-builder
WORKDIR /app
COPY backend/ .
RUN go build
FROM alpine
COPY --from=frontend-builder /frontend/dist /var/www/html
COPY --from=backend-builder /app/main /usr/local/bin/
CMD ["main"]
总结
多阶段构建真的是 Docker 优化的一把利器,学会了能省不少事儿。它核心思想就是“分离构建环境和运行环境”,用完的依赖就扔掉,只保留最终需要的东西。
不管你是写 Node.js、Go、Python 还是其他语言,都可以尝试用多阶段构建来精简镜像。镜像小了,部署速度快,存储成本也低,何乐而不为呢?
赶紧回去检查一下自己的 Dockerfile,看看有没有优化空间吧!如果有什么问题,欢迎在评论区交流。
Docker 多阶段构建优化镜像
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法