Docker 基础体系 · 第 33/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker ARG、ENV、LABEL 与 Metadata:作用域、继承和敏感边界
在 Dockerfile 中,ARG、ENV 和 LABEL 都表现为“给构建过程或镜像附加一个键值”,但它们属于不同的状态空间:
ARG:构建参数,主要服务于 Dockerfile 解析和构建过程。ENV:环境变量,既影响后续构建步骤,也可成为运行时默认环境。LABEL:镜像标签,属于镜像元数据,不是进程环境变量。- Metadata:更大的概念,包含镜像配置、标签、历史、清单、平台信息、签名与构建证明等。
如果只把三者都理解成“变量”,就容易产生三个危险误判:
- 认为
ARG会自动出现在运行中的容器里; - 认为
ENV只是构建时临时配置; - 认为
LABEL是安全的备注字段,可以放入任意构建信息或凭据。
下面从作用域、继承、持久化位置和泄漏边界逐层展开。
一、先区分 Docker 的几类状态
一个镜像构建并不是单一过程。至少可以区分为四类状态:
flowchart LR
A[Dockerfile 与构建上下文] --> B[BuildKit 构建状态]
B --> C[镜像配置 Image Config]
B --> D[镜像层 Filesystem Layers]
B --> E[Manifest / Index]
C --> F[运行时容器配置]
D --> F
E --> G[镜像分发与平台选择]
B --> H[Provenance / Attestation]
1. 构建状态
BuildKit 在处理 Dockerfile 时需要保存:
- 当前阶段;
- 当前阶段可见的
ARG; - 已设置的
ENV; - 文件系统快照;
- 指令依赖关系;
- 缓存键;
- 构建参数和平台参数;
- Secret、SSH 等临时挂载。
这一状态只服务于构建。构建完成后,构建状态中的某些数据可能被丢弃,但不能因此假设它们从未进入缓存、历史、日志或构建证明。
2. 文件系统层
RUN, COPY, ADD 等指令通常会改变镜像文件系统,最终形成一个或多个层。文件本身、文件内容和文件权限属于这部分。
例如:
RUN echo "hello" > /app/message.txt
这里的 message.txt 是文件系统内容。
3. 镜像配置
镜像配置通常包含:
- 默认环境变量;
- 默认入口点和命令;
- 工作目录;
- 用户;
- 暴露端口;
- 根文件系统层的引用;
- 镜像历史;
- 标签等配置数据。
ENV 和 LABEL 的结果主要进入镜像配置,而不是普通应用文件。
可以通过以下命令观察部分结果:
docker image inspect example:latest
典型结构中会看到类似字段:
{
"Config": {
"Env": [
"APP_ENV=production"
],
"Labels": {
"org.opencontainers.image.title": "example"
}
},
"RootFS": {
"Layers": []
},
"History": []
}
实际输出还会包含架构、创建时间、入口点、层摘要等信息。不要把 Config.Env、Config.Labels 和根文件系统内容混为一谈。
4. Manifest、Index 和构建证明
镜像 manifest 描述镜像层和镜像配置对象的摘要。多平台镜像还会有 manifest list,也称 image index,用来映射:
linux/amd64 -> amd64 镜像 manifest
linux/arm64 -> arm64 镜像 manifest
BuildKit 还可能生成 provenance、SBOM 或其他 attestation。它们不一定等同于镜像配置,但可能记录构建输入、源码、构建参数或构建过程信息。
因此,“没有写入最终文件系统”不等于“不会出现在任何构建产物中”。
二、ARG:构建期参数与阶段作用域
2.1 ARG 的基本语义
ARG 声明一个只能在构建期间使用的参数:
ARG VERSION=1.0
RUN echo "building version ${VERSION}"
构建时可以覆盖默认值:
docker build \
--build-arg VERSION=2.0 \
-t example:2.0 .
在上述构建中,RUN 可以读取 VERSION=2.0。但容器启动后,默认不会存在名为 VERSION 的环境变量:
docker run --rm example:2.0 sh -c 'printf "<%s>\n" "${VERSION-unset}"'
预期结果是:
<unset>
这是因为 ARG 是构建变量,不是运行时环境变量。
2.2 ARG 的阶段作用域
Dockerfile 的多阶段构建使 ARG 具有明确作用域。
ARG GLOBAL_NAME=global
FROM alpine:3.20 AS first
ARG STAGE_NAME=first
RUN echo "global=${GLOBAL_NAME}, stage=${STAGE_NAME}"
FROM alpine:3.20 AS second
RUN echo "global=${GLOBAL_NAME}"
这个例子不能按直觉工作:
GLOBAL_NAME在第一个FROM之前声明;- 它可以用于
FROM; - 但它不会自动进入
first或second阶段; STAGE_NAME只在first阶段可见;second阶段也看不到GLOBAL_NAME,因为没有重新声明。
正确写法是:
ARG GLOBAL_NAME=global
FROM alpine:3.20 AS first
ARG GLOBAL_NAME
ARG STAGE_NAME=first
RUN echo "global=${GLOBAL_NAME}, stage=${STAGE_NAME}"
FROM alpine:3.20 AS second
ARG GLOBAL_NAME
RUN echo "global=${GLOBAL_NAME}"
这里的第二个 ARG GLOBAL_NAME 不是重新指定一个独立默认值,而是把顶层构建参数引入当前阶段。构建时传入的值可以继续使用。
作用域可以形式化为:
G:第一次FROM之前的全局参数集合;S_i:第i个构建阶段显式声明的参数集合;Parent(i):阶段i的父阶段,如果该阶段基于另一个构建阶段;VisibleARG(i):阶段i中可用的构建参数。
对于普通的顶层参数:
VisibleARG(i) = S_i
对于父阶段继承:
VisibleARG(child) = VisibleARG(parent) ∪ S_child
但这里有一个重要条件:父阶段必须是另一个构建阶段,例如:
FROM alpine:3.20 AS base
ARG BUILD_MODE=release
FROM base AS child
RUN echo "${BUILD_MODE}"
child 可以继承 base 中已经声明的 ARG。
如果是直接从外部镜像开始:
FROM alpine:3.20 AS child
RUN echo "${BUILD_MODE}"
它不会知道其他 Dockerfile 阶段中的参数。
2.3 ARG 在 FROM 中的特殊位置
构建基础镜像的参数必须在使用它的 FROM 之前声明:
ARG ALPINE_VERSION=3.20
FROM alpine:${ALPINE_VERSION}
以下写法不成立:
FROM alpine:${ALPINE_VERSION}
ARG ALPINE_VERSION=3.20
因为 Docker 在处理 FROM 时还没有得到这个参数。
完整示例:
ARG ALPINE_VERSION=3.20
ARG TARGETPLATFORM
FROM --platform=${TARGETPLATFORM} alpine:${ALPINE_VERSION}
ARG TARGETPLATFORM
RUN echo "target platform: ${TARGETPLATFORM}"
这里有两个不同问题:
- 顶层
ARG ALPINE_VERSION用于解析FROM; ARG ALPINE_VERSION若要在RUN中再次使用,需要在阶段内重新声明;TARGETPLATFORM是 BuildKit 提供的自动平台参数之一;- 自动平台参数可用于
FROM,若要在阶段的普通指令中使用,也应在阶段内声明。
常见自动参数包括:
BUILDPLATFORM:执行构建的目标平台;TARGETPLATFORM:最终镜像目标平台;BUILDOS、BUILDARCH;TARGETOS、TARGETARCH;TARGETVARIANT等。
这些参数解决的是跨平台构建中的平台选择问题,不代表容器运行时自动获得同名环境变量。
2.4 ARG 与缓存
构建参数可能参与缓存键计算,但影响范围取决于它是否被实际使用。
FROM alpine:3.20
ARG VERSION
RUN echo "version=${VERSION}" > /version.txt
RUN apk add --no-cache curl
当 VERSION 改变时,使用它的第一条 RUN 指令需要重新执行。其后的指令通常也会因为前一层发生变化而失去缓存。
如果某个参数只是声明但没有被指令使用:
FROM alpine:3.20
ARG UNUSED
RUN echo "constant"
它通常不会因为单纯改变 UNUSED 就让这个 RUN 失效。这里应区分规范语义和实现细节:BuildKit 会根据指令实际依赖构造缓存,但不能把所有未使用参数的行为当成业务逻辑依赖。
构建参数还可能进入:
- 构建日志;
- 镜像历史的指令表示;
- BuildKit 缓存记录;
- provenance 等构建证明。
因此 ARG 不是 Secret 机制。
三、ENV:构建期环境与运行时默认环境
3.1 ENV 同时影响两个时期
ENV 设置环境变量:
FROM alpine:3.20
ENV APP_ENV=production
RUN echo "build environment: ${APP_ENV}"
CMD ["sh", "-c", "echo runtime environment: ${APP_ENV}"]
它有两个效果:
- 对后续 Dockerfile 指令可见;
- 写入镜像配置,作为由该镜像创建的容器的默认环境。
构建时的 RUN 使用的是构建阶段环境;运行时的 CMD 使用的是容器进程环境。两者处于不同生命周期,但都可以看到这个 ENV。
运行:
docker build -t env-demo .
docker run --rm env-demo
预期输出:
runtime environment: production
如果覆盖运行时变量:
docker run --rm -e APP_ENV=staging env-demo
输出变为:
runtime environment: staging
因此,镜像中的 ENV 是默认值,不是不可修改的强制值。
3.2 ENV 的顺序关系
变量通常只对后续指令可见:
FROM alpine:3.20
RUN echo "before=${APP_MODE-unset}"
ENV APP_MODE=production
RUN echo "after=${APP_MODE}"
输出类似:
before=unset
after=production
同一条 ENV 指令中也应注意展开关系:
ENV APP_DIR=/opt/app \
APP_BIN=${APP_DIR}/bin
Dockerfile 变量替换遵循指令解析时可见的环境值。为了降低歧义,更稳妥的写法是拆开:
ENV APP_DIR=/opt/app
ENV APP_BIN=${APP_DIR}/bin
RUN 的 shell 形式和 exec 形式也不同:
ENV GREETING=hello
RUN echo "$GREETING"
RUN ["sh", "-c", "echo $GREETING"]
RUN ["echo", "$GREETING"]
前两条会通过 shell 展开变量;最后一条直接执行 echo,不会由 Docker 自动把 "$GREETING"替换成 hello,输出通常是字面量:
$GREETING
这里的关键不是 ENV 是否存在,而是变量由谁展开:
- Dockerfile 指令解析器可以做一部分环境替换;
- shell 可以做 shell 变量展开;
- exec-form 程序自身不会自动进行 shell 展开。
3.3 ENV 的继承
如果一个阶段基于另一个阶段,则父阶段的环境变量会进入子阶段:
FROM alpine:3.20 AS base
ENV APP_HOME=/srv/app
FROM base AS build
RUN echo "${APP_HOME}"
build 可以看到 /srv/app。
但如果只是从阶段中复制文件:
FROM alpine:3.20 AS base
ENV APP_HOME=/srv/app
RUN mkdir -p "${APP_HOME}"
FROM alpine:3.20 AS final
COPY --from=base /srv/app /srv/app
RUN echo "${APP_HOME-unset}"
final 只复制了文件,不继承 base 的 ENV。输出中的变量会是 unset。
这说明:
COPY --from=stage
传递的是文件系统内容;而:
FROM stage
建立的是父阶段关系,能够继承部分构建和镜像配置状态。
3.4 ARG 与 ENV 同名时的优先关系
常见模式如下:
FROM alpine:3.20
ARG APP_MODE=production
ENV APP_MODE=${APP_MODE}
RUN echo "mode=${APP_MODE}"
这里的含义是:
ARG APP_MODE提供构建期输入;ENV APP_MODE=${APP_MODE}把它转换为镜像默认环境;- 后续的
RUN和运行中的容器都可以使用APP_MODE。
但 ENV 一旦设置,就会覆盖同名 ARG 在后续指令中的作用:
FROM alpine:3.20
ARG APP_MODE=production
ENV APP_MODE=debug
RUN echo "${APP_MODE}"
输出是:
debug
形式化地说,在某个阶段中,构建指令读取名为 x 的变量时:
Value(x) =
ENV(x),如果当前阶段已经设置 ENV(x)
ARG(x),否则如果当前阶段可见 ARG(x)
未定义,否则
这也是为什么“使用同名 ARG 和 ENV”虽然方便,却会增加排查成本。若两者含义不同,最好使用不同名称,例如:
ARG BUILD_VERSION
ENV APP_VERSION=${BUILD_VERSION}
四、LABEL:镜像元数据而不是环境变量
4.1 LABEL 的语义
LABEL 为镜像增加键值形式的元数据:
FROM alpine:3.20
LABEL org.opencontainers.image.title="example" \
org.opencontainers.image.description="A small demo image" \
org.opencontainers.image.version="1.0.0"
查看:
docker image inspect example:latest \
--format '{{json .Config.Labels}}'
可能得到:
{
"org.opencontainers.image.title": "example",
"org.opencontainers.image.description": "A small demo image",
"org.opencontainers.image.version": "1.0.0"
}
LABEL 不会:
- 创建环境变量;
- 创建文件;
- 自动传给应用进程;
- 自动改变容器行为;
- 替代镜像版本标签,例如
example:1.0.0。
应用若要读取镜像标签,通常需要由外部工具通过 Docker API 或镜像检查命令读取,而不是从进程环境中读取。
4.2 LABEL 的继承与覆盖
如果一个阶段基于父阶段,父镜像的标签可以被继承。相同键再次设置时,后面的值覆盖前面的值:
FROM alpine:3.20 AS base
LABEL com.example.owner="team-a" \
com.example.tier="base"
FROM base
LABEL com.example.owner="team-b"
结果中通常是:
com.example.owner = team-b
com.example.tier = base
“继承”并不表示每个标签都应该由应用依赖。基础镜像的标签可能描述基础镜像的构建者、版本或来源;最终镜像可以继续保留这些标签,也可以覆盖冲突键。
4.3 LABEL 可以引用构建变量
可以用 ARG 或 ENV 生成标签:
FROM alpine:3.20
ARG VERSION=dev
LABEL org.opencontainers.image.version="${VERSION}"
构建:
docker build \
--build-arg VERSION=2.4.1 \
-t example:2.4.1 .
查询:
docker image inspect example:2.4.1 \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}'
输出:
2.4.1
这里 VERSION 最终进入了镜像元数据。若把它换成令牌:
ARG REGISTRY_TOKEN
LABEL com.example.registry-token="${REGISTRY_TOKEN}"
令牌就可能进入镜像配置以及后续的镜像分发对象,任何能拉取并检查镜像的人都可能获得它。LABEL 是公开元数据边界,不是私密存储。
五、Metadata 的完整边界:镜像标签只是其中一部分
5.1 Docker 镜像 Metadata
本文中的 Metadata 指描述镜像及其构建来源的非业务文件内容信息。常见组成如下:
| 类型 | 典型内容 | 是否等同于文件系统 |
|---|---|---|
| Image Config | ENV、入口点、工作目录、用户、标签 |
否 |
| History | Dockerfile 指令的历史记录及层关联 | 否 |
| Manifest | 配置对象和层的摘要、媒体类型 | 否 |
| Image Index | 多平台镜像映射 | 否 |
| Provenance | 构建来源、输入、构建器等 | 否 |
| SBOM/Attestation | 软件清单或其他证明 | 否 |
| Container Config | 创建容器时的运行时覆盖设置 | 否 |
文件系统层中的一个配置文件也可以被视为应用配置,但它不是镜像 Metadata 的同一层概念。
5.2 查看镜像历史
docker history --no-trunc example:2.4.1
历史记录可能展示类似:
RUN echo "version=2.4.1" > /version.txt
ENV APP_ENV=production
LABEL org.opencontainers.image.version="2.4.1"
具体是否完整显示、是否经过 BuildKit 处理、是否显示参数值,受构建器和镜像格式影响。不能依赖“某次命令没有显示出来”来判断数据安全。
5.3 查看远程镜像的多平台 Metadata
使用 Buildx 时可以查看镜像索引:
docker buildx imagetools inspect alpine:3.20
该命令可以帮助确认镜像支持的目标平台及其摘要。它观察的是分发层级的 metadata,不等于查看某一个容器内的环境变量。
六、敏感边界:为什么 ARG、ENV、LABEL 都不能存 Secret
6.1 Secret 的定义
Secret 是一旦泄露就可能造成权限扩大或数据访问的值,例如:
- 云服务访问密钥;
- 私有仓库令牌;
- SSH 私钥;
- 数据库密码;
- 签名密钥;
- API Token。
判断一个值是否敏感,不取决于它叫不叫 PASSWORD,而取决于泄露后是否能代表某个身份或权限。
6.2 把 Secret 放进 ARG 的失败路径
FROM alpine:3.20
ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken "${NPM_TOKEN}" \
&& npm install \
&& npm config delete //registry.example.com/:_authToken
即使最后删除配置,仍有多个泄漏路径:
RUN指令及其参数可能进入构建历史;- 构建日志可能打印命令或错误信息;
- 缓存记录可能关联该次构建输入;
- provenance 可能记录构建参数;
- 安装过程可能把令牌写入其他文件或错误报告;
- 恶意依赖可能读取构建环境。
“最后删除文件”只解决文件系统最终状态,不解决构建过程中的暴露。
6.3 把 Secret 放进 ENV 的失败路径
ENV DATABASE_PASSWORD=secret-value
这会使密码进入镜像配置,并作为默认环境传给容器。任何可以检查镜像、读取容器配置或在容器中读取进程环境的人,都可能得到它。
在 Linux 容器中,进程环境通常可通过:
tr '\0' '\n' < /proc/1/environ
读取,前提是权限和进程关系允许。环境变量还可能被应用错误日志、诊断接口、崩溃转储或监控系统收集。
6.4 把 Secret 放进 LABEL 的失败路径
LABEL com.example.password="secret-value"
标签往往更容易被自动化扫描、镜像仓库索引或运维工具读取。它是公开描述信息,暴露边界通常比应用文件更宽。
6.5 BuildKit Secret Mount 的正确数据流
构建时需要使用 Secret,应使用 BuildKit 的临时挂载:
Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN --mount=type=secret,id=repo_token \
token="$(cat /run/secrets/repo_token)" && \
test -n "$token" && \
echo "authenticated operation would run here"
构建:
printf '%s' 'temporary-token' > repo_token
DOCKER_BUILDKIT=1 docker build \
--secret id=repo_token,src=repo_token \
-t secret-demo .
rm -f repo_token
其数据流是:
宿主机 Secret 文件
│
▼
BuildKit 在某个 RUN 期间挂载
│
▼
/run/secrets/repo_token
│
▼
RUN 结束后卸载
│
▼
不自动进入最终文件系统层
这个机制降低了 Secret 进入镜像层和 Dockerfile 参数的风险,但并不保证应用绝不会泄露它。以下情况仍然会泄露:
RUN --mount=type=secret,id=repo_token \
cat /run/secrets/repo_token > /tmp/token.txt
因为 /tmp/token.txt 会成为该层文件系统的一部分。
还应避免:
RUN --mount=type=secret,id=repo_token \
echo "$(cat /run/secrets/repo_token)"
因为 Secret 会进入构建日志。
Secret mount 解决的是“构建阶段如何临时提供凭据”,不是运行时 Secret 管理。运行时需要由编排系统、平台 Secret、挂载文件或外部密钥服务提供,并配合轮换和权限控制。
七、一个完整示例:参数、环境、标签和多阶段边界
下面的 Dockerfile 展示四种状态如何分工:
# syntax=docker/dockerfile:1
ARG ALPINE_VERSION=3.20
FROM alpine:${ALPINE_VERSION} AS build
ARG APP_VERSION=dev
ARG TARGETARCH
ENV BUILD_APP_VERSION=${APP_VERSION}
RUN mkdir -p /out && \
printf '%s\n' "built-version=${BUILD_APP_VERSION}" > /out/version.txt && \
printf '%s\n' "target-arch=${TARGETARCH}" >> /out/version.txt
FROM alpine:${ALPINE_VERSION} AS runtime
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION} \
APP_HOME=/app
LABEL org.opencontainers.image.title="arg-env-label-demo" \
org.opencontainers.image.version="${APP_VERSION}"
WORKDIR ${APP_HOME}
COPY --from=build /out/version.txt .
CMD ["sh", "-c", "cat /app/version.txt && echo runtime-version=${APP_VERSION}"]
构建:
docker build \
--build-arg APP_VERSION=1.2.3 \
--platform linux/amd64 \
-t arg-env-label-demo:1.2.3 .
逐步分析:
- 顶层
ALPINE_VERSION可用于两个FROM; build阶段单独声明APP_VERSION和TARGETARCH;build阶段的BUILD_APP_VERSION是环境变量,只在该阶段及其后续指令中使用;runtime阶段不是FROM build,因此不会继承build阶段的ENV;COPY --from=build只复制/out/version.txt;runtime阶段重新声明APP_VERSION,再转换为运行时ENV;LABEL将版本写入镜像配置;- 容器启动后只能看到
runtime阶段设置的APP_VERSION; TARGETARCH被写入文件,因为构建逻辑显式使用了它;它不会自动成为运行时环境变量。
验证:
docker run --rm arg-env-label-demo:1.2.3
可能输出:
built-version=1.2.3
target-arch=amd64
runtime-version=1.2.3
查看环境和标签:
docker image inspect arg-env-label-demo:1.2.3 \
--format 'env={{json .Config.Env}} labels={{json .Config.Labels}}'
可能看到:
env=["APP_VERSION=1.2.3","APP_HOME=/app"] labels={"org.opencontainers.image.title":"arg-env-label-demo","org.opencontainers.image.version":"1.2.3"}
这个结果同时说明:
- 文件中的
built-version来自构建阶段; APP_VERSION是运行时默认环境;org.opencontainers.image.version是镜像标签;- 三者值相同只是因为它们都使用了同一个构建参数,并不表示它们属于同一种机制。
八、常见误解与失败表现
8.1 误解:ARG 会自动传给 docker run
错误示例:
FROM alpine:3.20
ARG PORT=8080
CMD ["sh", "-c", "echo PORT=$PORT"]
构建成功并不代表运行时有 PORT。修复方式是明确转换:
FROM alpine:3.20
ARG PORT=8080
ENV PORT=${PORT}
CMD ["sh", "-c", "echo PORT=$PORT"]
但如果 PORT 只是运行时配置,更合适的做法通常是使用运行命令或 Compose 注入,而不是在镜像中固化一个环境值。
8.2 误解:COPY --from 会继承前一阶段所有设置
不会。它只复制指定文件或目录:
COPY --from=build /app /app
不会自动复制:
ENV;ARG;LABEL;CMD;ENTRYPOINT;- 工作目录。
如果运行阶段需要某个变量,必须在运行阶段显式设置。
8.3 误解:标签是容器标签
Docker 中至少要区分:
- 镜像标签:
docker image inspect中的Config.Labels; - 容器标签:创建容器时附加到容器对象的标签;
- 镜像引用标签:
example:1.2.3中冒号后的 tag。
例如:
docker run -d \
--name demo \
--label com.example.owner=team-a \
example:1.2.3
这里的 --label 是容器标签,不会修改已经存在的镜像标签。
检查容器标签:
docker inspect demo \
--format '{{json .Config.Labels}}'
检查镜像标签:
docker image inspect example:1.2.3 \
--format '{{json .Config.Labels}}'
两者对象不同,生命周期也不同。
8.4 误解:删除了 Secret 文件就没有泄漏
以下过程仍然有风险:
RUN echo "${TOKEN}" > /tmp/token \
&& use-token /tmp/token \
&& rm -f /tmp/token
删除动作只影响当前层之后的文件系统视图。构建历史、日志、缓存、最终生成文件以及工具自身的诊断输出仍可能保留凭据。
8.5 误解:修改 ENV 只影响运行时
ENV 设置后会影响后续构建指令:
ENV NODE_ENV=production
RUN npm install
这里 NODE_ENV 可能改变依赖安装结果,因此它不仅是运行时配置,也是构建输入。若构建和运行需要不同值,应分开表达:
ARG BUILD_NODE_ENV=production
RUN NODE_ENV="${BUILD_NODE_ENV}" npm install
ENV NODE_ENV=production
这样可以避免构建期变量和运行期变量互相覆盖。
九、诊断方法:从“值在哪里”反推机制
遇到变量不生效或敏感值疑似泄露时,应先定位它进入了哪一类状态。
9.1 构建阶段看不到变量
检查顺序:
ARG NAME=value
FROM alpine:3.20
RUN echo "$NAME"
这里的问题是 ARG 在 FROM 前声明,未在阶段内重新声明。应改为:
ARG NAME=value
FROM alpine:3.20
ARG NAME
RUN echo "$NAME"
如果变量用于 FROM,看它是否位于第一个 FROM 之前;如果变量来自父阶段,看当前阶段是否确实使用了 FROM parent AS child。
9.2 运行容器看不到变量
检查:
docker image inspect image:tag \
--format '{{json .Config.Env}}'
如果不存在,说明 Dockerfile 没有把它转换成 ENV。如果镜像中存在但容器值不同,检查运行时覆盖:
docker inspect container \
--format '{{json .Config.Env}}'
还要检查 Compose、docker run -e、平台部署配置是否覆盖了镜像默认值。
9.3 标签看不到或值不对
检查镜像而不是容器:
docker image inspect image:tag \
--format '{{json .Config.Labels}}'
若标签依赖 ARG,确认构建命令确实传入了:
docker build --build-arg VERSION=1.2.3 .
同时确认没有后续 LABEL 使用同一个键覆盖前值。
9.4 怀疑 Secret 已进入镜像
可检查:
docker history --no-trunc image:tag
docker image inspect image:tag
对于远程 registry,还应检查构建器生成的 provenance、缓存导出物和日志系统。不能只扫描最终文件系统:
docker run --rm image:tag find / -type f
因为 Secret 可能不在最终文件中,却已进入历史、配置、标签或构建证明。
已经把凭据写入镜像后,正确恢复路径通常是:
- 立即撤销或轮换凭据;
- 删除暴露的镜像和相关缓存;
- 修正 Dockerfile,改用 Secret mount;
- 重新构建并推送;
- 检查日志、构建证明、缓存和仓库访问记录;
- 不把旧镜像仅仅改名或覆盖 tag 当作清除,因为旧 digest 可能仍可访问。
十、Compose 与 Dockerfile 的边界
Compose 经常同时管理构建参数、运行环境和 Secret,因此更容易把三者混淆。
概念上:
services:
app:
build:
context: .
args:
APP_VERSION: "1.2.3"
environment:
APP_ENV: production
这里:
build.args对应 Dockerfile 的ARG;environment对应容器运行时环境;ENV只是镜像提供的默认运行时环境;- Compose 的运行时设置可以覆盖镜像中的
ENV。
例如 Dockerfile:
ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION}
Compose:
services:
app:
build:
args:
APP_VERSION: "1.2.3"
environment:
APP_VERSION: "runtime-override"
最终容器中通常是:
APP_VERSION=runtime-override
因为构建参数先决定镜像默认值,Compose 的 environment 再在容器创建阶段覆盖它。
运行时 Secret 则不应放进 build.args 或镜像 environment 的明文配置中。应使用 Compose 规范支持的 Secret 机制或部署平台的 Secret 注入方式,并确认实际实现如何挂载、授权和轮换。
十一、可重复构建中的 Metadata 取舍
ARG、ENV 和 LABEL 都可能影响可重复构建,但影响方式不同。
1. 基础镜像引用
ARG ALPINE_VERSION=3.20
FROM alpine:${ALPINE_VERSION}
使用可变 tag 时,未来同一个 Dockerfile 可能解析到不同的基础镜像内容。更强的可重复性通常需要固定 digest:
FROM alpine:3.20@sha256:<digest>
digest 必须由实际镜像发布者或可信仓库提供,不能随意填写。
2. 构建参数
版本、目标平台和特性开关如果参与了构建逻辑,就应作为显式输入记录:
docker build \
--build-arg APP_VERSION=1.2.3 \
--platform linux/amd64 \
-t example:1.2.3 .
相同源码、相同基础镜像 digest、相同构建参数和相同工具链,才有机会得到可比较的结果。ARG 本身不会自动让构建可重复。
3. 标签中的时间戳
下面的标签会使 Metadata 每次构建都不同:
ARG BUILD_TIME
LABEL org.opencontainers.image.created="${BUILD_TIME}"
这并非错误,但会妨碍按 digest 比较构建结果。若时间信息只是审计信息,可以交给 provenance 或外部发布系统保存,而不是强行写入影响镜像身份的字段。
4. ENV 的构建影响
如果某个 ENV 参与包安装、编译或生成文件,它就是构建输入:
ENV GOFLAGS=-trimpath
RUN go build ./...
如果它只用于容器启动,也可以在最终阶段设置,以减少构建阶段和运行阶段的耦合。
十二、实际分工原则
可以用下面的判断过程选择机制:
这个值是否只影响 Dockerfile 构建?
是 -> ARG
这个值是否应该成为容器进程的默认环境?
是 -> ENV,或由运行平台注入
这个值是否用于描述镜像来源、版本、组件或所有者?
是 -> LABEL / 其他构建 Metadata
这个值泄露后是否会造成权限或数据风险?
是 -> 不使用 ARG、ENV、LABEL;使用 Secret 机制
典型映射如下:
| 需求 | 合适机制 |
|---|---|
| 选择基础镜像版本 | ARG,必要时固定 digest |
| 选择编译特性 | ARG |
| 将构建版本展示给运行中的应用 | 显式转换为 ENV 或写入版本文件 |
| 描述镜像版本和来源 | LABEL,并结合 provenance |
| 注入数据库密码 | 运行时 Secret |
| 构建时访问私有包仓库 | BuildKit Secret mount |
| 容器启动时覆盖默认配置 | docker run -e、Compose、平台配置 |
| 记录构建平台 | 自动平台参数、标签或 provenance |
核心边界可以概括为:
ARG 是构建输入;
ENV 是构建环境与运行时默认环境;
LABEL 是镜像描述信息;
Metadata 是比 LABEL 更大的镜像与构建描述集合;
Secret 不应进入上述任何公开或可持久化边界。
理解这些边界后,多阶段构建中的行为就可以按数据流准确判断:FROM stage 继承阶段状态,COPY --from 只复制文件,ARG 需要在作用域内声明,ENV 可能进入镜像和容器,LABEL 进入镜像元数据,而 Secret 应只在需要它的最短生命周期内出现。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Build Secret 与 SSH Mount:凭据注入、缓存和泄漏防护
- 下一篇:Docker 可重复依赖安装:锁文件、镜像源、缓存、校验和离线
- 延伸:Dockerfile 与 BuildKit:构建上下文、缓存挂载、Secret 和可重复构建
- 延伸:Docker 配置与 Secret:环境变量、文件注入、轮换和泄漏防护
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论