Docker多阶段构建实战
title: "Docker 多阶段构建:镜像从 1.2GB 到 12MB 的实战" date: 2026-09-14 tags: [Docker, 容器化, 部署, 优化] category: 技术 description: "一个 Go 项目的 Docker 镜像优化过程。多阶段构建 + scratch 基础镜像,体积缩小 100 倍。"
Docker 多阶段构建:镜像从 1.2GB 到 12MB 的实战
项目上线前做了最后一轮优化,发现 Docker 镜像竟然有 1.2GB。
一个 Go 编译出来的二进制才 20MB,凭什么镜像 1.2GB?
问题出在哪
最初的 Dockerfile 很朴素:
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
EXPOSE 8080
CMD ["./server"]
golang:1.22 基础镜像本身就 800MB+,加上源码和编译缓存,轻松破 1GB。
但运行时只需要那个编译好的二进制文件。 Go 编译器、源码、依赖缓存,全是运行时不需要的东西。
多阶段构建
把「构建」和「运行」拆成两个阶段:
# ===== 阶段一:构建 =====
FROM golang:1.22-alpine AS builder
WORKDIR /app
# 先复制依赖文件,利用 Docker 缓存层
COPY go.mod go.sum ./
RUN go mod download
# 再复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# ===== 阶段二:运行 =====
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
关键点:
CGO_ENABLED=0— 禁用 CGO,生成纯静态二进制,不依赖系统 libc-ldflags="-s -w"— 去掉调试符号和 DWARF 信息,体积再减 30%FROM scratch— 空镜像,什么都不带,只有你 COPY 进去的东西
结果对比
| 阶段 | 镜像大小 |
|------|---------|
| FROM golang:1.22 | 1.2GB |
| FROM golang:1.22-alpine | 350MB |
| 多阶段 + alpine | 25MB |
| 多阶段 + scratch | 12MB |
从 1.2GB 到 12MB,缩小 100 倍。
scratch 的坑
scratch 是空镜像,意味着:
- 没有 shell —
docker exec -it container sh进不去。调试用docker logs。 - 没有 CA 证书 — 如果要调 HTTPS 外部接口,需要手动复制证书:
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - 没有时区数据 — 需要复制:
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo - 没有
/tmp目录 — 某些程序需要,手动mkdir。
如果嫌 scratch 坑太多,用 gcr.io/distroless/static 替代 — 极简但带 CA 证书和时区:
FROM gcr.io/distroless/static
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
镜像大小约 18MB,比 scratch 大一点但省心很多。
依赖层缓存优化
多阶段构建还有个好处:利用 Docker 层缓存加速构建。
# 先复制依赖清单
COPY go.mod go.sum ./
RUN go mod download
# 再复制源码
COPY . .
RUN go build ...
只要 go.mod 和 go.sum 没变,go mod download 这一层就会被缓存。改了代码不需要重新下载依赖。
在 CI/CD 里,这个优化能把构建时间从 3 分钟降到 30 秒。
总结
Go 项目做多阶段构建几乎零成本:
- 把 Dockerfile 拆成 builder + runtime 两阶段
- builder 用
golang:alpine,runtime 用scratch或distroless CGO_ENABLED=0+-ldflags="-s -w"生成静态精简二进制- 依赖文件和源码分开 COPY,利用层缓存
镜像小 = 推拉快 = 部署快 = 省带宽 = 省钱。
相关:
[[Caddy vs Nginx:我为什么换到了Caddy]]里有更多部署相关的经验。