强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Dockerfile 写作精讲 / 03 - COPY 与 ADD

03 - COPY 与 ADD:文件复制指令详解

3.1 指令概览

COPYADD 都用于将文件从构建上下文复制到镜像中,但它们的行为有重要差异。

特性COPYADD
基本文件复制
自动解压 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

支持的压缩格式:gzipbzip2xz

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

模式匹配规则

模式匹配示例不匹配示例
*.logapp.log, dir/error.log
!important.logimportant.log(取消排除)
temptemp/, dir/temp/temporary/
/temp./temp/(仅根目录)dir/temp/
**/temptemp/, 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/
# 使用 --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 编译,第二阶段使用 scratchdistroless/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、清理缓存的最佳实践。