Docker Compose 中 .env、env_file 和 environment 区别
Docker Compose 中的 env 文件有 3 种含义:.env 负责插值,env_file 注入容器,environment 直接设值。本文用命令验证优先级,并说明密码为何不该放进环境变量。
人们常说的 env 文件其实指 3 种不同的东西
Docker Compose 提供 3 种名称相近但机制不同的功能。.env 文件会在 Compose 解析文件之前,替换 compose.yaml 中的 ${VARIABLE} 占位符。env_file: 属性会将键值对文件中的变量加载到容器环境中。environment: 属性则直接在 compose 文件中为容器设置变量。它们不能互换。如果其中两种机制设置了同一个键,最终值由文档规定的优先级顺序决定。
本指南会分别演示这 3 种机制,使用可运行的命令验证优先级,然后介绍更重要的问题:任何可以运行 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 会在项目目录中查找 .env。项目目录就是存放 compose 文件的目录。然后,Compose 会替换找到的每个 ${NAME}。
现在运行服务。
docker compose run --rm demoprintenv ALPINE_TAG 以状态码 1 退出,且不输出任何内容。该变量在容器内不存在。这是最常见的误解:.env 配置的是 compose 文件,而不是进程。仅仅在 .env 文件中写入 POSTGRES_PASSWORD=hunter2,对数据库完全不起作用,除非 compose 文件的某个部分引用了它。
${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 文件。大多数情况下,引号会作为值的一部分保留,不需要使用 export 前缀。不要在 = 符号两侧添加空格,因为 KEY = value 会生成一个名为 KEY 的变量,并在其值开头包含一个空格。
找不到 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 中将该变量透传过来。
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 会将其打印到终端,因此它可能被复制并粘贴到支持论坛中。容器内的任何进程都可以读取 /proc/1/environ,并且每个子进程都会继承该变量。应用程序的崩溃处理程序还经常将整个环境转储到日志或错误报告中。
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} 占位符。它不会设置容器内的变量。要将值传入容器,请引用它: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 文件中可以使用引号和空格吗?
使用 KEY=value with spaces,并省略引号。Compose 会将该行剩余的全部内容作为值,因此引号通常会成为值中的字面字符。= 两侧不要添加空格,否则键名会带有尾随空格,导致任何匹配都失败。