导读:本期聚焦于孙志远创作的《Dockerfile 中 RUN、COPY、ADD 指令到底该怎么用?常见误区与最佳实践详解》,敬请观看详情。编写 Dockerfile 时,RUN、COPY、ADD 是使用频率最高的三个指令,但也是最容易写错的地方。RUN 负责在镜像构建阶段执行命令,COPY 用于把本地文件复制进镜像,而 ADD 看起来像是加强版的 COPY,实际上暗藏不少坑。本文围绕这三条指令展开,先讲清 RUN 的两种写法 shell 形式与 exec 形式的区别,分析为什么推荐使用 exec 形式;再对比 COPY 与 ADD 的差异,说明 ADD 自动解压和远程 URL 功能何时该用何时不该用;最后结合分层缓存机制,给出利用指令顺序和缓存特性优化构建速度的实用技巧,帮助写出更快、更小、更可靠的镜像。

Dockerfile 里最常被写错的三个指令,大概就是 RUN、COPY 和 ADD。RUN 负责执行命令,COPY 和 ADD 负责搬运文件,看起来简单,实际写起来却有很多细节:RUN 写成 shell 形式还是 exec 形式?ADD 能干的 COPY 是不是都能干?为什么有时候缓存全部失效,构建慢得像全量重来?这篇文章把这三个指令一次讲透。

Dockerfile 中 RUN、COPY、ADD 指令到底该怎么用?常见误区与最佳实践详解

RUN 指令:shell 形式与 exec 形式的区别

RUN 指令用于在镜像构建过程中执行命令,并把结果提交为新的镜像层。它有两种写法,第一种是 shell 形式,直接跟命令字符串;第二种是 exec 形式,用 JSON 数组的方式传入命令和参数。

FROM ubuntu:22.04

# shell 形式
RUN apt-get update && apt-get install -y curl

# exec 形式
RUN ["apt-get", "install", "-y", "vim"]

两者的核心区别在于:shell 形式的命令会被包装成 /bin/sh -c "你的命令" 来执行,所以可以使用 shell 的变量替换、管道、通配符等特性;而 exec 形式会直接调用系统调用执行指定程序,没有 shell 参与。这也意味着 exec 形式里写 RUN ["echo", "$HOME"] 是拿不到环境变量的,因为变量替换依赖 shell,而这里根本没有 shell。

那为什么很多场合仍然推荐 exec 形式?关键在于信号处理。容器中 PID 为 1 的进程如果是通过 shell 形式启动的,shell 不会把 SIGTERM 信号转发给子进程,导致 docker stop 时进程无法优雅退出,只能等超时后被 SIGKILL 强杀。虽然 RUN 只在构建阶段执行,不直接影响运行时进程,但同样的书写习惯延伸到 CMD 和 ENTRYPOINT 时就会踩坑,所以养成 exec 形式的习惯是有价值的。

另一个实用技巧是把多条命令用 && 串联在同一条 RUN 里。Dockerfile 的每条指令都会生成一个层,如果 apt-get updateapt-get install 分成两条 RUN,最终镜像里会保留 update 产生的缓存层,白白增加体积。合并写还能顺手清理缓存:

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

注意 rm 必须和前面的安装命令放在同一个 RUN 里。分层文件系统里,后面一层的删除并不能真正抹掉前面一层的文件,只是生成了一个"删除标记",镜像总体积不变。

COPY 与 ADD:能用 COPY 就不要用 ADD

COPY 和 ADD 都能把文件放进镜像,功能高度重叠,官方建议是优先使用 COPY。原因在于 ADD 比 COPY 多了两个"隐式行为",这些行为在你不了解时容易造成意外。

第一个隐式行为是自动解压。如果源文件是一个本地压缩包(如 tar.gz、tar.bz2),ADD 会自动把它解压到目标目录:

FROM alpine:3.19
# 自动解压到 /app 目录下
ADD app.tar.gz /app/
# 而下面这条只是原样复制一个压缩文件
COPY app.tar.gz /app/app.tar.gz

如果你确实需要解压,ADD 这个特性很方便,省了一条 RUN tar 命令。但问题在于读 Dockerfile 的人不一定知道这个行为,看到 ADD 未必意识到文件会被解开。反过来,如果你只是想复制一个压缩包(比如后续在容器里运行时再解压),用 ADD 就会得到错误结果。

第二个隐式行为是支持远程 URL。ADD 可以直接写 ADD https://ipipp.com/file.tar.gz /tmp/ 这样的远程地址。但这个功能的实现很粗糙:Docker 会用 curl 或 wget 把文件下载下来,没有认证支持,没有超时控制,也不会对下载结果做校验,而且下载的文件权限是 600,常常还需要额外一条 RUN 去改权限。官方明确建议,需要下载远程文件时用 RUN 配合 curl 或 wget 自己处理,还能利用缓存控制:

RUN curl -fsSL https://ipipp.com/file.tar.gz -o /tmp/file.tar.gz \
    && tar -xzf /tmp/file.tar.gz -C /opt \
    && rm /tmp/file.tar.gz

两条指令在复制时的另一个细节是目标路径的结尾斜杠。如果目标以 / 结尾,会被视为目录;如果结尾没有斜杠,会被视为文件名。写成 COPY config.ini /etc/app/COPY config.ini /etc/app 的结果完全不同,后者会把 app 当作文件名覆盖内容。

此外,COPY 还有一个 ADD 没有的能力:可以用 --from 参数从多阶段构建的 earlier stage 或者基础镜像中复制文件,这是多阶段构建的基础用法,Go、Java 项目里编译和运行分离几乎都靠它:

FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /out/server ./cmd/server

FROM alpine:3.19
COPY --from=builder /out/server /usr/local/bin/server
ENTRYPOINT ["server"]

利用缓存机制优化构建速度

Docker 构建时会逐条检查指令缓存。对于 RUN,只要命令文本没变,缓存就有效;对于 COPY 和 ADD,Docker 会校验源文件的校验和,文件内容变了缓存就失效。理解这条规则,就能通过调整指令顺序大幅加速构建。

最常见的反面教材是把 COPY 放在最前面:

COPY . /app
RUN go mod download
RUN go build -o /app/server .

这样写的后果是,只要项目里任何一个源码文件改动,第一层 COPY 缓存失效,后面所有 RUN 全部重跑,依赖下载也要从头来一遍。正确做法是先复制依赖清单,下载依赖,再复制全部源码:

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server .

改动业务代码后,前两层的缓存依然有效,只有最后的构建需要重新执行,构建时间可以从几分钟缩短到几十秒。Node.js 项目先复制 package.json、Python 项目先复制 requirements.txt,都是同样的思路。

配合 .dockerignore 文件一起使用效果更佳。把 .gitnode_modules、日志文件等排除在构建上下文之外,既能减小上下文传输体积,又能避免无关文件变动导致 COPY 缓存意外失效。最后再配合多阶段构建,把编译工具链留在 builder 阶段,最终镜像里只放运行产物,RUN、COPY、ADD 三条指令各司其职,就能得到一个又快又小的生产级镜像。

总结

记住三条原则:RUN 优先用 exec 形式并合并相关命令,减少层和控制体积;复制文件一律首选 COPY,只有明确需要自动解压本地 tar 包时才用 ADD,远程下载交给 curl;把变动最频繁的内容放在 Dockerfile 靠后的位置,让缓存尽可能命中。写 Dockerfile 时多想一步,构建速度和镜像体积都会给你回报。

DockerfileRUN指令COPY和ADD区别修改时间:2026-09-13 18:56:49

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56183.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。