Dockerfile 写作精讲 / 03 - COPY 与 ADD
03 - COPY 与 ADD:文件复制指令详解
3.1 指令概览
COPY 和 ADD 都用于将文件从构建上下文复制到镜像中,但它们的行为有重要差异。
| 特性 | COPY | ADD |
|---|---|---|
| 基本文件复制 | ✅ | ✅ |
| 自动解压 tar 包 | ❌ | ✅ |
| 支持 URL 远程下载 | ❌ | ✅(不推荐) |
| 语义清晰度 | 高 | 低 |
| 推荐使用 | ✅ 默认首选 | 仅在需要自动解压时 |
最佳实践:默认使用
COPY,只在需要自动解压 tar 包时才使用ADD。语义清晰是可维护性的基础。
3.2 COPY 指令详解
基本语法
# 复制单个文件
COPY app.py /app/app.py
# 复制目录(目录本身,包含内容)
COPY src/ /app/src/
# 复制目录内容(注意尾部斜杠的区别在某些场景下很重要)
COPY src /app/src
# 使用通配符
COPY *.py /app/
COPY hom?.txt /app/
# 复制到当前 WORKDIR
COPY requirements.txt .
COPY 的多种形式
# 形式一:COPY <src> <dest>
COPY package.json /app/
# 形式二:COPY ["<src>", "<dest>"](路径含空格时必须用这种形式)
COPY ["my file.txt", "/app/my file.txt"]
# 形式三:COPY --from=<stage>(多阶段构建,详见第 09 章)
COPY --from=builder /app/dist /usr/share/nginx/html
# 形式四:COPY --chown(设置文件所有者)
COPY --chown=appuser:appgroup src/ /app/src/
# 形式五:COPY --chmod(设置文件权限,需要 BuildKit)
COPY --chmod=755 script.sh /app/script.sh
–chown 的注意事项
FROM ubuntu:22.04
RUN groupadd -r appuser && useradd -r -g appuser appuser
# ✅ 使用 --chown 在复制时设置所有权(不产生额外层)
COPY --chown=appuser:appuser . /app/
# ❌ 先复制再 chown(产生额外层,增大镜像体积)
COPY . /app/
RUN chown -R appuser:appuser /app/
注意:
--chown使用用户名时,该用户必须在基础镜像中已经存在。如果使用数字 UID/GID 则无需预先创建。
3.3 ADD 指令详解
自动解压 tar 包
ADD 最有用的特性是自动解压:
# 自动解压 tar.gz 到指定目录
ADD app-v1.0.tar.gz /opt/app/
# 等价于:
# COPY app-v1.0.tar.gz /tmp/
# RUN tar -xzf /tmp/app-v1.0.tar.gz -C /opt/app/ && rm /tmp/app-v1.0.tar.gz
支持的压缩格式:gzip、bzip2、xz
ADD 的 URL 下载(不推荐)
# ❌ 不推荐:使用 ADD 下载远程文件
ADD https://example.com/file.tar.gz /tmp/
# ✅ 推荐:使用 RUN + curl/wget(更灵活,可校验)
RUN curl -fsSL https://example.com/file.tar.gz -o /tmp/file.tar.gz && \
echo "expected_sha256 /tmp/file.tar.gz" | sha256sum -c - && \
tar -xzf /tmp/file.tar.gz -C /opt/ && \
rm /tmp/file.tar.gz
为什么 ADD URL 不推荐:
- 无法校验下载内容的完整性
- 无法在下载失败时重试
- 缓存行为不可控(仅按 URL 缓存,不检查远程文件变化)
- 无法在下载后立即执行清理
ADD 与 COPY 的缓存行为差异
COPY:
缓存键 = 指令文本 + 源文件内容的 checksum
ADD:
本地文件: 缓存键 = 指令文本 + 源文件内容的 checksum
URL: 缓存键 = 指令文本 + URL 字符串(不检查远程文件变化!)
tar 自动解压: 缓存键 = 压缩文件的 checksum(不是解压后的内容)
3.4 .dockerignore 深度指南
基本语法
# 注释
*.md # 排除所有 .md 文件
!README.md # 但保留 README.md(取反规则)
/build # 排除根目录下的 build 目录
**/temp # 排除所有层级的 temp 目录
.DS_Store
.git
.gitignore
node_modules
.env
.env.*
*.log
docker-compose*.yml
Dockerfile*
.vscode
.idea
__pycache__
*.pyc
.mypy_cache
.pytest_cache
.coverage
htmlcov
dist
build
*.egg-info
模式匹配规则
| 模式 | 匹配示例 | 不匹配示例 |
|---|---|---|
*.log | app.log, dir/error.log | — |
!important.log | important.log(取消排除) | — |
temp | temp/, dir/temp/ | temporary/ |
/temp | ./temp/(仅根目录) | dir/temp/ |
**/temp | temp/, a/b/temp/ | — |
temp/ | 仅目录 | temp(文件) |
temp* | temp, temp1, temporary | — |
不同项目的 .dockerignore 模板
Node.js 项目:
node_modules
npm-debug.log*
.nyc_output
coverage
.git
.github
*.md
Dockerfile*
docker-compose*
.env*
Python 项目:
__pycache__
*.pyc
*.pyo
.git
.github
.mypy_cache
.pytest_cache
.venv
venv
*.egg-info
dist
build
.coverage
htmlcov
Dockerfile*
docker-compose*
.env*
Go 项目:
.git
.github
vendor
bin
*.exe
*.test
*.out
Dockerfile*
docker-compose*
.env*
*.md
3.5 COPY 的缓存失效机制
COPY 是最容易导致缓存失效的指令之一。
文件内容校验(checksum)
Docker 使用文件内容的 checksum(而非修改时间)来判断缓存是否失效:
# 假设文件内容不变,即使 touch 更新了时间戳,缓存仍然命中
touch app.py # 仅更新时间戳
docker build -t myapp . # COPY app.py 层仍然命中缓存
减少不必要的缓存失效
# ❌ 错误:COPY . . 在任何文件变化时都会失效
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --production
# ✅ 正确:分层复制,先复制变化频率低的文件
FROM node:20-alpine
WORKDIR /app
# 第一层:依赖清单(变化频率低)
COPY package.json package-lock.json ./
RUN npm ci --production
# 第二层:配置文件(偶尔变化)
COPY tsconfig.json .eslintrc.json ./
# 第三层:源码(频繁变化)
COPY src/ ./src/
# 第四层:静态资源(偶尔变化)
COPY public/ ./public/
利用 BuildKit 的 COPY –link
# 使用 --link 使 COPY 层独立于父层(不影响父层缓存)
FROM ubuntu:22.04
COPY --link package.json /app/
COPY --link src/ /app/src/
--link 的优势:每个 COPY 层可以独立缓存,不会因为前面的层变化而失效。详见第 11 章 BuildKit 高级特性。
3.6 多阶段构建中的 COPY –from
# 阶段一:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段二:生产
FROM nginx:alpine AS production
# 从 builder 阶段复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 也可以从外部镜像复制
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf
# 也可以使用阶段索引(从 0 开始)
COPY --from=0 /app/dist /usr/share/nginx/html
业务场景:在构建 Go 应用时,第一阶段使用
golang:1.22编译,第二阶段使用scratch或distroless/static只包含二进制文件,最终镜像可以小到 10MB 以下。
3.7 实战:不同场景的文件复制策略
场景一:前端构建
FROM node:20-alpine AS builder
WORKDIR /app
# 先复制依赖清单
COPY package.json package-lock.json ./
RUN npm ci
# 复制源码并构建
COPY . .
RUN npm run build
# 生产阶段:Nginx 服务静态文件
FROM nginx:1.25-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
场景二:Go API 服务
FROM golang:1.22-alpine AS builder
WORKDIR /src
# 先复制依赖清单
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /server ./cmd/server
# 生产阶段
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
COPY --from=builder /src/configs /configs
EXPOSE 8080
ENTRYPOINT ["/server"]
场景三:Java Spring Boot
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# 先复制 Maven 配置(利用缓存)
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN ./mvnw dependency:go-offline
# 复制源码并构建
COPY src ./src
RUN ./mvnw package -DskipTests
# 生产阶段
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/target/*.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
3.8 常见错误与排查
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
COPY failed: file not found | 文件不在构建上下文中 | 检查 .dockerignore 和文件路径 |
COPY failed: forbidden path | 路径超出构建上下文 | 不能使用 ../ 访问上级目录 |
no such file: /app/ | 目标目录不存在 | Dockerfile 中 COPY 会自动创建目录 |
| 文件权限不正确 | 以 root 复制,运行时以非 root 读取 | 使用 --chown 设置正确的所有者 |
| tar 文件未被解压 | 使用了 COPY 而非 ADD | 改用 ADD 或手动 tar 解压 |
3.9 扩展阅读
上一章:02 - 基础镜像选择 下一章:04 - RUN 指令 — Shell 形式与 Exec 形式、合并 RUN、清理缓存的最佳实践。