## 7.2 COPY 复制文件

### 何时使用 COPY

`COPY` 是在构建镜像时，将构建上下文（Dockerfile 所在目录及其子目录）中的文件或目录复制到容器内的指令。它是处理应用代码、配置文件最常用的方式。

典型场景：

- 复制应用源码：`COPY . /app`
- 复制配置文件：`COPY nginx.conf /etc/nginx/nginx.conf`
- 复制静态资源：`COPY public /app/public`

**为什么 COPY 比 ADD 更值得推荐？** 笔者建议在 90% 的情况下使用 COPY。原因是 COPY 的语义更清晰——它就是简单地复制文件。而 ADD 有额外的功能（自动解压、支持 URL），这些功能往往会带来意外的行为。除非你明确需要 ADD 的自动解压功能，否则用 COPY。详见 [7.3 ADD 指令](7.3_add.md) 中的详细对比。

### 7.2.1 基本语法

```docker
COPY [选项] <源路径>... <目标路径>
COPY [选项] ["<源路径1>", "<源路径2>", ... "<目标路径>"]
```
`COPY` 指令将构建上下文中的文件或目录复制到镜像内。

---

### 7.2.2 基本用法

#### 复制单个文件

```docker
## 复制文件到指定目录

COPY package.json /app/

## 复制文件并重命名

COPY config.json /app/settings.json
```

#### 复制多个文件

```docker
## 复制多个指定文件

COPY package.json package-lock.json /app/

## 使用通配符

COPY *.json /app/
COPY src/*.js /app/src/
```

#### 复制目录

```docker
## 复制整个目录的内容（不是目录本身）

COPY src/ /app/src/
```
> ⚠️ **注意**：复制目录时，复制的是目录的 **内容**，不包含目录本身。

```bash
构建上下文：              镜像内：
src/                     /app/src/
├── index.js      →      ├── index.js
└── utils.js             └── utils.js
```
---

### 7.2.3 通配符规则

COPY 支持 Go 的 `filepath.Match` 通配符规则：

| 通配符 | 说明 | 示例 |
|--------|------|------|
| `*` | 匹配任意字符序列 | `*.json` |
| `?` | 匹配单个字符 | `config?.json` |
| `[abc]` | 匹配括号内任一字符 | `[abc].txt` |
| `[a-z]` | 匹配范围内字符 | `file[0-9].txt` |

```docker
COPY hom* /mydir/       # home.txt, homework.md 等
COPY hom?.txt /mydir/   # home.txt, homy.txt 等
COPY app[0-9].js /app/  # app0.js ~ app9.js
```
---

### 7.2.4 目标路径

#### 绝对路径

```docker
COPY app.js /usr/src/app/
```

#### 相对路径：基于 WORKDIR

```docker
WORKDIR /app
COPY package.json ./        # 复制到 /app/package.json
COPY src/ ./src/            # 复制到 /app/src/
```

#### 自动创建目录

如果目标目录不存在，Docker 会自动创建：

```docker
## /app/config/ 不存在也会自动创建

COPY settings.json /app/config/
```
---

### 7.2.5 修改文件所有者

使用 `--chown` 选项设置文件的用户和组：

```docker
## 使用用户名和组名

COPY --chown=node:node package.json /app/

## 使用 UID 和 GID

COPY --chown=1000:1000 . /app/

## 只指定用户

COPY --chown=node . /app/
```
> 💡 结合 `USER` 指令使用，确保应用以非 root 用户运行。

---

### 7.2.6 保留文件元数据

COPY 会保留源文件的元数据：

- 读、写、执行权限
- 修改时间

这对于脚本文件特别重要：

```docker
## start.sh 的可执行权限会被保留

COPY start.sh /app/
```
---

### 7.2.7 COPY vs ADD

| 特性 | COPY | ADD |
|------|------|-----|
| 复制本地文件 | ✅ | ✅ |
| 自动解压 tar | ❌ | ✅ |
| 支持 URL | ❌ | ✅ (不推荐)|
| 推荐程度 | ✅ **推荐** | ⚠️ 特殊场景使用 |

```docker
## 推荐：使用 COPY

COPY app.tar.gz /app/
RUN tar -xzf /app/app.tar.gz

## ADD 会自动解压（行为不明显，不推荐）

ADD app.tar.gz /app/
```
> 笔者建议：除非需要自动解压 tar 文件，否则始终使用 COPY。明确的行为比隐式的魔法更好。

---

### 7.2.8 多阶段构建中的 COPY

#### 从其他构建阶段复制

```docker
## 构建阶段

FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

## 生产阶段

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
```

#### 使用 --link 优化缓存

```docker
## 使用 --link 后，文件以独立层添加，不依赖前序指令

COPY --link --from=builder /app/dist /usr/share/nginx/html
```
`--link` 的优势：

- 更高效利用构建缓存
- 并行化构建过程
- 加速多阶段构建

> ⚠️ **注意**：使用 `--link` 需要满足以下条件：
> - 启用 BuildKit（Docker Engine 23.0+ 默认启用）
> - Dockerfile 语法版本 1.4 或更高（在首行声明 `# syntax=docker/dockerfile:1`）
> - 目标路径在前序指令中应不存在或为空目录

---

### 7.2.9 dockerignore

使用 `.dockerignore` 排除不需要复制的文件：

```text
## .dockerignore

node_modules
.git
.env
*.log
Dockerfile
.dockerignore
```
这可以：

- 减小构建上下文大小
- 加速构建
- 避免复制敏感文件

---

### 7.2.10 最佳实践

#### 1. 利用缓存，先复制依赖文件

```docker
## ✅ 好：先复制依赖定义，再安装，最后复制代码

COPY package.json package-lock.json ./
RUN npm install
COPY . .

## ❌ 差：一次性复制所有文件，代码变更会导致重新 npm install

COPY . .
RUN npm install
```

#### 2. 使用 .dockerignore

```docker
## 确保 node_modules 不被复制

COPY . .

## .dockerignore 中应包含 node_modules

...
```

#### 3. 明确复制路径

```docker
## ✅ 好：明确的路径

COPY src/ /app/src/
COPY package.json /app/

## ❌ 差：过于宽泛

COPY . .
```
---

> **🔥 踩坑实录**
>
> 某公司在优化 Node.js 应用的 Docker 镜像时，发现构建出来的镜像体积超过了 2GB，远远超过生产部署的需求。排查发现，Dockerfile 中使用了 `COPY . .` 把整个构建上下文复制进镜像，导致 `node_modules/`、`.git/` 目录和大量测试数据全部被打包进镜像。最初他们没有创建 `.dockerignore` 文件，默认会复制所有文件。解决方案是添加一个 `.dockerignore` 文件，排除这些不必要的目录，使镜像缩小到了 200MB。这个教训深刻地说明了：`.dockerignore` 和 `.gitignore` 一样重要，应该在项目初始化时就创建，而不是等到出现问题时才想起来。建议的标准做法是先复制 `package.json` 和 `package-lock.json` 安装依赖，再复制应用代码，同时在 `.dockerignore` 中明确列出 `node_modules`、`.git`、test 目录等。
