Docker Compose 在读取 compose.yml 时会先做一轮变量插值,再把结果交给 Docker。这个「先于 Docker 的一轮预处理」是所有 $ 相关诡异现象的根源——尤其是当你的配置里本来就需要一个**字面意义的美元符号**时。
一、插值支持哪些写法
官方文档列出的语法一共这几档:
$VARIABLE或${VARIABLE}:替换为环境变量值;${VARIABLE:-default}:未设置或为空时用 default;${VARIABLE-default}:仅在未设置时用 default(空字符串不算);${VARIABLE:?err}/${VARIABLE?err}:缺失时直接报错中止,适合强制必填项;$$:转义,输出一个字面的$。
变量来源按优先级大致是:宿主机 shell 环境 → .env 文件 → 文件里显式写的 environment。
二、核心那条:$$ 表示字面的 $
用
$$可以让 Compose 跳过插值,把它当成一个普通的$字符交给容器。
反过来说,如果你在配置里直接写了一个 $,Compose 就一定会尝试把它当成变量开头——后面跟的不是合法变量名时,轻则被替换成空串,重则直接报插值错误、整个栈起不来。
三、三种典型「中招」场景
1)密码/令牌里带 $
最经典。任何以 $ 开头的令牌或哈希,如果不转义,会被 Compose 吃成空值。表现是「明明写在配置里,容器里却是空的」。
2)$ 出现在哈希串中间
# 错误:$argon2id$v=19$m=... 会被逐段当成变量
# 正确:每个 $ 都写成 $$
ADMIN_TOKEN=$$argon2id$$v=19$$m=65536,p=4,t=3$$...
Argon2 的 PHC 字符串天然就是一堆 $ 分隔的字段,所以只要用它做凭据,就必然要处理转义。
3)命令里需要 shell 变量
写进 command 或 entrypoint 的 $HOME、$${HOSTNAME} 之类,如果本意是让容器里的 shell 去展开,就必须用 $$ 拦下 Compose 这一层——否则在宿主机侧就被替换掉了,容器拿到的是宿主机环境的值。
command: /bin/sh -c 'echo 容器内视角: $$HOSTNAME'
四、出处
- Docker Docs — Interpolation(Compose 环境变量)
https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
五、在自家服务器上的印证
这不是理论问题——本站的密码管理器在换管理令牌时就实打实踩过:
新令牌改用 Argon2id PHC 格式,形如 $argon2id$v=19$m=65536,p=4,t=3$盐$哈希。第一版直接原样写进 compose.yml,结果 Compose 把每个 $ 后面的字段当成变量名去解析,容器里的令牌根本不是想要的值。
改法就是官方文档那一行:把每个 $ 换成 $$。改完重启,「验证新令牌 → 通过 / 验证旧令牌 → 失败」,一次到位。
一条经验:只要 compose.yml 里出现 $,先问自己一句「这个 $ 是给 Compose 看的,还是给容器看的?」——答案决定了它要不要写成 $$。
评论