「能跑起来的 Dockerfile」和「能进生产的 Dockerfile」之间隔着体积、安全和构建速度三道坎。本文以一个 Go Web 应用为例,把一个粗糙的 Dockerfile 逐步改到合格,每一步都解释为什么。
反面教材:一把梭
FROM golang:1.23WORKDIR /appCOPY . .RUN go build -o server .CMD ["./server"]它能工作,但问题一堆:镜像近 900MB(整个 Go 工具链都在里面)、构建上下文混入无关文件、以 root 运行、每次改代码都全量重装依赖。
优化一:选对基础镜像
基础镜像是体积的大头,常见档位:
| 镜像 | 体积 | 说明 |
|---|---|---|
golang:1.23 | ~850MB | 完整工具链,只用于构建 |
debian:bookworm-slim | ~80MB | 精简 Debian,适合需要 libc 的动态链接程序 |
alpine | ~8MB | musl libc,极小,但注意 glibc 兼容问题 |
scratch | 0 | 空镜像,静态编译程序的终极选择 |
distroless | ~20MB | Google 出品,只有运行时依赖,无 shell 无包管理器 |
优化二:多阶段构建——构建环境与运行环境分离
核心思想:编译用的镜像不进入最终镜像。第一阶段构建,第二阶段只拷贝产物:
# ---- 构建阶段 ----FROM golang:1.23 AS builderWORKDIR /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-debian12WORKDIR /appCOPY --from=builder /app/server .EXPOSE 8080USER nonroot:nonrootENTRYPOINT ["/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 . . 打进镜像:
.gitnode_modules*.log.env.dockerignoreDockerfiledocs/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直接显示健康状态。
验收清单
构建完成后自查:
docker images myapp # 体积是否达标(Go 静态程序应 < 50MB)docker history myapp:1.0 # 每层是否合理,有没有敏感文件痕迹docker run --rm myapp:1.0 # 能否以非 root 正常启动体积、缓存、安全三件事做到位,这个 Dockerfile 就可以进生产了。
延伸阅读
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 7 天
部分内容可能已过时