要点速览

  • 每条 Dockerfile 指令都会形成一层(layer)并被缓存。
  • COPY / ADD 的输入(即被复制的文件内容)会参与缓存键:如果被复制的文件有任何变化,相关层会失效,后续层必须重建。
  • 所以把“很少变的东西”(比如 go.mod / go.sum)单独复制并 go mod download,能让依赖下载层长期命中缓存,避免每次改代码都重跑依赖安装和后续编译。
  • 另外:优先用 COPY(而不是 ADD),并使用 .dockerignore 排除无关大文件,能显著加速构建。

逐步详解

1) Docker 缓存是怎么工作的

  • Docker 按行读取 Dockerfile,每执行一条指令就会生成一层

  • Docker 在构建时会检查“当前指令 + 指令依赖的输入(比如 COPY 的文件)”是否与之前构建过的某个层相同。

    • 如果相同,就使用缓存层(显示为 Using cachecached),跳过执行该条指令。
    • 如果不同,就重新执行并生成新层,同时使之后的层也需要重建。

2) 为什么 ADD ./shorturl /src/shorturl 是性能坑

  • ADD ./shorturl /src/shorturl 把整个 shorturl 目录一次性复制进镜像。
  • 这个目录里包含源码,只要源码里任意文件改动,这条 COPY/ADD 相关的层就会失效,导致后面的 RUN go build 等都要重新执行,也就是“每次改代码都把后面所有步骤都重跑” —— 很慢。

3) 如何通过分离依赖与源码来利用缓存(核心技巧)

  • 依赖描述文件go.modgo.sum)先单独复制并 go mod download,这样:

    • 当你只改了源码(例如 *.go),依赖层不会失效go mod download 会走缓存,节省时间。
    • 只有当 go.mod/go.sum 真变了,才重新下载依赖。
  • 然后再把源码复制进来并编译 —— 这样只有编译层因源码变化而重跑。

4) .dockerignoreADD/COPY 的关系

  • 构建上下文会被打包发给 Docker Daemon,.dockerignore 决定哪些文件不打包进去。把大文件、不需要的目录(如 .gitnode_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.sumRUN 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 cachecached):

    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:ADDCOPY 有优势吗?
A:除非你需要 ADD 的自动解压或 URL 下载功能,否则优先用 COPY(更显式、更可控)。

Q:把 go mod download 放在 RUN go build 之前有副作用吗?
A:没有,这是常见最佳实践。先安装依赖,再复制源码并构建,能最大化缓存利用。


额外建议

  • 使用 .dockerignore 排除 .gitnode_modules、构建产物等,减小上下文体积。
  • 优先用 COPY 而不是 ADD(除非你需要 ADD 的特殊功能)。
  • DOCKER_BUILDKIT=1 docker build --progress=plain 可获得更详细的构建输出,便于调试缓存命中情况。

https://github.com/0voice

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐