上周五下午五点半,我想着改个环境变量就下班——结果硬是搞到七点多才走。问题很简单:我把 docker-compose.yml 旁边的 .env 文件里的 DB_PORT 从 3306 改成了 3307,然后 docker compose up -d,结果容器起来后还是连的 3306。
说出来你可能不信,就这个破事儿,我排查了快两个小时。记录一下踩坑过程,下次别再犯。
复现问题
先看我的目录结构:
project/
├── docker-compose.yml
├── .env
└── app/
└── .env
docker-compose.yml 长这样:
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
ports:
- "${DB_PORT}:3306"
app:
build: ./app
environment:
DB_PORT: ${DB_PORT}
DB_PASSWORD: ${DB_PASSWORD}
.env 文件内容:
DB_PORT=3307
DB_PASSWORD=MySecret123
看起来没问题对吧?改完 DB_PORT=3307,执行:
docker compose down && docker compose up -d
然后用 docker compose ps 一看,端口映射还是 0.0.0.0:3306->3306/tcp。我人都傻了。

排查过程
第一反应:是不是缓存?
docker compose down -v # 加 -v 清 volume
docker compose up -d
没用,还是 3306。
第二反应:是不是没读 .env?
用 docker compose config 看看实际解析出来的配置:
docker compose config | grep DB_PORT
输出:
DB_PORT: "3306"
好家伙,确实是 3306,根本没读我改的 .env。
第三反应:检查文件编码和换行符
file .env
# .env: ASCII text
不是编码问题。
关键突破:app/ 目录下还有一个 .env
我突然想起来,app/ 目录下的 app/.env 文件是之前写的,里面有一行:
DB_PORT=3306
而 docker compose config 的文档里写了这么一段话(很多人没注意):
When you set the same environment variable in multiple sources, Docker Compose uses priority: Shell env > –env-file > project .env > service working dir .env
更坑的是:docker compose 在 build 阶段会自动读取 build context 目录下的 .env 文件,而且这个优先级在某些情况下高于项目根目录的 .env。
验证一下:
# 在 app/ 目录放一个 .env
echo "DB_PORT=3306" > app/.env
# 项目根 .env 写 DB_PORT=3307
echo "DB_PORT=3307" > .env
# compose config 看看
docker compose config | grep DB_PORT
# 输出: DB_PORT: "3306" ← 取的是 app/.env 的值!

解决方案
有三种修法,按推荐度排序:
方案一(推荐):用 env_file 明确指定
services:
app:
build: ./app
env_file:
- .env # 明确指定读项目根目录的 .env
方案二:删掉子目录的 .env
如果 app/.env 不是必须的,直接删掉。简单粗暴但有效。
方案三:用 –env-file 参数强制指定
docker compose --env-file .env up -d
我最终用了方案一,因为 app/.env 是给本地开发用的(npm run dev 会读它),不能删。

踩坑总结
- docker compose config 是排查神器。变量解析有问题,第一时间用它看实际值,别对着 compose 文件和 .env 文件肉眼 Debug。
- build context 里的 .env 会干扰变量解析。这个官方文档写得很隐晦,在 Compose Build Specification 那一节才提到。如果你的 compose 文件里既有 build 又有根目录 .env,要特别注意优先级。
- 项目根 .env 和 env_file 不是一回事。根 .env 是 compose 自动读的默认文件,用于变量替换(
${VAR}语法)。env_file 是把整个文件内容注入容器的环境变量。两者语义不同,优先级也不同。 - 不同 Compose 版本行为有差异。Compose v1(docker-compose)和 v2(docker compose)在这个细节上的处理不完全一致,实测用 v2 遇到这个问题概率更高。
- 排查时注意区分「变量替换」和「环境变量注入」。
${DB_PORT}在 compose 文件解析阶段替换是一次,容器内的$DB_PORT是另一次。两个阶段可能读到不同的值——别搞混了。
折腾完这两个小时,我在想一个问题:Docker Compose 的 .env 优先级逻辑,算不算过度设计了?一个 .env 能解决的事,非要搞多层覆盖,最后坑的都是没仔细看文档的人。
你说是不是?