1080 字
5 分钟
Dockerfile 最佳实践:多阶段构建与镜像瘦身

「能跑起来的 Dockerfile」和「能进生产的 Dockerfile」之间隔着体积、安全和构建速度三道坎。本文以一个 Go Web 应用为例,把一个粗糙的 Dockerfile 逐步改到合格,每一步都解释为什么。

反面教材:一把梭#

FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]

它能工作,但问题一堆:镜像近 900MB(整个 Go 工具链都在里面)、构建上下文混入无关文件、以 root 运行、每次改代码都全量重装依赖。

优化一:选对基础镜像#

基础镜像是体积的大头,常见档位:

镜像体积说明
golang:1.23~850MB完整工具链,只用于构建
debian:bookworm-slim~80MB精简 Debian,适合需要 libc 的动态链接程序
alpine~8MBmusl libc,极小,但注意 glibc 兼容问题
scratch0空镜像,静态编译程序的终极选择
distroless~20MBGoogle 出品,只有运行时依赖,无 shell 无包管理器

优化二:多阶段构建——构建环境与运行环境分离#

核心思想:编译用的镜像不进入最终镜像。第一阶段构建,第二阶段只拷贝产物:

# ---- 构建阶段 ----
FROM golang:1.23 AS builder
WORKDIR /app
# 先拷贝依赖清单,利用层缓存
COPY go.mod go.sum ./
RUN go mod download
# 再拷贝源码并构建(CGO_ENABLED=0 产出静态二进制)
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# ---- 运行阶段 ----
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app/server"]

效果:最终镜像从 ~900MB 降到 20MB 以内。拉取更快、攻击面更小、漏洞扫描报告更干净。

几个细节:

  • -ldflags="-s -w" 去掉符号表和调试信息,二进制再小一截。
  • USER nonroot 让进程以非特权用户运行,容器被攻破时攻击者拿到的不是 root。distroless 自带 nonroot 用户。
  • 多阶段构建对 Node、Java 同样适用:Node 的最终镜像只拷 node_modules 生产依赖和构建产物;Java 用 jlink 定制 JRE。

优化三:利用层缓存,让重复构建秒级完成#

Docker 按层缓存:某一层变了,它之后的所有层全部重建。因此 Dockerfile 的指令顺序按「变化频率从低到高」排列:

COPY go.mod go.sum ./ # 依赖清单很少变
RUN go mod download # 上面没变这层就用缓存
COPY . . # 源码天天变,放最后
RUN go build ...

依赖下载层被缓存后,日常改代码的构建不再访问网络,速度差一个数量级。反例就是开头那把梭的写法:COPY . . 在前,任何文件变动都会让 go mod download 缓存失效。

优化四:.dockerignore 管住构建上下文#

docker build 的第一步是把整个目录发给 Docker daemon。没有 .dockerignore 的话,.git、本地依赖、测试数据、密钥文件全都会被发送,甚至可能被 COPY . . 打进镜像:

.git
node_modules
*.log
.env
.dockerignore
Dockerfile
docs/
tests/

这既是体积问题,更是安全问题——.env 进了镜像层,就算后面 RUN rm .env,文件依然存在于历史层中,docker history 可以翻出来。

优化五:其他值得遵守的细节#

  • 一个容器一个进程:用 ENTRYPOINT 直接启动应用,不要在容器里跑 supervisord 管一堆服务——那是虚拟机的玩法。
  • 不要装调试工具进生产镜像:curl、vim 留给排障 sidecar 或临时 docker exec 挂载,常驻只会扩大攻击面。
  • 明确 tag,不用 latest:myapp:1.4.2 可追溯可回滚;latest 让「线上现在跑的到底是哪版」变成悬案。
  • 健康检查写进镜像:HEALTHCHECK CMD wget -qO- http://localhost:8080/healthz || exit 1,让 docker ps 直接显示健康状态。

验收清单#

构建完成后自查:

Terminal window
docker images myapp # 体积是否达标(Go 静态程序应 < 50MB)
docker history myapp:1.0 # 每层是否合理,有没有敏感文件痕迹
docker run --rm myapp:1.0 # 能否以非 root 正常启动

体积、缓存、安全三件事做到位,这个 Dockerfile 就可以进生产了。

延伸阅读#

Dockerfile 最佳实践:多阶段构建与镜像瘦身
https://meow.us.kg/posts/dockerfile-multi-stage-best-practices/
作者
もゆちゃん
发布于
2026-10-15
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取