Docker 中 COPY 和 ADD 的选择

FreeGuideOnline 最新 2026-07-06

dockerfile COPY [--chown=:] <源路径>... <目标路径> COPY [--chown=:] ["<源路径>",... "<目标路径>"]


#### 典型场景

```dockerfile
# 复制单个文件
COPY app.jar /app/

# 复制整个目录(注意:目录本身不复制,复制的是其内容)
COPY ./src /app/src

# 使用通配符复制多个文件
COPY ./config/*.yaml /app/config/

# 设置文件所有者(需要镜像已存在该用户)
COPY --chown=myuser:mygroup ./scripts /home/myuser/scripts

ADD 的“智能”双刃剑

ADD 是较早引入的指令,它实现了 COPY 的全部功能,并附加了两个特性:

  1. 如果 <源> 是一个本地 tar 归档文件(gzip、bzip2、xz 等压缩格式均可),则自动解压到目标目录。
  2. 如果 <源> 是一个 URL,则下载该文件并放入镜像(但不会对下载的 tar 文件进行自动解压)。

正是这两个特性让 ADD 成为引诱与陷阱并存的存在。

自动解压:唯一合理使用场景

当你确实需要将一个 tar 包的内容直接展开在镜像中,并且不关心 tar 包本身时,ADD 可以省去 COPY + RUN tar 的步骤,让 Dockerfile 更简洁。

# 将一个预编译的软件包解压到 /opt
ADD software.tar.gz /opt/

执行后 /opt/ 下直接就是 software.tar.gz 里面的内容,而不是 software.tar.gz 文件本身。这是 ADD 的核心价值。

URL 下载:为什么必须避免

ADD https://example.com/file.tar.gz /tmp/ 会做两件事:下载文件,然后将其放在 /tmp/(注意:是原样放置,不会解压)。这听起来方便,但问题重重:

  • 缓存失效难控制ADD 通过检查 HTTP Last-Modified 头判断文件是否变化。如果远程服务器不支持该头或不返回一致的时间戳,每次构建都会重新下载,导致后续层全部缓存失效。
  • 带来不可预料的文件:你无法在 Dockerfile 中直接审计下载的内容。文件可能被替换,导致构建出一致性被破坏。
  • 增加构建脆弱性:构建过程依赖外部网络状态,拉取失败则构建终止。
  • 镜像层污染:下载的文件单独占据一层。更优的做法是使用 RUN 组合 curlwget,在完成下载、验证后立即清理缓存和临时文件,将操作合并在同一层。

永远用 RUN 替代 ADD 进行远程下载。

# 推荐的下载方式:原子操作,单层完成,可清理
RUN curl -SL https://example.com/file.tar.gz -o /tmp/file.tar.gz \
    && tar -xzf /tmp/file.tar.gz -C /opt/ \
    && rm -f /tmp/file.tar.gz

对比混乱的 ADD 版本:

# 不推荐:ADD 下载后不解压,还需要额外步骤,且无清理
ADD https://example.com/file.tar.gz /tmp/
RUN tar -xzf /tmp/file.tar.gz -C /opt/ && rm /tmp/file.tar.gz

前者不仅更清晰,而且由于 curl 和清理在同一层,最终镜像里不会残留下载的临时文件。

决策流程图:不再纠结

遇到文件复制需求,按以下顺序快速决策:

  1. 只是想从构建上下文复制文件或目录?
    • → 使用 COPY
  2. 需要复制的文件恰好是一个本地 tar 包,而且你希望它被自动解压?
    • → 使用 ADD
  3. 文件来自远程 URL?
    • → 使用 RUN 配合 wgetcurl,并在同一层完成解压、安装和清理。
  4. 需要从上一阶段(多阶段构建)复制成果?
    • → 使用 COPY --from=stage,这是 COPY 的扩展,干净且专为此目的设计。

这个流程可以帮你覆盖超过 99% 的实际构建场景。

常见误区澄清

  • 误区一:ADD 会解压从 URL 下载的 tar 文件。
    • 真相:不会。ADD 只会自动解压本地的 tar 文件。从 URL 下载时,永远只是存放原文件。
  • 误区二:ADD 更高级,所以应该作为默认选择。
    • 真相:COPY 才是为“复制”这一单一动作专门优化的指令,行为简单、缓存可预测,是 Docker 官方明确推荐的默认选择。
  • 误区三:为了极简 Dockerfile,可以用 ADD 完成所有复制和解压缩。
    • 真相:为了极致的可维护性和缓存效率,明确区分指令职责更重要。一个既下载又解压的 RUN 行可以比 ADD+额外步骤更干净。

深入构建缓存:为什么选择影响速度

Docker 基于层缓存加速构建。当某条指令的输入未发生变化时,可以直接复用之前构建的层。

  • COPY 的缓存逻辑:比较构建上下文中文件的内容哈希、元数据等。如果完全相同,则命中缓存。非常稳定。
  • ADD 用于本地文件时,缓存逻辑与 COPY 类似,但需要额外判断是否需要解压;用于 URL 时,依赖远程 Last-Modified 头,或者如果使用特定构建参数(如 --no-cache)则完全失效。

一个真实例子:你使用 ADD http://example.com/config.toml /app/ 拉取配置。第二天远程文件内容没变,但服务器版本更新导致 Last-Modified 变了,你的镜像层缓存立即失效,整个构建链都需要重跑。而如果用 RUN curl ... 并在下载前设置合理的缓存控制或使用固定版本号,你可以实现更可靠的重构。

安全维度:最小权限与可审计性

COPY 仅从构建上下文读取,来源完全受你控制。ADD 的 URL 能力却为供应链攻击打开了潜在窗口——如果远程服务器被攻破或你误用了不可信的 URL,注入的恶意文件将直接进入镜像。

更安全的 DevOps 实践是:将所有外部依赖的下载在早期阶段用 RUN 明确处理,校验哈希值或签名,然后通过多阶段构建用 COPY --from 将验证过的产物带入最终镜像。这样,每一字节的来源都是可追溯、可验证的。

多阶段构建中的黄金搭档

在多阶段构建中,COPYADD 的组合规则同样适用。从先前阶段复制文件时,唯一正确的选择是 COPY --from=<stage>。这保证了跨阶段复制的透明度和缓存有效性。

# 第一阶段:编译
FROM golang:1.21 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app/server

# 第二阶段:运行
FROM alpine:3.19
# 正确:从 builder 阶段复制,清晰无比
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]

不要幻想使用 ADD 从先前阶段带来 tar 并解压——那是不可能且不必要的。

终极实践清单

  • 默认拿起 COPY:任何文件、目录的单纯复制都用它。
  • 解压本地 tar 才用 ADD:并明确在注释里说明为何必须用 ADD。
  • 拒绝 ADD 的 URL 形式:永远用 RUN + 下载工具替代,同时在同一层完成清理。
  • 善用 .dockerignore:无论使用哪个指令,都要排除不必要的文件(如 .git、临时文件)以减少构建上下文大小,提升复制速度。
  • 结合多阶段构建:用 COPY 在外层阶段优雅地组装最终镜像。
  • 添加注释:如果你在团队中使用 ADD,注释解释原因,避免其他人“好心”改回 COPY。
# 使用 ADD 是因为需要直接解压本地 tar 包,避免额外的 RUN tar 层
ADD app-packed.tar.gz /opt/app/
COPY config.yaml /opt/app/