你修改了项目里的 .env 文件,把数据库连接串、某个开关或 API 地址更新成了新值,然后按肌肉记忆敲下 docker compose restart。容器转了一圈回来了,可程序里读到的还是那个旧值。你反复确认 .env 文件明明已经改了,甚至怀疑自己改错了文件。
现象
典型表现是:程序行为"没变"——连的旧地址、用的旧超时、开关还是老状态。但你去容器里查环境变量:
docker compose exec app env | grep MY_VAR
发现 MY_VAR 果然还是旧值。可 .env 文件里的新值就写在那里。更气人的是,把容器删掉重建,新值立刻就生效了。
根因
关键在于理解环境变量是在什么时候注入容器的。
容器的环境变量,是在创建容器的那一刻(docker create / docker run)由 Docker 写进容器配置里的。这些值会被固化在容器的元数据中,此后容器的进程启动时读到的,就是这份固化下来的快照。
而 docker compose restart 做的事情非常有限:它只是给已经存在的那个容器发一个信号(默认 SIGTERM,等待后 SIGKILL),然后让它重新启动里面的进程。注意——容器本身没有重建,容器配置一个字都没改,那份固化下来的旧环境变量快照原封不动。
于是就成了这样:配置文件变了,容器配置没变,进程读的是容器配置,所以你看到旧值。
有一个很直观的办法可以验证这一点:看容器的创建时间。
docker inspect --format '{{.Created}}' <容器名>
如果这个创建时间比你改 .env 的时间还早,那就说明容器根本没被重建过——它还是当初那个,环境变量自然还是当初那份。restart 不会刷新这个 Created 时间戳,只有重建才会。
up -d 则不同。它会比较"当前 compose 文件 + .env 计算出的目标状态"和"现有容器的实际状态",一旦发现环境变量、镜像、命令、挂载等创建期属性不一致,就会销毁旧容器、按新配置重建一个。重建时环境变量被重新注入,新值自然生效。
一句话总结:restart 重的是进程,up -d 重的是容器。
解决
改了 .env、改了 compose.yaml 里的 environment/env_file 之后,用:
docker compose up -d
它会自己检测到差异并重建需要重建的容器(不需要重建的会原样保留,不会全部推倒)。
改完确认一下注入的值:
docker compose exec app env | grep MY_VAR
docker compose config | grep -A2 MY_VAR # 看 compose 最终解析出的配置
如果重建后还是旧值,八成是"改错了地方"——比如值定义在 compose.yaml 的 environment: 里,而 environment 的优先级高于 env_file,你只改了 .env 是压不过它的。docker compose config 会把它最终认定的一份配置打印出来,照着看就不会被优先级绕晕。
延伸与预防
把这条规则推广成一句话:任何"在容器创建时注入"的东西,改完都必须重建容器。属于这一类的有:
- 环境变量(
environment/env_file); - 端口映射(
ports); - 卷挂载(
volumes); - 网络归属(
networks); - 镜像本身(
image); - 命令与入口(
command/entrypoint)。
反过来,restart 只适合"进程卡死了、想让它重新跑一遍"这种场景。
养成一个习惯:改配置后先问自己"我改的是创建期属性,还是运行时读的文件"。前者要 up -d(重建),后者(比如挂载进去的配置文件)才可能 restart 就够。这个判断能省掉大量"我明明改了为什么没用"的抓狂。