DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Docker Compose 环境变量含 ${Time.now} 时报 Invalid interpolation format

Compose 把应用需要的 ${Time.now} 当成变量表达式时,使用双美元符号保留原文,并区分 YAML、环境文件和 shell 的解析阶段,验证容器收到的值。

应用需要收到字面量 ${Time.now},但把它写进 Compose 的 environment 后,容器还没正常启动,Compose 就报错:

Invalid interpolation format for "environment" option in service "time_service": "${Time.now}"

这里的 ${Time.now} 是要交给应用处理的模板文本。Compose 却把它当成自己的变量表达式,解析失败。如果需要保留这段原文,可以在 Compose 文件中把开头的一个美元符号写成两个:$${Time.now}。

本文针对直接写在 Compose YAML 值中的字面量。命令采用当前的 docker compose;旧版 docker-compose 的错误措辞可能不同。环境文件、容器里的 shell 和应用模板各自还有解析规则,不能把所有文件中的 $ 一律替换成 $$。

先确定这段文本应该由谁处理

容易触发错误的配置如下。time_service 是示例服务名,实际项目应保留自己的镜像或构建配置:

services:
  time_service:
    build: .
    environment:
      TIME: "${Time.now}"

Compose 支持 $VAR 和 ${VAR},变量名称遵循字母、数字和下划线的规则,不能把 Time.now 当作普通变量名。若本意是从宿主机环境读取变量,应改用合法的变量名,并设置对应值;若本意是把 ${Time.now} 原样传给应用,则应阻止 Compose 展开它。

先分清这两种目标。给宿主机增加一个 TIME 变量,不会让 ${Time.now} 自动变成对 TIME 的引用;把表达式删成 Time.now 虽然可能避开报错,却已经改变应用要收到的内容。

当前 Compose 还支持默认值、必填值等形式,例如 ${VAR:-default}、${VAR:?error}。不要因为某个旧版报错,就认定当前 Compose 不支持所有带符号的表达式。相反,${VAR/foo/bar} 这类 shell 字符串替换并不属于 Compose 支持的语法。

只转义需要保留的美元符号

先保存原 Compose 配置及覆盖文件,然后修改这一项:

services:
  time_service:
    build: .
    environment:
      TIME: "$${Time.now}"

$$ 在这里表示一个字面美元符号,并阻止 Compose 把后面的文本当作自己的变量替换。目标是让容器环境变量 TIME 的值为 ${Time.now},而不是 $${Time.now},也不是当前时间。

这一步不会让 Compose 执行 Ruby、JavaScript 或任何应用代码。应用收到模板字符串后会不会继续求值,取决于应用本身的功能。如果应用只把 TIME 当普通文本,它就应当继续保持原文。

不要顺手给整个文件的变量都加倍。例如镜像标签中的 ${IMAGE_TAG} 若原本应该由 Compose 替换,改成 $${IMAGE_TAG} 就会阻止正常替换。应逐项区分“部署时取值”和“传给应用的原文”。

用同一组启动参数检查配置

在实际项目目录中先做不启动容器的校验:

docker compose config --quiet

如果部署时使用多份 -f 文件、指定项目名或 --env-file,校验时也要沿用。Compose 按文件执行插值,再合并配置;即使后一份文件覆盖了 TIME,前一份文件里无效的表达式仍可能先触发解析错误。需要修改报错指向的实际文件,不能只看最终想要的覆盖值。

若仍报错,可对照原文件检查服务名和键路径,再查看未插值的配置:

docker compose config --no-interpolate

该选项用于观察原始表达式,不是修复开关。它能绕过这一阶段的展开,所以它运行成功不代表正常启动也会成功;修改后仍需让不带 --no-interpolate 的校验通过。配置输出可能带出其他环境变量中的凭据,应只在受控终端查看,不要直接贴到公开日志。

环境文件与 shell 不能照搬同一层转义

如果字符串实际存放在 .env 或服务的普通 env_file 中,要按环境文件的规则处理。普通环境文件的未加引号和双引号值会参与插值,单引号值可按字面量保存。例如,下面是环境文件内容,不是 YAML:

TIME='${Time.now}'

这段内容是否进入容器,还取决于文件通过哪种方式使用,以及 environment 是否覆盖了同名项。不要把 Compose YAML 中的 $${Time.now} 原封不动搬进单引号环境文件,期望它在每一层都再缩成一个美元符号。

若错误出现在 command 而不是 environment,还要考虑后续是否显式调用 shell。Compose 的 command 不会因为写成字符串就自动获得 shell 的变量展开能力;需要 shell 特性时,应明确使用镜像中存在的 shell。反过来,把含 ${Time.now} 的文本直接拼进 sh -c 脚本,可能在 shell 这一层再次被当作参数表达式。Compose 不再报错,只能说明第一层处理通过,不代表后面的解析器接受同一语法。

对于本文的场景,优先通过 environment 把原文交给应用,不为绕过 Compose 报错额外增加 shell 或动态求值。

验证应用实际收到的值,再更新已有服务

配置校验通过后,在测试项目中启动对应服务。示例依赖服务应已经就绪;--no-deps 不会替你启动它们:

docker compose up -d --no-deps time_service

修改环境变量后,up 会按配置变化重建相应容器。已有业务可能中断,部署前应安排维护时间,保存可恢复的配置、镜像及必要数据备份。无需删除数据卷,也不需要通过清空整个项目来修复插值错误。

如果镜像提供 printenv,且服务仍在运行,可用无敏感信息的示例值核对:

docker compose exec -T time_service printenv TIME

这里预期得到一行 ${Time.now}。这条命令直接执行 printenv,不会让 shell 对输出内容再求值。没有 printenv 的精简镜像,应使用应用自己的受控诊断方式;一次性任务已经退出时,也不能据此认为环境变量传递失败。

真实字段若存放密码,不要照搬打印命令。可以让应用在受控检查中只返回匹配与否,既验证字符没有丢失,也避免把值写进日志。最终还要确认应用的模板处理或业务行为符合预期,不能只以 Compose 退出码为成功标准。

需要撤回时,只恢复本次修改的配置项,再用相同项目参数更新对应服务。恢复到原先无效的 ${Time.now} 写法会重新带回解析错误,因此可运行的上一版配置比“改动前文件”更适合作为业务回退版本。配置回退也不会撤销应用已经执行的任务或写入的数据。