## 7.1 RUN 执行命令

### 何时使用 RUN

`RUN` 是 Dockerfile 中最常用的指令，主要用于在镜像构建阶段执行命令来修改镜像。具体来说：

- **安装依赖**：`RUN apt-get install nginx`
- **编译程序**：`RUN gcc -o app main.c`
- **下载文件**：`RUN curl -O https://example.com/file.tar.gz`
- **配置系统**：`RUN mkdir -p /app/data`

理解 RUN 的核心是理解**镜像分层**：每一个 RUN 都会在当前层之上创建新的一层，这会影响镜像大小。因此，合理使用 RUN（特别是合并多个 RUN）是构建轻量级镜像的关键。

### 7.1.1 基本语法

```docker
RUN <command>
RUN ["executable", "param1", "param2"]
```
`RUN` 指令是 Dockerfile 中最常用的指令之一。它在 **当前镜像层** 之上创建一个新层，执行指定的命令，并提交结果。

---

### 7.1.2 两种格式对比

#### 1. Shell 格式

```docker
RUN apt-get update
```
- **特点**：默认通过 `/bin/sh -c` 执行。
- **优势**：可以使用环境变量、管道、重定向等 Shell 特性。
- **示例**：
  ```docker
  RUN echo "Hello" > /test.txt
  ```

#### 2. Exec 格式

```docker
RUN ["apt-get", "update"]
```
- **特点**：直接调用可执行文件，不经过 Shell。
- **优势**：避免 Shell 字符串解析问题，适用于参数中包含特殊字符的情况。
- **注意**：无法使用 `$VAR` 环境变量替换 (除非显式调用 shell)。

---

### 7.1.3 常见最佳实践

#### 1. 组合命令：减少层数

每一个 `RUN` 指令都会新建一层镜像。为了减少镜像体积和层数，应使用 `&&` 连接命令。

**❌ 糟糕的写法** (创建 3 层)：

```docker
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*
```
**✅ 推荐写法** (创建 1 层)：

```docker
RUN apt-get update && \
    apt-get install -y nginx && \
    rm -rf /var/lib/apt/lists/*
```

#### 2. 清理缓存

在安装完软件后，立即清除缓存，可以显著减小镜像体积。

- **Debian/Ubuntu**:
  ```docker
  RUN apt-get update && apt-get install -y package-bar \
      && rm -rf /var/lib/apt/lists/*
  ```

- **Alpine**:
  ```docker
  RUN apk add --no-cache package-bar
  ```

#### 3. 使用 `set -e` 和 `pipefail`

默认情况下，管道命令 `cmd1 | cmd2` 的返回值由最后一个命令 (`cmd2`) 决定，即使前面的命令失败了，整个 `RUN` 仍可能视为成功。

**❌ 隐蔽的错误**：

```docker
## 如果下载失败，gzip 可能会报错，但如果不影响后续，构建可能继续

RUN wget http://error-url | gzip -d > file
```
**✅ 推荐写法**：

```docker
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN wget http://url | gzip -d > file
```
---

### 7.1.4 常见问题

#### Q：为什么 `RUN cd /app` 不生效？

```docker
RUN cd /app
RUN touch hello.txt
```
**结果**：`hello.txt` 会出现在根目录 `/`，而不是 `/app`。**原因**：每个 `RUN` 都在一个新的 Shell/容器环境中执行。`cd` 只影响当前 `RUN` 的环境。**解决**：使用 `WORKDIR` 指令。

```docker
WORKDIR /app
RUN touch hello.txt
```

#### Q：环境变量不生效？

```docker
RUN export MY_VAR=hello
RUN echo $MY_VAR
```
**结果**：输出为空。**原因**：同上，环境变量只在当前 `RUN` 有效。**解决**：使用 `ENV` 指令，或在同一行 `RUN` 中导出。

```docker
ENV MY_VAR=hello
RUN echo $MY_VAR
```
---

### 7.1.5 高级技巧

#### 1. 使用 BuildKit 的挂载缓存

BuildKit 支持在 `RUN` 指令中使用 `--mount` 挂载缓存，加速构建。

```docker
## 缓存 apt 包

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y gcc
```
```docker
## 缓存 Go 模块

RUN --mount=type=cache,target=/go/pkg/mod \
    go build -o app
```

#### 2. 挂载密钥

安全地使用 SSH 密钥或 Token，而不将其记录在镜像中。

```docker
RUN --mount=type=secret,id=mysecret \
    cat /run/secrets/mysecret
```

#### 3. Heredoc 语法

BuildKit 支持使用 heredoc 语法编写多行脚本，无需行末反斜杠 `\` 连接：

```docker
RUN <<EOF
apt-get update
apt-get install -y \
  build-essential \
  curl \
  git
rm -rf /var/lib/apt/lists/*
EOF
```

优势：

- 可读性更强，不需要在每行末尾添加 `\`
- 避免在脚本中转义引号
- 支持多个 heredoc 块，可指定不同的 Shell

```docker
RUN <<EOF
echo "使用默认 /bin/sh"
EOF

RUN <<'EOF'
#!/bin/bash
set -euo pipefail
echo "使用 bash"
wget http://example.com/file.tar.gz
EOF
```

> 使用 heredoc 需要在 Dockerfile 首行声明 BuildKit 语法版本：`# syntax=docker/dockerfile:1`

---
