资讯动态

Docker --build-arg 动态构建多环境镜像:ARG与ENV实战指南

发布时间:2026/10/21 12:17:49 来源:尧图企业网站定制
老话讲得好Docker 镜像构建这事儿最磨人的不是镜像本身而是一套代码要出多套环境镜像的时候。我见过身边不少同事测试环境跑得欢一到生产环境就因为数据库地址、日志级别、接口域名不一致要么手动去改 Dockerfile要么维护一份Dockerfile.dev和Dockerfile.prod改着改着就漏改、改错最后构建出个看起来对但一跑就崩的镜像。后来我换了思路用docker build --build-arg把变量传进 Dockerfile让一份模板文件通过构建参数动态生成不同环境的镜像。这篇文章就把这套玩法完整拆开从 ARG 和 ENV 的区别、Dockerfile 里的声明姿势、命令行传参、多阶段构建的坑到 CI 里怎么联动一步步捋清楚适合正在做多环境镜像改造、刚接触 Docker 构建参数的朋友。1. 动态构建到底解决了什么实际问题1.1 为什么不能只靠改文件来出镜先说一个最常见的反例。很多人一开始是这么写 Dockerfile 的ENV DB_HOST127.0.0.1 ENV APP_ENVdev写得很爽本地构建也很快。但问题马上就来了如果想打一个生产环境的包就得把这两行改了再重新 build。一旦环境多了Dockerfile 会被改得乱七八糟。更难受的是 Docker 构建是有缓存的你只是改了文件内容构建时如果缓存判断失误很可能会把旧层带出来实际跑起来还是旧配置。也有人尝试用sed命令去替换 Dockerfile 里的地址比如sed -i s/127.0.0.1/10.0.0.5/ Dockerfile。这方法偶发一两次还行一旦变量多了、值里带着$或者/这类特殊字符转义就把人搞到崩溃。而且 sed 改文件是破坏性的改完忘了还原下次构建就一路歪下去。--build-arg的做法恰好绕开这些麻烦Dockerfile 只负责用占位符声明变量构建时通过命令行把具体值灌进去。相当于 Dockerfile 从死配置变成活模板一份文件可以喂出 dev、test、prod 好几套镜像。1.2 典型场景多环境、多版本、私有依赖源我实际用下来--build-arg主要解决三类问题。多环境镜像是最常见的。一套后端服务开发环境连本地数据库测试环境连测试库预发和生产连各自的集群这时候把数据库地址、Redis 地址、短信服务地址全部做成构建参数构建命令里传不同的值就行。多版本发布也很好用。打的镜像 tag 经常要带上版本号比如app-1.3.0、app-1.4.0-rc1。版本号不但要出现在 tag 里还要写进镜像内部作为程序启动时的标识。把这个版本号通过构建参数传进去Dockerfile 里用ARG APP_VERSIONlatest接收再注入到环境变量里程序运行时就能直接读到版本信息排查问题的时候一眼知道当前跑的是哪个包。私有依赖源也有点意思。公司内部通常有私有的 npm 源、pip 源或 Maven 仓库。如果 Dockerfile 写死公网地址某些场景下依赖拉取会非常慢尤其是安全要求高的网络环境。把源地址做成ARG NPM_REGISTRYhttps://registry.npm.example.com构建时传入公司内部地址既不用改 Dockerfile也不会让同事在自己的电脑上构建时踩到源不可达的坑。2. ARG 和 ENV 的把戏构建期变量与运行期变量2.1 生命周期决定一切很多刚接触 Dockerfile 的人会把ARG和ENV混在一起其实这两个变量的生命周期完全不一样。ARG只在镜像构建过程中有效。Dockerfile 里的RUN、COPY等指令执行时可以读取它但镜像构建完容器跑起来进程里是没有这个 ARG 的。哪怕你在 Dockerfile 里写了ARG DB_HOSTlocalhostbuild 完进入容器执行echo $DB_HOST大概率是空值。ENV则不同它会被写进镜像的配置里。容器启动后ENV定义的环境变量自动出现在进程环境中程序代码可以直接os.Getenv(DB_HOST)读到。这么说吧ARG是构建时的一次性配料ENV是运行时的常驻配置。想用构建参数影响最终运行环境最稳妥的方式是在 Dockerfile 里先用ARG接收外部传值再赋值给同名ENV这样既能享受构建期动态配置又能在容器启动后保留结果。ARG DB_HOSTlocalhost ENV DB_HOST$DB_HOST这一步操作看起来简单却是整套动态构建的核心衔接环节后面所有案例都基于这个组合。2.2 FROM 前后的作用域陷阱自己写 Dockerfile 的时候最容易踩的坑是ARG的作用域。很多人以为在文件开头声明一个ARG整个 Dockerfile 都能用事实并不是这样。看这个例子ARG APP_VERSIONlatest FROM node:20-alpine RUN echo version: $APP_VERSION如果你执行docker build --build-arg APP_VERSION1.4.0 .最后镜像里的echo结果大概率还是latest。因为FROM node:20-alpine会开启一个新的构建阶段之前声明的ARG APP_VERSION在FROM之后根本不可见。想让它在后续指令里生效必须在FROM之后重新声明一次ARG APP_VERSIONlatest FROM node:20-alpine ARG APP_VERSIONlatest RUN echo version: $APP_VERSION同样的规则在多阶段构建里更明显每个FROM后面都算一个新的作用域就算第一个阶段已经声明了ARG第二个阶段依然要再声明一次。这个坑我踩过好几次后面专门做了个检查清单。2.3 用 ARG 给 ENV 赋值实现动态默认值如果想让镜像既支持外部传参又能在没人传参时兜底ARG的默认值就派上用场了。ARG APP_ENVdev ARG LOG_LEVELinfo ENV APP_ENV$APP_ENV ENV LOG_LEVEL$LOG_LEVEL构建时不传--build-argENV取到的就是默认值dev和info构建时传入新值比如--build-arg APP_ENVprod --build-arg LOG_LEVELwarn镜像里的环境变量就跟着变成prod和warn。这种写法的好处是降级友好。本地开发的人不需要额外传参直接 build 就能跑CI 里或者发布系统里再显式传生产配置。你不需要维护多份 Dockerfile一份模板就能覆盖所有使用场景。3. Dockerfile 里写 ARG 的四个高频姿势3.1 普通参数声明与默认值语法ARG的基础语法很简单ARG 变量名[默认值]比如ARG CODE_VERSIONlatest ARG TZAsia/Shanghai声明之后在同一个构建阶段里后面所有的RUN、ENV、COPY、WORKDIR等指令都能引用。引用的方式就是$变量名或者${变量名}。我习惯用花括号形式比如RUN echo version: ${CODE_VERSION}因为当变量名后面紧跟着字母或数字时花括号能避免解析歧义。有一点要注意不同 Dockerfile 里ARG的名字可以重复声明以最后一次声明为准。所以你可以先放一个带默认值的声明后面再放一个不带默认值的声明覆盖掉默认值。这种覆盖声明在某些基础镜像继承场景里很实用。3.2 多阶段构建中的 ARG 传递每个阶段都得重新声明多阶段构建的场景里ARG传递是最容易出问题的环节。比如你想先在一个 Node 环境里编译前端资源再把编译结果复制到 Nginx 镜像里FROM node:20-alpine AS builder ARG APP_VERSIONlatest RUN npm run build:${APP_VERSION} FROM nginx:alpine ARG APP_VERSIONlatest COPY --frombuilder /app/dist /usr/share/nginx/html两个阶段各写了一次ARG APP_VERSION看起来重复但必须这样写。如果你只在第一个阶段写了ARG第二个阶段想用COPY --frombuilder拿编译产物是没问题的但第二个阶段里的RUN、ENV就用不到APP_VERSION了。还有个进阶细节FROM指令本身也可以使用ARG。比如ARG BASE_IMAGEnode:20-alpine FROM ${BASE_IMAGE}这种用法要求ARG必须写在FROM之前而且只能用在FROM的镜像名部分不能用来做复杂拼接。之前见过有人试图写FROM ${REGISTRY}/library/${CODE}:${TAG}这种两段式拼接在部分版本上没问题但在一些构建环境下会报错我建议尽量保持简单。3.3 用 ARG 拼接下载地址、标签和构建信息ARG最常见的用法之一是拼接可变的内容。比如程序安装包地址里带着版本号ARG APP_VERSION1.0.0 ARG DOWNLOAD_BASEhttps://downloads.example.cn RUN curl -fsSL ${DOWNLOAD_BASE}/app-${APP_VERSION}.tar.gz -o app.tar.gz \ tar -xzf app.tar.gz看起来简单但有两个细节值得留意一是整个地址最好用双引号包起来防止变量值里带空格导致 shell 拆词二是如果变量出现在RUN指令里Docker 先会对 Dockerfile 里的$变量做一层替换再把剩下的命令交给/bin/sh执行所以不同层的替换逻辑要提前想清楚。构建信息也经常用ARG注入。比如把 git commit、构建时间、CI 任务号写进镜像的某个文件里ARG GIT_COMMITunknown ARG BUILD_TIMEunknown RUN echo {\commit\:\$GIT_COMMIT\,\buildTime\:\$BUILD_TIME\} /app/version.json这样以后排查线上版本时直接看容器里的version.json就知道是哪个 commit 构建出来的不用再猜镜像 tag 对应哪次提交。3.4 ARG 在 Docker 构建缓存里的参与方式很多人发现一个问题明明我改了--build-arg的值重新构建时 Docker 却还在用旧缓存结果镜像内容没变。这里面的规则值得说清楚。如果某个ARG只声明了但在后续指令中完全没有被引用那么给它传不同的值不会导致缓存失效。比如 Dockerfile 里只有一句ARG FOObar之后没有任何指令用它你传--build-arg FOObaz也不会触发重新构建。反过来如果ARG参与了某个指令比如RUN echo $FOO或者ENV FOO$FOO那么当FOO的值发生变化时对应的那一层缓存就会失效。Docker 会比较指令内容和变量展开后的值不一致就从那一层开始重跑。所以想让构建参数变了就自动重新构建必须让ARG真正参与进RUN、ENV、COPY这些实际层里。如果你只是想传个标签但又不希望缓存失效也可以故意只在LABEL里引用它这样不参与文件系统变化但具体需求要具体分析。4. docker build --build-arg 实操命令与 CI 联动4.1 命令行传参的基本语法与多参数传递命令行传参的格式非常直接docker build --build-arg APP_ENVprod --build-arg DB_HOST10.0.0.5 -t app:prod .一个--build-arg传一个变量多个变量就重复写多个--build-arg。参数后面跟的是.表示构建上下文在当前目录。要注意的是虽然--build-arg写在docker build后面但实际上它只影响 Dockerfile 里声明的变量不会把变量传进宿主机 shell也不会传进构建上下文的文件里。还可以利用宿主机 shell 的变量来传值APP_ENVprod DB_HOST10.0.0.5 docker build --build-arg APP_ENV$APP_ENV --build-arg DB_HOST$DB_HOST -t app:prod .这里留了双引号避免 shell 对字符串里的空格做拆分。如果你直接用--build-arg APP_ENV$APP_ENV一旦变量值里带空格或特殊符号shell 和 Docker 的解析可能会跑偏。4.2 特殊字符、密码与$符号的转义传参时最头疼的是值里有特殊字符。比如密码是Pssw0rd$123直接写docker build --build-arg DB_PASSWORDPssw0rd$123 -t app:prod .你会发现在宿主机 shell 里$123会被 shell 当作变量展开。结果 Dockerfile 里收到的值变成了Pssw0rd后面跟一个空串或者别的值。解决办法是用单引号包住整个值docker build --build-arg DB_PASSWORDPssw0rd$123 -t app:prod .单引号让 shell 不做任何展开原样传给 Docker。这还没完。如果这个值是要在 Dockerfile 的RUN指令里被 shell 继续使用的比如RUN echo password is $DB_PASSWORDDockerfile 解析时$DB_PASSWORD会被替换成Pssw0rd$123替换后的命令变成echo password is Pssw0rd$123容器 shell 又会把$123当作变量。这里可能变成空值。想避免二次展开要么在 Dockerfile 的 shell 命令里对$做转义写$$要么尽量别把带$的值塞进 RUN 指令里使用。我现在的习惯是敏感字符多的值尽量通过ENV赋值而不是在RUN里直接 echo从根源上减少 shell 展开问题。4.3 在 CI 流水线里动态注入构建信息--build-arg在 CI 里的作用会被放大。因为你不需要在仓库里维护任何环境相关的文件只需要在流水线变量里配置好不同环境的值。拿 GitLab CI 举个例子可以这样写build: stage: build variables: APP_ENV: dev script: - docker build --build-arg APP_ENV$APP_ENV --build-arg APP_VERSION$CI_COMMIT_SHORT_SHA -t app:$CI_ENVIRONMENT_NAME .注意这里我们用了 CI 自带的预定义变量CI_COMMIT_SHORT_SHA也就是当前提交的短哈希。这样每次 commit 打出来的镜像都会自动带上版本号不用手动改 Dockerfile。如果你用其他 CI思路也一样把流水线里已经存在的环境变量用--build-arg传给 Dockerfile。这里有个小提醒如果 CI 日志对用户可见不要在script里直接把带密码的构建参数打印出来也不要在构建命令里明文写密码。CI 的日志往往会被保留很久密码一旦进了日志就等于泄密。4.4 BuildKit 模式下的进阶玩法新版 Docker 默认开启了 BuildKitdocker build --build-arg的行为在大部分场景下和旧版一致但 BuildKit 额外支持一些更安全的能力。比如经常被需要的 secret 注入。BuildKit 支持在RUN指令里挂载 secret 文件内容不会写入镜像层里RUN --mounttypesecret,iddb_password \ echo The password is $(cat /run/secrets/db_password) /app/config构建命令变成DOCKER_BUILDKIT1 docker build --secret iddb_password,src./db_password.txt -t app:prod .这种方案和--build-arg的最大区别是secret 不进入镜像的 history也不容易被docker inspect查到适合传递密码、Token 这类敏感信息。一般的配置信息仍然推荐用ARGENV的方式既简单又直观。5. 完整实操构建一个多环境后端服务镜像5.1 场景设定与 Dockerfile 编写假设我有一个比较典型的 Node.js 后端服务需要在 dev 和 prod 两个环境跑。不同环境的差异集中在三个地方APP_ENV环境标识、DB_HOST数据库地址、DB_PORT数据库端口。同时我还想往镜像里写一个APP_VERSION方便程序启动时打印版本。Dockerfile 可以这样写FROM node:20-alpine AS builder ARG APP_VERSIONlatest WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine ARG APP_ENVdev ARG DB_HOSTlocalhost ARG DB_PORT5432 ARG APP_VERSIONlatest ENV APP_ENV$APP_ENV \ DB_HOST$DB_HOST \ DB_PORT$DB_PORT \ APP_VERSION$APP_VERSION WORKDIR /app COPY --frombuilder /app/dist ./dist COPY package*.json ./ RUN npm ci --omitdev EXPOSE 3000 CMD [node, dist/index.js]这里有几个我特意设置的细节两个构建阶段都重新声明了ARG保证ENV阶段能读到值ENV用一条指令同时声明多个变量减少镜像层数ARG APP_VERSION在 builder 阶段也有这样RUN npm run build如果需要读版本号也能用。5.2 分别构建 dev 和 prod 镜像构建 dev 镜像docker build \ --build-arg APP_ENVdev \ --build-arg DB_HOST127.0.0.1 \ --build-arg DB_PORT5432 \ --build-arg APP_VERSIONv1.2.0-dev \ -t demo-app:dev .构建 prod 镜像docker build \ --build-arg APP_ENVprod \ --build-arg DB_HOST10.0.0.5 \ --build-arg DB_PORT5432 \ --build-arg APP_VERSIONv1.2.0 \ -t demo-app:prod .构建完之后可以直接跑容器验证环境变量是否注入成功docker run --rm demo-app:dev env | grep -E APP_ENV|DB_HOST|DB_PORT|APP_VERSION输出里应该能看到APP_ENVdev DB_HOST127.0.0.1 DB_PORT5432 APP_VERSIONv1.2.0-dev这个例子里DB_HOST被写进了镜像环境变量要覆盖它只要在启动时再传-e DB_HOSTxxx即可但要注意运行期覆盖的是容器环境不影响镜像本身。5.3 用 docker history 和 inspect 验证注入结果有时候光看容器跑起来还不够我们还想确认镜像里到底写了什么。两个命令很好用。docker history可以看每一条构建指令和大小docker history --no-trunc demo-app:prod如果某个RUN指令里直接使用了ARG展开后的内容可能会出现在docker history里。所以不适合把密码放进去。但查看普通的变量没问题。docker inspect可以看镜像配置docker inspect demo-app:prod --format {{json .Config.Env}}输出是一个 JSON 数组里面能看到ENV指令写入的变量和值。这个方法在排查变量到底有没有进入镜像时特别管用。6. 高频问题排查与避坑速查表6.1 典型问题速查表我整理了实际使用中反复踩过的问题做成一张表方便你对照排查。遇到的问题可能原因解决办法--build-arg传了但容器里没值Dockerfile 里没有声明对应的ARG或者声明位置和ENV赋值不在同一阶段在对应构建阶段先声明ARG再用ENV赋值多阶段构建第二阶段取不到值第二阶段没有重新声明ARG在第二个FROM后重新声明需要的所有ARG值包含$或空格导致结果截断宿主机 shell 展开了变量或 Dockerfile 的 shell 二次展开命令行用单引号包住值Dockerfile 内对$使用双写$$转义改了构建参数但镜像没更新ARG没有参与实际层指令或者缓存命中了旧层让ARG参与RUN/ENV等指令必要时加--no-cacheFROM指令里的ARG不生效ARG写在了FROM后面或者表达式过于复杂把ARG写在FROM之前且只用简单变量名Docker 版本不同行为不一致旧版 Docker 与 BuildKit 作用域细节有差异统一 Docker 版本或在 CI 里固定 BuildKit 版本6.2 安全红线不要用 ARG 传递密码和令牌一定要记住ARG不是 secret 方案。构建参数的值在 Dockerfile 的RUN指令里使用时会被展开并留在构建日志里如果写入文件或者通过echo输出镜像层里也能被翻出来。这就意味着任何不希望被别人看到的值都不要通过--build-arg传。之前我有个项目图省事把数据库密码塞在--build-arg里然后在构建阶段写进配置文件。过了很久有人告诉我镜像用docker history --no-trunc就能看到明文密码。那次之后我给自己定了几条规矩配置类信息环境名、域名、端口、版本号用ARGENV。密码、Token、私钥这类信息一律不走镜像构建参数。运行时需要的敏感信息在容器启动时通过环境变量挂载或密钥管理服务注入。构建过程中临时需要的 secret用 BuildKit 的RUN --mounttypesecret挂载。6.3 我常用的 Dockerfile 模板照着写不容易翻车基于这些经验我每次写支持动态构建的 Dockerfile基本都遵循下面这个模板结构# 第一部分全局 ARG仅供 FROM 使用 ARG BASE_IMAGEnode:20-alpine FROM ${BASE_IMAGE} AS builder # 第二部分每个阶段独立声明 ARG ARG APP_ENVdev ARG APP_VERSIONlatest RUN echo build env: $APP_ENV, version: $APP_VERSION # 第三部分最终运行阶段 FROM ${BASE_IMAGE} ARG APP_ENVdev ARG DB_HOSTlocalhost ARG APP_VERSIONlatest ENV APP_ENV$APP_ENV \ DB_HOST$DB_HOST \ APP_VERSION$APP_VERSION # 后续指令...这样做的原因很简单第一部分只控制基础镜像第二部分让编译阶段能感知参数第三部分把参数固定成运行环境变量。三段各干各的不会互相干扰也避免了很多作用域问题。说了这么多最后分享一点我的个人习惯做过的项目多了之后我现在接到动态构建镜像的需求第一件事不是急着写--build-arg而是先列一个问题清单哪些值必须构建期固定哪些值应该运行时再覆盖哪些是无关紧要的配置哪些是绝对不能落到镜像里的敏感信息。然后才动手写 Dockerfile。构建期能少的参数尽量少。ARG虽方便但参数越多构建命令越容易传错CI 里维护的成本也跟着涨。能用一个参数解决的不要拆成三个。公共配置用ARGENV没问题敏感信息一定要走运行时注入或者 BuildKit 的 secret 机制。还有一个小技巧我到现在都用得挺顺手在 Dockerfile 里把ARG和ENV写成同一个名字、同一个默认值这样后面的人接手时看到ARG DB_HOSTlocalhost和ENV DB_HOST$DB_HOST一眼就明白这个变量最终是给容器用的。别为了省几行代码改成一个叫DB_ADDR、一个叫DB_HOST那种花里胡哨的命名只会让维护者带着敬畏之心读代码然后一边读一边骂。如果你还没在项目里用过--build-arg建议先从一个简单的版本号参数开始把 dev 和 prod 两套镜像打出来对比一下。对比完之后你会发现原来手动改 Dockerfile 的日子真的可以彻底结束了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑