爱库录 目录即站点,文件即文章

Docker多阶段构建实战

2026-09-17 · 技术 · 返回列表

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"]

关键点:

  1. CGO_ENABLED=0 — 禁用 CGO,生成纯静态二进制,不依赖系统 libc
  2. -ldflags="-s -w" — 去掉调试符号和 DWARF 信息,体积再减 30%
  3. 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 是空镜像,意味着:

  • 没有 shelldocker 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.modgo.sum 没变,go mod download 这一层就会被缓存。改了代码不需要重新下载依赖。

在 CI/CD 里,这个优化能把构建时间从 3 分钟降到 30 秒。

总结

Go 项目做多阶段构建几乎零成本:

  1. 把 Dockerfile 拆成 builder + runtime 两阶段
  2. builder 用 golang:alpine,runtime 用 scratchdistroless
  3. CGO_ENABLED=0 + -ldflags="-s -w" 生成静态精简二进制
  4. 依赖文件和源码分开 COPY,利用层缓存

镜像小 = 推拉快 = 部署快 = 省带宽 = 省钱。


相关:[[Caddy vs Nginx:我为什么换到了Caddy]] 里有更多部署相关的经验。