Docker Compose 环境变量与 .env 配置管理实战:从变量插值到多环境部署

为什么 Compose 的配置管理值得单独讲

用 Docker Compose 部署个人网站一段时间后,几乎每个站长都会遇到同一个尴尬:数据库密码、API 密钥、域名、端口这些东西,散落在 docker-compose.yml 里,明文躺着。等到要换服务器、要给别人看配置、或者想把项目提交到 Git 仓库时,才发现这些敏感信息根本没法见人。更糟的是,同一套编排要在测试和正式环境跑两份,改一个端口要改好几处,稍不留神就漏掉。

Compose 其实内置了一套配置管理机制——.env 文件与变量插值——专门解决这个问题。可惜很多人只用了它最表面的功能,没理解背后的规则,于是踩坑不断:变量不生效、引号被吃掉、环境变量互相覆盖、.env 被误传上仓库。这篇文章把 Compose 的变量体系一次讲透,从基础到进阶,都是实战里真会遇到的问题。

基础:.env 文件与变量插值

Compose 会自动读取项目目录下的 .env 文件,把它里面的键值对当作变量,并在解析 docker-compose.yml 时做文本替换。这个「插值」发生在 Compose 把 YAML 交给 Docker 引擎之前,是最早的一步。用法很简单,.env 文件里写:

# .env
MYSQL_ROOT_PASSWORD=SuperSecret123
MYSQL_DATABASE=wordpress
WEB_PORT=8080
DOMAIN=www.example.com

然后 YAML 里用 ${变量名} 引用:

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
  web:
    image: nginx:alpine
    ports:
      - "${WEB_PORT}:80"

就这么两处改动,密码和端口就跟编排文件解耦了。.env 加进 .gitignore,仓库里只保留一份 .env.example 做模板,发布到公开仓库时就不会泄露任何机密。这是使用 Compose 的第一条纪律,也是最容易被忽略的一条——每年都有大量数据库凭据因为 .env 被误提交而泄露。

插值语法的四种写法与默认值

Compose 支持比 ${VAR} 更丰富的语法,灵活运用能免去很多麻烦。四种常见形式:

${VAR} 是最基础的替换。如果变量未定义,Compose 会替换成空字符串,并给一条警告。空字符串往往会导致更难懂的下游错误,所以建议对关键变量显式给默认值。

${VAR:-default} 表示「变量未定义或为空时,使用默认值 default」。注意这里是「或为空」,也就是说即使 .env 里写了个空值,也会走默认。这个语义在多数场景下正是我们想要的。

${VAR-default} 只处理「未定义」的情况。如果变量定义了但值是空字符串,则保留空字符串,不使用默认值。这种细微差别在需要区分「未设置」和「显式设为空」的场合才有意义,日常很少用到。

${VAR:?error message} 是「必须提供」的强制校验。如果变量未定义或为空,Compose 会直接报错退出,并把 error message 显示出来。用它保护像数据库 root 密码这种绝不能为空的变量,能在启动前就拦下配置错误,比让它带着空密码跑起来安全得多:

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?数据库 root 密码必须设置}

把默认值和强制校验结合使用,一套配置就能同时应付开发环境和生产环境:开发时用默认值随便跑,生产时强制要求填写真实值。

优先级:谁覆盖谁,这是最容易搞错的地方

变量从哪来?来源不止一个,而且有明确的优先级顺序。搞不清顺序,就会遇到「我明明改了却没生效」的情况。Compose 解析变量的优先级,从高到低大致是:

一、命令行 --env-file 或 shell 中 export 的环境变量。shell 里已经导出的变量优先级最高。也就是说,如果你在启动前执行了 export WEB_PORT=9000,那么即使 .env 里写着 WEB_PORT=8080,最终生效的也是 9000。这个特性既是便利也是陷阱——排查「配置不生效」时,第一件事就是 env | grep 变量名 看看 shell 里是不是残留了同名变量。

二、.env 文件。在没有任何 shell 变量干扰的情况下,项目根目录的 .env 是主要来源。

三、YAML 里 environment 段的直接赋值或者 env_file 指令。注意区分:YAML 里的 environment: 是把变量注入进容器内部,它和 Compose 自己解析 YAML 时用的插值变量是两套机制,别混淆。而 env_file: 指令指定一个文件,把里面的变量注入容器——它不参与 YAML 的插值替换,只负责往容器里塞。

用一个具体例子说清区别:

services:
  app:
    image: myapp
    env_file:
      - .env.app          # 变量进入容器内部
    environment:
      - APP_ENV=production  # 覆盖上面的值,进入容器内部

这里 .env.app 里的变量不会用来替换 YAML 中的 ${...},它只是往容器环境里塞。environment 里的同名变量会覆盖 env_file 的同名变量。很多新手以为 env_file 的内容能用于 YAML 插值,结果发现端口没变,就是混淆了这两件事。

多环境管理:override 文件

有了变量解耦,进一步的需求是「同一套服务,不同环境用不同配置」。Compose 官方的推荐做法是文件叠加(override):基础文件写通用配置,环境文件写差异。运行时会自动合并:docker-compose.yml 加上 docker-compose.override.yml(默认自动加载),或者用 -f 显式指定:

# 生产环境
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# 开发环境
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

合并规则要记住:字典(mapping)是深度合并的,后面的文件覆盖同名的键;而列表(sequence,比如 portsvolumes)是整体追加替换,不是按元素合并。这意味着如果你在基础文件里写了端口 80,在 override 里再写一个端口 8080,结果两个端口都会映射,而不是替换。想真正替换,得在 override 里用 ! 之类的技巧或者干脆把端口配置也变量化。这个「列表不合并」的行为是 Compose 配置管理里最常被误解的一点。

在容器内部使用变量:别把机密塞进镜像

还有一种常见错误,是把配置在构建阶段就写进镜像。比如在 Dockerfile 里 ENV DB_PASSWORD=xxx,或者用 --build-arg 把密码打进镜像层。这两种做法都会把机密留在镜像的历史层里,docker history 一查就暴露,推送到仓库更是灾难。

正确的分层原则是:镜像只包含代码和不变的依赖,所有和环境相关的、敏感的值都在运行时通过环境变量注入。Compose 的 environmentenv_file 正是干这个的。如果确实需要在构建期传参(比如国内镜像源的地址),用 build.args,但要清楚这些值会进入镜像元数据,绝不放过任何机密。敏感数据一律运行时注入。

再进一步,如果对安全要求高,环境变量本身也不是最安全的——docker inspect 能看到容器所有环境变量。更严格的做法是用 Docker secrets(Swarm 模式)或把凭据放在容器外的文件中,通过 volume 挂载进去,并限制文件权限。.env 用于日常个人站已经足够,但知道这条边界,能让你在需要时知道往哪儿升级。

实战排错清单

下面这些是 Compose 变量使用中最常撞上的问题,照着排查基本能覆盖九成:

变量不生效。先运行 docker compose config,它会打印出合并并替换后的最终配置,一眼就能看出变量到底被替换成了什么。这是排查变量问题最有力的工具——比盯着 .env 猜有效得多。

值里的 $ 被吃掉。如果密码里本身含 $(这在随机生成的强密码里很常见),Compose 会把它当成变量引用的开始。解决办法是用 $$ 转义,写成 $$ 才会在容器里得到一个字面量的 $。这个坑在密码为随机串时尤其阴险,因为它不会报错,只是拼出来的密码悄悄不对。

引号被原样带进值里。.env 里的引号处理规则和 shell 不一样。A="hello world" 会正确得到 hello world(去掉外层引号),但如果引号包裹不当,可能把引号也当成内容的一部分。稳妥做法:值里含空格或特殊字符时用双引号包裹,并在 docker compose config 里确认结果。

YAML 冒号后必须有空格。写成 KEY:value(冒号后无空格)不是合法的键值对,YAML 会解析成别的结构或直接报错。这类错误 Compose 的报错信息有时不直观,养成 config 校验的习惯能省很多时间。

改了 .env 但容器没更新。环境变量是在容器创建时注入的,修改 .env 后光 restart 不够——restart 只是重启已有容器,配置没有重新解析。必须 docker compose up -d 让 Compose 重建容器,或者 down 之后再 up。这是「改了配置不生效」最常见的原因。

小结

Docker Compose 的配置管理,核心就三句话:.env 把敏感配置和编排文件解耦、用插值语法加默认值和强制校验、用 override 文件管理多环境差异。围绕这三条,记住几个关键细节:变量优先级是 shell > .env > YAML;env_file 只管往容器里塞、不参与插值;列表类型在 override 里不合并;密码里的 $ 要写成 $$;排查问题先跑 docker compose config

.env 加进 .gitignore、保留一份 .env.example 做模板,配合「镜像只装代码、机密运行时注入」的分层原则,你的 Compose 项目就能在安全、可维护、易于迁移之间取得一个漂亮的平衡。这套东西学一次,之后每一套容器化部署都用得上,值得花时间彻底搞懂。

Last modification:September 14th, 2026 at 12:31 pm

Leave a Comment