COPY 更适合常规文件复制,因其语义明确、仅单向拷贝,而 ADD 会隐式解压 tar 或拉取 URL,易导致意外行为;静态文件一律用 COPY,仅明确需解压本地 tar 时才用 ADD。

为什么 COPY 比 ADD 更适合常规文件复制
COPY 是语义明确的复制指令,只做本地文件到镜像层的单向拷贝;ADD 则隐含额外行为(如自动解压 tar、支持 URL 源),容易触发意外层变更或缓存失效。构建时若用 ADD ./src /app,哪怕 src 下有个 config.tar.gz,Docker 就会悄悄解压它——你根本没打算让它解压。
实操建议:
- 所有静态文件、源码、配置都用
COPY,例如COPY package.json .、COPY src/ /app/src/ - 仅在明确需要解压本地 tar 包时才用
ADD,且必须确保该 tar 不含相对路径或危险文件(如../../../etc/passwd) - 避免
ADD从远程 URL 下载内容——它无法被 Docker 构建缓存复用,每次都会重新拉取
多阶段构建中如何安全传递编译产物
多阶段本质是用前一阶段的文件“注入”后一阶段,但直接 COPY --from=0 /app/dist /usr/share/nginx/html 很危险:如果阶段 0 的构建失败或输出路径变更,整个构建就静默出错,且镜像体积可能暴增(比如把 node_modules 和 build 全拷进最终镜像)。
实操建议:
- 显式声明构建阶段名,别依赖数字索引:
FROM node:18 AS builder,后续用COPY --from=builder /app/dist /usr/share/nginx/html - 只拷你需要的产物,不带源码和开发依赖:
COPY --from=builder /app/dist/. /usr/share/nginx/html/(注意末尾斜杠,避免嵌套) - 在 builder 阶段末尾加
RUN ls -la /app/dist(临时调试用),确认输出结构符合预期,再删掉
RUN 指令合并与 shell 缓存失效的坑
Docker 对每条 RUN 单独生成一层并缓存,但缓存只在指令字符串完全一致时才命中。写成 RUN apt-get update && apt-get install -y curl 和 RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* 是两条不同指令,后者无法复用前者缓存。
实操建议:
- 把有依赖关系的命令塞进**单条**
RUN,尤其涉及包管理:RUN apt-get update && apt-get install -y curl wget && rm -rf /var/lib/apt/lists/* - 避免在
RUN中写条件逻辑(如if [ -f ... ]),它会让缓存变得不可预测;改用构建参数或脚本文件 - 不要用
RUN echo "xxx" >> file追加内容——每次都会触发新层;改用heredoc或提前准备好配置文件再COPY
.dockerignore 文件漏配导致镜像臃肿
.dockerignore 不是可选优化项,而是防止敏感文件、冗余目录(node_modules、.git、dist)被意外传入构建上下文的关键防线。一旦漏写 node_modules,Docker 就会把它打包上传,即使你后面用 RUN npm ci 重装,那层体积也已计入镜像。
实操建议:
- 基础必加项:
.git、node_modules、npm-debug.log、.DS_Store、dist(除非你要COPY dist) - 检查是否误写了通配符,比如
**/node_modules比node_modules更稳妥,覆盖子目录 - 用
docker build --no-cache -q .测试忽略效果:如果输出里出现Sending build context to Docker daemon后体积明显变小,说明生效了
分层不是越细越好,关键在每层是否「不可变」且「职责单一」。最容易被忽略的是:构建上下文路径选错、.dockerignore 写了但没生效、多阶段里用了未声明的 stage 名——这些不会报错,只会让镜像变大、变慢、变不可靠。

















