Compose 环境变量的两套机制:${VAR} 展开与 env_file 注入

「改了 .env 为什么容器里没生效」是 Docker Compose 被问得最多的问题之一。根源是一个误解:把 .env、env_file:、environment: 当成同一套机制的三个写法。它们其实是两个独立平面,混淆都源于把它们当成一回事。

平面一:展开——compose 文件自己的解析

docker compose 解析 yml 时,会把里面的 ${VAR} 占位符替换成具体值。替换结果只影响解析出的配置长什么样,不会进容器。值的来源按优先级:

  1. shell 环境变量(VAR=x docker compose up)
  2. --env-file <文件> 显式指定
  3. 项目目录的 .env(自动加载的默认值)

--env-file 和 .env 是同一个角色的两种指定方式:指定了 --env-file 就不再看默认的 .env。典型用途是端口、镜像名、路径这类「compose 配置本身需要」的值:

image: registry.example.com/my-app:${TAG:-latest}

平面二:注入——容器进程的环境变量

容器内代码 os.environ.get() 读到的值只来自这个平面,优先级从高到低:

  1. environment:(yml 里显式写)
  2. env_file:(yml 引用文件,逐行注入)
  3. Dockerfile 的 ENV / 镜像自带

两平面之间唯一的桥

environment:
  DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}

environment: 的值里写 ${VAR},就是用平面一的展开结果当平面二的输入——这是两个平面之间唯一的桥,而且必须手写。反过来两条规则要记死:

一句话记忆:.env 是给 compose 自己看的,environment: / env_file: 是给容器看的。

三条排查规则

把两个平面分清之后,Compose 的环境变量就没有玄学问题了:解析阶段替换了什么,docker compose config 一验便知;容器里实际拿到什么,docker compose exec <svc> env 一看便知。