Docker Compose 中 .env、env_file 和 environment 区别
Docker Compose 中的 env 文件有 3 种含义:.env 负责插值,env_file 注入容器,environment 直接设值。本文用命令验证优先级,并说明密码为何不该放进环境变量。
人们称为 env 文件的三种机制
Docker Compose 提供了三种名称相近但机制不同的功能。.env 文件会在 Compose 读取 YAML 后、验证或运行任何内容前,替换 compose.yaml 内的 ${VARIABLE} 占位符。env_file: 属性会将键值对文件中的内容加载到容器环境中。environment: 属性会直接为容器设置变量,这些变量写在 compose 文件中。它们不能互换使用。如果其中两个机制设置了同一个键,最终值将按照文档规定的优先级顺序确定。
本指南将演示每种机制的实际用法,并通过可运行的命令验证优先级。随后介绍更重要的问题:任何能够运行 docker inspect 的用户都可以读取环境变量,因此不应将密码放入其中。如果您刚开始接触 compose 文件,请先阅读 VPS 上的 Docker Compose 基础知识,然后返回这里了解配置。
.env 文件供 compose 文件使用,而不是供容器使用
创建一个目录,并在其中放入两个文件。
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAG现在让 Compose 显示它实际解析出的内容。
docker compose config输出显示 image: alpine:3.20。占位符已经消失,因为 Compose 在加载文件时就完成了变量插值。Compose 会在项目目录中查找 .env:如果设置了 --project-directory,则使用该选项指定的目录;否则使用通过 -f 传入的第一个文件所在的目录;如果两者都未设置,则使用当前目录。Compose 会替换 YAML 值中的 ${NAME} 占位符。键不会进行插值,连续两个 $$ 会生成字面量美元符号,而不会触发替换。
然后启动服务。
docker compose run --rm demoprintenv ALPINE_TAG 以状态码 1 退出,且不输出任何内容。该变量在容器内不存在。这是最常见的误解:.env 配置的是 compose 文件,而不是进程。除非 compose 文件中的某个部分引用了 .env 文件,否则其中包含 POSTGRES_PASSWORD=hunter2 对数据库完全不起作用。
${NAME:-default} 会在变量未设置或为空时提供备用值。${NAME:?message} 会让 Compose 拒绝启动并输出指定消息。对于没有安全默认值的配置项,应选择这种方式。
env_file 将变量加载到容器中
`env_file:` 属性用于指定一个或多个文件,这些文件中的内容会成为容器环境变量。
printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.envservices:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.envdocker compose run --rm demo此命令输出 `from_env_file。文件格式是纯 KEY=value 行,每行一个变量;以 # 开头的内容表示注释。它不是 shell 文件,但 Compose 会按少量类似 shell 的规则解析它。首尾引号会被去除,因此 KEY="value" 和 KEY=value 的结果相同。双引号值和未加引号的值会插入 ${...} 占位符,单引号值则按字面使用。可以使用 export 前缀,但不是必需的。= 符号两侧的空格会被去除。需要注意行内注释:在未加引号的值中,空格后紧跟 # 会开始注释,因此包含 #` 的值必须加引号。
如果 `env_file` 路径不存在,就会报错,Compose 会停止运行。如果文件确实可能不存在,请将其标记为可选:
env_file:
- path: ./app.env
required: falseenvironment 可内联设置变量
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environment支持两种语法:上面的映射形式,以及使用 - GREETING=from_environment 的列表形式。两者行为相同。两种形式还有一个额外特性:不指定值的键会从运行 docker compose 的 shell 中透传该变量。在列表形式中,这是单独的 - GREETING;在映射形式中,这是冒号后没有任何内容的 GREETING:。
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demo该命令会输出 from_my_shell。如果未在 shell 中设置 GREETING 就运行该命令,Compose 不会设置任何值,也不会发出警告。需要注意这种静默的透传失败,因为服务使用空密码变量启动时通常不会启动失败,而是直接处于未受保护状态。
谁的优先级更高
Docker 规定了优先级顺序,越靠前优先级越高:命令行中的 docker compose run -e,然后是 environment 或 env_file,其值会从 shell 或 env 文件中插入,再然后是 compose 文件中的普通 environment,接着是 env_file,最后是镜像中固化的 ENV 指令。
日常使用可以简化为:environment: 的优先级高于 env_file:,命令行中的 -e 则高于这两者。下面在一个文件中验证这一点。
services:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.env
environment:
GREETING: from_environmentdocker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETING第一个命令输出 from_environment,说明 environment: 覆盖了 app.env 中的值。第二个命令输出 from_cli。compose 文件中的任何内容都不会覆盖命令行参数。
如果容器的行为表明配置没有生效,不要凭猜测判断。docker compose config 会输出完全解析后的文件,docker compose config --environment 会输出 Compose 使用的插值变量。大多数“我的 env 文件被忽略了”问题,最终都源于同一个值在两个不同层级被设置了两次。
环境变量为何会泄露
在 environment: 中设置密码后,密码会存储在容器的配置文件中。docker 组中的任何用户都可以查看该文件。
docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'输出中包含明文形式的 "DB_PASSWORD=hunter2"。另外还有 3 个路径会暴露相同的值。docker compose config 会将其打印到终端,因此它可能被复制并粘贴到支持论坛中。除非父进程有意使用替换后的环境启动子进程,否则子进程默认会继承该变量。与 PID 1 以同一用户身份运行的进程,或持有 CAP_SYS_PTRACE 的进程,可以从 /proc/1/environ 读取该变量。在所有进程都以 root 身份运行的容器中,任何进程都可以读取它。如果应用以非特权用户身份运行,而 PID 1 以 root 身份运行,那么应用读取该文件时会收到权限错误;但该变量仍保存在应用自身的环境中。此外,应用的崩溃处理程序通常会将整个环境写入日志或错误报告。
在 docker 组中的成员实际上拥有主机上的 root 权限,因此不能将该组视为权限边界。VPS 上的最小权限用户账户一文解释了为什么在任何共享服务器上都应限制该组的成员资格。
Compose secrets 将值保存在文件中
Compose 支持基于文件的 secrets。值会以文件形式挂载到容器中,而不是注入环境变量。
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtsecret 会挂载到容器内的 /run/secrets/db_password。斜杠后的名称来自顶层 secrets: 块中的 secret 名称。
_FILE 后缀是 Docker Official Images 使用的约定,包括 postgres、mysql 和 mariadb。这些 entrypoint 脚本会检查 VARNAME_FILE,读取文件并使用其内容。这不是 Docker 的功能,因此只有镜像实现了该约定时才有效。在假设 SOMETHING_FILE 会生效前,请先查看镜像文档。不支持该约定的应用通常可以在启动时自行读取文件;也可以传递文件路径,由您自己的 entrypoint 处理。
从运行中的容器内部验证:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORD第一条命令会输出密码。第二条命令不会输出任何内容,因为该值从未进入环境变量。这正是这种方式的意义:此容器中的 docker inspect 只显示无害的文件路径。
保护主机上的源文件,因为 secret 的私密性取决于其背后的文件:
chmod 600 db_password.txtVPS 上的实用折中方案
许多自托管镜像不支持 _FILE 变量,因此环境变量是唯一的传入方式。对于只有一名管理员的 VPS,现实目标是避免这些值存放在项目目录中所有用户都可读取的文件里,并将其排除在 git 之外。
sudo install -o root -g root -m 600 /dev/null /etc/myapp/app.env
sudo nano /etc/myapp/app.env env_file:
- /etc/myapp/app.envinstall -m 600 会以预设权限创建文件,因此不会出现文件暂时对所有用户可读的时间窗口。文件归 root 所有,因此主机上的非 root 用户无法读取它,但任何能够运行 docker 的用户仍可从容器中读取该值。将 *.env 和 .env 添加到 .gitignore,然后提交一个 app.env.example,其中保留密钥名称,值为空。已提交到代码库的密码就应视为已泄露并完成轮换。
轮换值意味着重启服务。容器进程启动时只读取一次环境变量,因此编辑文件不会立即生效,直到运行 docker compose up -d --force-recreate db。VPS 上通过 HTTPS 运行 n8n 指南也使用相同模式,其中加密密钥存放在 compose 文件之外。
按环境拆分配置
Compose 默认从项目目录读取 .env。使用 --env-file 可将其指定到其他位置。
docker compose --env-file .env.staging configCompose 会按顺序读取多个文件,后读取的文件会覆盖先读取的文件。将非机密默认值保存在已提交到版本控制的文件中,将密钥保存在永不离开服务器的文件中。env_file: 也遵循相同规则:对于重复键,最后列出的文件优先。
FAQ
为什么容器内的 .env 文件会被忽略?
它并没有被忽略。.env 文件会替换 compose 文件中的 ${NAME} 占位符,也可以设置 Compose 自身的变量,例如 COMPOSE_PROJECT_NAME 或 COMPOSE_FILE。它不会设置容器内的变量。要将值传入容器,请引用它:environment: { KEY: "${NAME}" },或改用 env_file: ./that-file.env。
environment 会覆盖 env_file,还是反过来?
environment: 的优先级更高。Docker 的文档规定,environment 属性的优先级高于 env_file 属性,而这两者的优先级都低于命令行中的 docker compose run -e。如果同一个键在两处都设置,env_file 中的值会被静默忽略。
如何查看 Compose 最终使用的值?
运行 docker compose config,即可输出已完全解析的 compose 文件,其中包含所有插值结果。对于已经运行的容器,docker inspect <container> --format '{{json .Config.Env}}' 会准确显示其进程接收到的内容。
Compose secrets 会加密吗?
不会。基于文件的 secret 会以普通文件的形式挂载到容器中的 /run/secrets/<name>,源文件也会以未加密的形式存储在主机磁盘上。它的优势在于作用域,而不是加密:该值不会出现在容器环境变量中、docker inspect 输出中,也不会出现在打印环境变量的崩溃转储中。
env 文件支持引号和空格吗?
支持。Compose 会去除外层引号,因此 KEY="value with spaces" 和 KEY=value with spaces 都会得到 value with spaces。如果值包含不应进行插值的 $,请使用单引号;如果值包含 #,也应将其加引号,因为未加引号时它会开始一段注释。= 两侧的空格会被去除,因此 KEY = value 仍会设置 KEY。