Compose 环境变量的两套机制:${VAR} 展开与 env_file 注入
「改了 .env 为什么容器里没生效」是 Docker Compose 被问得最多的问题之一。根源是一个误解:把 .env、env_file:、environment: 当成同一套机制的三个写法。它们其实是两个独立平面,混淆都源于把它们当成一回事。
平面一:展开——compose 文件自己的解析
docker compose 解析 yml 时,会把里面的 ${VAR} 占位符替换成具体值。替换结果只影响解析出的配置长什么样,不会进容器。值的来源按优先级:
- shell 环境变量(
VAR=x docker compose up) --env-file <文件>显式指定- 项目目录的
.env(自动加载的默认值)
--env-file 和 .env 是同一个角色的两种指定方式:指定了 --env-file 就不再看默认的 .env。典型用途是端口、镜像名、路径这类「compose 配置本身需要」的值:
image: registry.example.com/my-app:${TAG:-latest}
平面二:注入——容器进程的环境变量
容器内代码 os.environ.get() 读到的值只来自这个平面,优先级从高到低:
environment:(yml 里显式写)env_file:(yml 引用文件,逐行注入)- Dockerfile 的
ENV/ 镜像自带
两平面之间唯一的桥
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
environment: 的值里写 ${VAR},就是用平面一的展开结果当平面二的输入——这是两个平面之间唯一的桥,而且必须手写。反过来两条规则要记死:
env_file:里的值不参与${VAR}展开.env里的值不经environment:引用就到不了容器
一句话记忆:.env 是给 compose 自己看的,environment: / env_file: 是给容器看的。
三条排查规则
- 「改了 .env 容器里没生效」:先看同一服务有没有
environment:覆盖了同名变量——environment:优先级高于env_file:,这是最常见的坑。 - 文件落位约定:给展开用的
.env放 compose 文件同目录;给env_file:注入用的文件不要放同目录、不要叫.env(避免被自动加载机制误读),用container.env这类能表明用途的名字。 - 新增变量时先想清楚:这个值是 compose 配置自己要用(展开成端口/镜像名),还是容器代码要读?据此决定走哪个平面,别两边都塞一份。
把两个平面分清之后,Compose 的环境变量就没有玄学问题了:解析阶段替换了什么,docker compose config 一验便知;容器里实际拿到什么,docker compose exec <svc> env 一看便知。