【Docker】通过分离依赖层和逻辑层构建顺序,使得开发中的多次构建情境下,依赖层的构建可以走缓存,加快构建速度
·
要点速览
- 每条 Dockerfile 指令都会形成一层(layer)并被缓存。
- COPY / ADD 的输入(即被复制的文件内容)会参与缓存键:如果被复制的文件有任何变化,相关层会失效,后续层必须重建。
- 所以把“很少变的东西”(比如
go.mod/go.sum)单独复制并go mod download,能让依赖下载层长期命中缓存,避免每次改代码都重跑依赖安装和后续编译。 - 另外:优先用
COPY(而不是ADD),并使用.dockerignore排除无关大文件,能显著加速构建。
逐步详解
1) Docker 缓存是怎么工作的
-
Docker 按行读取 Dockerfile,每执行一条指令就会生成一层。
-
Docker 在构建时会检查“当前指令 + 指令依赖的输入(比如 COPY 的文件)”是否与之前构建过的某个层相同。
- 如果相同,就使用缓存层(显示为
Using cache或cached),跳过执行该条指令。 - 如果不同,就重新执行并生成新层,同时使之后的层也需要重建。
- 如果相同,就使用缓存层(显示为
2) 为什么 ADD ./shorturl /src/shorturl 是性能坑
ADD ./shorturl /src/shorturl把整个shorturl目录一次性复制进镜像。- 这个目录里包含源码,只要源码里任意文件改动,这条 COPY/ADD 相关的层就会失效,导致后面的
RUN go build等都要重新执行,也就是“每次改代码都把后面所有步骤都重跑” —— 很慢。
3) 如何通过分离依赖与源码来利用缓存(核心技巧)
-
把依赖描述文件(
go.mod、go.sum)先单独复制并go mod download,这样:- 当你只改了源码(例如
*.go),依赖层不会失效,go mod download会走缓存,节省时间。 - 只有当
go.mod/go.sum真变了,才重新下载依赖。
- 当你只改了源码(例如
-
然后再把源码复制进来并编译 —— 这样只有编译层因源码变化而重跑。
4) .dockerignore 和 ADD/COPY 的关系
- 构建上下文会被打包发给 Docker Daemon,
.dockerignore决定哪些文件不打包进去。把大文件、不需要的目录(如.git、node_modules、构建产物)排除掉,可以让上下文小很多,构建更快。
5) 额外建议
ADD有自动解压与从 URL 拉取的行为;如果只是复制文件,优先用COPY(更直观、没副作用)。MAINTAINER已被LABEL maintainer="..."取代(不是必须改,但可以注意)。- 检查路径是否写错(下面会指出示例里的可能笔误)。
推荐的的 Dockerfile(分阶段、多层缓存友好)
下面是我把你的优化建议整理并修改后的最终 Dockerfile(带行内注释)。把它放在项目根目录下执行构建(或在 CI 中执行)。
# stage0: build binary
FROM golang:1.23.1 AS builder
# 很少变化:设置 GOPROXY
RUN go env -w GOPROXY=https://goproxy.cn,https://proxy.golang.com.cn,direct
# 把 module 文件先复制进去(仅复制 go.mod 和 go.sum)
# 这样只有当 go.mod/go.sum 改变时才会重新执行下面的依赖下载层
COPY ./shorturl/go.mod ./shorturl/go.sum /src/shorturl/
WORKDIR /src/shorturl
# 下载依赖(如果 go.mod 没变,这一步会走缓存)
RUN go mod download
# 现在复制整个源码(代码改动频繁)
COPY ./shorturl /src/shorturl
# 编译二进制(只有源码改动时才会重新编译)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -o shorturl-server ./shorturl-server
# stage1: final minimal image
FROM quay.io/0voice/alpine:3.18 AS runtime
# 把健康检查二进制复制进镜像(如果只是复制文件,用 COPY)
COPY ./grpc_health_probe-linux-amd64 /usr/bin/grpc_health_probe
RUN chmod +x /usr/bin/grpc_health_probe
LABEL maintainer="chenawsll@outlook.com"
WORKDIR /app
# 把运行时配置文件放进去 —— 注意路径是否正确(见下面的备注)
COPY ./shorturl/dev.config.yaml /app/config.yaml
# 从 builder 拷贝编译好的可执行文件
COPY --from=builder /src/shorturl/shorturl-server ./
EXPOSE 50051
ENTRYPOINT ["./shorturl-server"]
CMD ["--config=config.yaml"]
小细节说明(针对你的原文中的几处)
ADD ./shorturl /src/shorturl→ 建议改为COPY,并按上面分两步:先单独COPY go.mod/go.sum,RUN go mod download,再复制源码。- 你贴的最后一个版本里有条可能是笔误:
ADD ./shorturl/shorturl-server/dev.config.yaml /app/config.yaml—— 很可能应该是COPY ./shorturl/dev.config.yaml /app/config.yaml(除非你的 config 确实在shorturl/shorturl-server/下)。请确认本地文件路径。 MAINTAINER是旧写法,推荐用LABEL maintainer="..."。
如何验证缓存是否生效(实用命令)
-
第一次构建(会完整执行):
docker build -t shorturl-server . -
修改一行代码后再构建(理想情况:只看到编译相关层重新执行,
go mod download那层显示为Using cache或cached):docker build -t shorturl-server . -
强制不使用缓存(用于对比):
docker build --no-cache -t shorturl-server . -
若用 BuildKit(新构建器)可以用更详细输出:
DOCKER_BUILDKIT=1 docker build --progress=plain -t shorturl-server .
常见误区与 FAQ(快速问答)
Q:-f 会改变上下文吗?
A:不会。-f 只改变 Dockerfile 的位置,构建上下文还是你在命令最后指定的目录(例如 .)。
Q:ADD 比 COPY 有优势吗?
A:除非你需要 ADD 的自动解压或 URL 下载功能,否则优先用 COPY(更显式、更可控)。
Q:把 go mod download 放在 RUN go build 之前有副作用吗?
A:没有,这是常见最佳实践。先安装依赖,再复制源码并构建,能最大化缓存利用。
额外建议
- 使用
.dockerignore排除.git、node_modules、构建产物等,减小上下文体积。 - 优先用
COPY而不是ADD(除非你需要ADD的特殊功能)。 - 用
DOCKER_BUILDKIT=1 docker build --progress=plain可获得更详细的构建输出,便于调试缓存命中情况。
更多推荐


所有评论(0)