Dockerfile 写作精讲 / 04 - RUN 指令
04 - RUN 指令:Shell 形式与 Exec 形式、合并与清理
4.1 RUN 指令概述
RUN 是 Dockerfile 中最常用的指令之一,用于在镜像构建过程中执行命令。每条 RUN 指令都会在当前镜像层之上创建一个新层,并将结果提交到该层。
# 基本语法
RUN <command> # Shell 形式
RUN ["executable", "param1", "param2"] # Exec 形式
4.2 Shell 形式 vs Exec 形式
Shell 形式
# Shell 形式:通过 /bin/sh -c 执行
RUN apt-get update && apt-get install -y curl
实际执行的是:/bin/sh -c "apt-get update && apt-get install -y curl"
特点:
- 支持变量替换(
$VAR、${VAR}) - 支持管道(
|)、重定向(>)、逻辑运算符(&&、||) - 进程 PID 不是 1(是 shell 的子进程)
Exec 形式
# Exec 形式:直接执行,不经过 shell
RUN ["apt-get", "update"]
实际执行的是:直接调用 apt-get,参数为 update
特点:
- 不经过 shell,不支持变量替换和管道
- 进程直接执行,效率略高
- 适合在没有 shell 的基础镜像中使用
对比表
| 特性 | Shell 形式 | Exec 形式 |
|---|---|---|
| Shell 解析 | ✅ /bin/sh -c | ❌ 直接执行 |
| 环境变量替换 | ✅ $VAR | ❌ |
| 管道/重定向 | ✅ | ❌ |
| 通配符 | ✅ *.txt | ❌ |
| 退出码传播 | ⚠️ 最后一条命令 | ✅ 直接传播 |
| 性能 | ⚠️ 多一个 shell 进程 | ✅ 略优 |
| 无 shell 镜像 | ❌ | ✅ |
何时使用哪种形式
# ✅ Shell 形式:需要管道、变量替换时
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
# ✅ Exec 形式:简单命令,或基础镜像无 shell 时
RUN ["/app/setup.sh", "--config", "/etc/app.conf"]
# ✅ 混合技巧:用 shell 形式执行复杂逻辑
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
gcc \
libc6-dev; \
# 编译某个 C 库
cd /tmp && make && make install; \
# 清理
apt-get purge -y --auto-remove gcc libc6-dev; \
rm -rf /var/lib/apt/lists/* /tmp/*
4.3 合并 RUN 指令
为什么需要合并
每条 RUN 指令生成一层。层是只读的,删除文件不会减小镜像体积(文件仍存在于前一层)。
# ❌ 错误:下载文件后删除并不会减小镜像体积
RUN curl -fsSL https://example.com/app.tar.gz -o /tmp/app.tar.gz
RUN tar -xzf /tmp/app.tar.gz -C /opt/
RUN rm /tmp/app.tar.gz # 删除无效!上一层仍包含该文件
# ✅ 正确:在同一层中下载、解压、清理
RUN curl -fsSL https://example.com/app.tar.gz -o /tmp/app.tar.gz && \
tar -xzf /tmp/app.tar.gz -C /opt/ && \
rm /tmp/app.tar.gz
合并 RUN 的最佳实践
# 系统依赖安装 + 清理
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Alpine 版本
RUN apk add --no-cache \
ca-certificates \
curl \
git \
openssh-client
合并策略的权衡
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 每步一层 | 调试方便 | 层多,可能有残留文件 | 开发调试阶段 |
| 合并同类操作 | 缓存粒度适中 | 推荐 | 生产环境 |
| 全部合并 | 层最少 | 任何改动都重建 | 简单应用 |
# 推荐:按逻辑分组合并
# 组一:系统依赖(变化频率极低)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates curl git && \
rm -rf /var/lib/apt/lists/*
# 组二:应用依赖(偶尔变化)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 组三:源码(频繁变化)
COPY . .
4.4 RUN 的缓存陷阱
RUN 的缓存判断
RUN 指令的缓存是基于指令字符串的,不检查命令的实际执行结果:
# 这条指令每次构建都会命中缓存(字符串未变)
# 即使远程仓库已经更新了!
RUN git clone https://github.com/example/repo.git /app
绕过缓存的方法
# 方法一:使用 --no-cache 构建(全局禁用缓存)
# docker build --no-cache -t myapp .
# 方法二:使用 ARG 创建缓存破坏因子
ARG CACHEBUST=1
RUN git clone https://github.com/example/repo.git /app
# 构建时:docker build --build-arg CACHEBUST=$(date +%s) -t myapp .
# 方法三:使用 BuildKit 的 NOCACHE 指令
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/app/cache \
git clone https://github.com/example/repo.git /app
4.5 RUN 中的包管理器最佳实践
apt-get(Debian/Ubuntu)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
package1 \
package2 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 一行解释:
# --no-install-recommends 不安装推荐包(减小体积)
# apt-get clean 清理 apt 缓存
# rm -rf /var/lib/apt/lists/* 删除包索引(必须在同一 RUN 中)
apk(Alpine)
# --no-cache 不下载索引到本地(减少一层清理步骤)
RUN apk add --no-cache \
package1 \
package2
# 如果需要先更新索引再安装(某些场景)
RUN apk update && \
apk add --no-cache package1 package2 && \
rm -rf /var/cache/apk/*
dnf/yum(Fedora/RHEL/CentOS)
RUN dnf install -y \
package1 \
package2 \
&& dnf clean all \
&& rm -rf /var/cache/dnf
# CentOS 7 使用 yum
RUN yum install -y package1 package2 \
&& yum clean all \
&& rm -rf /var/cache/yum
pip(Python)
# ✅ --no-cache-dir 不缓存下载的包
RUN pip install --no-cache-dir -r requirements.txt
# ❌ 默认会缓存到 ~/.cache/pip,增大镜像体积
RUN pip install -r requirements.txt
npm(Node.js)
# ✅ npm ci:严格按 lockfile 安装,适合 CI/生产
RUN npm ci --production
# ✅ 清理 npm 缓存
RUN npm ci --production && npm cache clean --force
# ✅ pnpm:使用 --frozen-lockfile
RUN pnpm install --frozen-lockfile --prod
4.6 RUN 指令中的权限与用户
FROM ubuntu:22.04
# 默认以 root 运行 RUN
RUN apt-get update && apt-get install -y curl
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 切换到非 root 用户
USER appuser
# 此后的 RUN 指令以 appuser 身份执行
RUN mkdir -p /home/appuser/app
# 需要 root 权限时临时切回
USER root
RUN chown -R appuser:appuser /home/appuser/app
USER appuser
4.7 RUN 中的网络与代理
# 构建时使用代理
# docker build --build-arg HTTP_PROXY=http://proxy:8080 \
# --build-arg HTTPS_PROXY=http://proxy:8080 \
# --build-arg NO_PROXY=localhost,127.0.0.1 .
RUN apt-get update && apt-get install -y curl
# 或在 Dockerfile 中临时设置
RUN HTTP_PROXY=http://proxy:8080 apt-get update
注意事项:使用 BuildKit 时,代理变量不会自动传递给 RUN 指令。需要显式使用
ARG声明:ARG HTTP_PROXY ARG HTTPS_PROXY RUN apt-get update && apt-get install -y curl
4.8 RUN 与安全
避免在 RUN 中泄露敏感信息
# ❌ 错误:密码会留在镜像层中
RUN curl -u admin:secret123 https://private.registry.com/packages.tar.gz
# ✅ 正确:使用 BuildKit 的 secret mount
RUN --mount=type=secret,id=registry_creds \
curl -u $(cat /run/secrets/registry_creds) \
https://private.registry.com/packages.tar.gz
# 构建时传入 secret
docker build --secret id=registry_creds,src=.registry_creds -t myapp .
验证下载文件的完整性
RUN curl -fsSL https://example.com/app-v1.0.tar.gz -o /tmp/app.tar.gz && \
echo "a1b2c3d4e5 /tmp/app.tar.gz" | sha256sum -c - && \
tar -xzf /tmp/app.tar.gz -C /opt/ && \
rm /tmp/app.tar.gz
4.9 常见错误与排查
| 错误 | 原因 | 解决方案 |
|---|---|---|
apt-get: command not found | 基础镜像没有 apt | 选择正确的包管理器(apk/dnf) |
Permission denied | 非 root 用户执行需要 root 的命令 | 临时切换 USER root |
| 层体积异常大 | 未在同一 RUN 中清理缓存 | 合并 RUN 并添加清理命令 |
E: Unable to locate package | 未更新索引 | apt-get update && apt-get install ... |
| 网络超时 | 构建环境网络问题 | 配置代理或使用 --network=host |
| 缓存导致安装旧版本 | RUN 字符串未变 | 修改字符串或使用 --no-cache |
4.10 扩展阅读
上一章:03 - COPY 与 ADD 下一章:05 - ENV 与 ARG — 环境变量、构建参数、作用域差异与秘密变量处理。