强曰为道

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

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 — 环境变量、构建参数、作用域差异与秘密变量处理。