IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
在第 12 篇中,我们把 Compose 文件的三大核心模块------services、networks、volumes------拆解得明明白白。但你有没有注意到一个问题:我们的 docker-compose.yml 里还残留着硬编码?
比如 FLASK_ENV=production 直接写死在 YAML 里,REDIS_HOST=cache 也是写死的。如果换一台机器、换一个环境,这些值需要改动怎么办?难道每次都要改 Compose 文件然后小心别提交到 Git?敏感信息比如数据库密码、API 密钥又该怎么处理,才能既保证安全又不影响团队协作?
这篇就来解决这些问题。读完你会发现,Docker Compose 提供了一套完整的环境变量注入机制------从简单的环境变量设置,到 .env 文件加载,再到多配置文件的组合覆盖。这套机制的核心思路,在 Kubernetes 的 ConfigMap 和 Secret 中得到了更彻底的实现。
一、为什么需要配置管理?
任何需要部署到多个环境的应用程序,都绕不开配置管理的三个核心挑战:
-
环境差异性:开发环境用本地 Redis,生产环境用 Redis 集群。如果配置写死在代码或镜像里,换个环境就要重新构建。
-
安全性:数据库密码、API 密钥、证书------这些东西绝对不能提交到 Git 仓库。
-
团队协作:每个开发者可能需要不同的本地配置,但不能相互干扰。
Docker Compose 的设计原则是:将应用的行为定义(YAML)与环境的差异配置(变量)分离 。镜像和 Compose 文件可以提交到 Git,而敏感的或与环境相关的配置通过环境变量在运行时注入。这个原则,在 Kubernetes 中体现为 ConfigMap(非敏感配置)和 Secret(敏感配置)的分离,以及 Deployment 中通过 envFrom 注入的方式。
二、Compose 环境变量的四种来源
Docker Compose 支持多种方式向容器传递环境变量,它们有不同的优先级。理解优先级至关重要,否则可能遇到"明明设置了变量,容器里读到的却是旧值"的困惑。
2.1 Compose 文件中的 environment
最直接的方式,在 docker-compose.yml 的 environment 下直接写:
bash
services:
flask-app:
environment:
- FLASK_ENV=production
- REDIS_HOST=cache
- LOG_LEVEL=info
或者用字典格式(等号写法):
bash
services:
flask-app:
environment:
FLASK_ENV: production
REDIS_HOST: cache
LOG_LEVEL: info
这种方式适合那些可以公开、不敏感的配置项,比如日志级别、运行模式。但如果把数据库密码写在这里,任何能访问代码仓库的人都能看到------这就是硬编码敏感信息的典型反模式。
2.2 env_file:从文件加载
bash
services:
flask-app:
env_file:
- .env
- .env.local # 可以指定多个文件,后者覆盖前者中同名的变量
.env 文件内容(标准 key=value 格式):
bash
FLASK_ENV=development
REDIS_HOST=localhost
REDIS_PORT=6379
env_file 和 environment 可以同时使用,environment 中的值会覆盖 env_file 中的同名变量。当同一个变量在多个来源中出现时,优先级从高到低为:命令行 -e 参数 > Compose 文件 environment > env_file 文件 > 镜像默认值。
关键补充------.env 文件的项目级用途:
需要注意,.env 文件除了作为 env_file 的来源,还有一个特殊角色:Compose 项目级变量 。当 .env 文件放在 docker-compose.yml 同目录时,其中的变量会自动被 Compose 用于解析 YAML 文件中的 ${VAR} 插值------但不会 自动注入容器。如果想让变量注入容器,必须在 env_file 或 environment 中显式引用。
安全提醒 :
.env文件如果包含密码、API 密钥等敏感信息,必须加入.gitignore,否则这些机密会被提交到版本控制系统。本系列后续 Kubernetes 部分将介绍如何用 Sealed Secrets 或 External Secrets Operator 实现更安全的 GitOps 密钥管理。
2.3 Shell 环境变量与 .env 项目文件
Compose 在解析 docker-compose.yml 时,会自动替换 ${VARIABLE} 占位符。这些变量的值可以从两个来源读取:一是 Shell 环境变量,二是 .env 项目文件。
重要:.env 项目文件 vs env_file 的区别
这是 Compose 使用中最容易混淆的概念之一:
-
.env项目文件:放在 Compose 文件同目录,自动被 Compose 读取,用于解析 YAML 中的${VAR}占位符,但不自动注入容器。 -
env_file:在 services 下显式声明,变量直接注入对应容器 内部,应用程序可以通过os.environ.get('KEY')读取。
举个例子就清楚了:
bash
# docker-compose.yml
services:
flask-app:
image: flask-redis-counter:${TAG:-2.0} # ← ${TAG} 从 .env 项目文件读取
environment:
- REDIS_HOST=${REDIS_HOST} # ← ${REDIS_HOST} 从 .env 项目文件读取
- DB_PASSWORD=${DB_PASSWORD} # ← 也来自 .env 项目文件,但会被 environment 注入容器
对应的 .env 项目文件:
bash
TAG=3.0
REDIS_HOST=redis-prod
DB_PASSWORD=secret123
TAG 只影响 YAML 的插值,不会被注入容器;REDIS_HOST 和 DB_PASSWORD 既影响 YAML 插值,又通过 environment 注入容器。而如果你在 env_file 中指定的文件里有 LOG_LEVEL=debug,这个变量会被直接注入容器,但不会 用于 YAML 中的 ${LOG_LEVEL} 替换------除非你在 .env 项目文件中也定义了它。
Shell 环境变量和 .env 项目文件的优先级:
Shell 环境变量会覆盖同名 .env 项目文件中的值。这个机制非常实用------CI/CD 流水线可以在 Shell 中设置生产环境的变量,自动覆盖 .env 中的默认值,而无需修改任何文件。
2.4 命令行 -e 参数(最高优先级)
bash
docker compose run -e FLASK_ENV=staging flask-app
这是临时调试或一次性操作时的利器,优先级最高,会覆盖其他所有来源的同名变量。
三、变量替换语法
Compose 支持多种 Shell 风格的变量替换语法:
实际应用示例:
bash
services:
flask-app:
image: flask-redis-counter:${TAG:-latest} # TAG 未设置时默认用 latest
ports:
- "${PORT:-5000}:5000" # 端口可动态配置
environment:
- DB_PASSWORD=${DB_PASSWORD:?数据库密码必须设置} # 强制要求设置,未设置则报错退出
四、多环境配置实战
现在我们为贯穿案例配置三种环境:开发、测试、生产。项目目录结构如下:
bash
flask-redis-counter/
├── .env.dev # 开发环境
├── .env.test # 测试环境
├── .env.prod # 生产环境
├── docker-compose.yml # 基础配置
└── docker-compose.override.yml # 本地覆盖(开发环境)
4.1 各环境的 .env 文件
.env.dev:
bash
TAG=2.0
FLASK_ENV=development
REDIS_HOST=redis
LOG_LEVEL=debug
PORT=5000
.env.prod:
bash
TAG=2.0
FLASK_ENV=production
REDIS_HOST=redis-prod.internal
LOG_LEVEL=warn
PORT=80
4.2 使用 --env-file 指定环境
bash
# 开发环境
docker compose --env-file .env.dev up -d
# 生产环境
docker compose --env-file .env.prod up -d
--env-file 参数告诉 Compose 使用指定的 .env 项目文件来解析 ${VAR} 占位符。这样,同一份 docker-compose.yml 就能在不同环境中表现出不同的行为。
4.3 优先级验证实验
我们来做一个完整的实验,验证各种变量来源的优先级。这是透彻理解配置管理的关键一步。
创建一个测试用的环境文件 .env.test:
bash
PRIORITY=from-env-file
ENV_FILE_ONLY=only-in-env-file
Compose 文件中定义一个测试服务:
bash
services:
test-env:
image: alpine
container_name: env-test
environment:
- PRIORITY=from-compose-file
- COMPOSE_ONLY=only-in-compose
env_file:
- .env.test
command: sh -c "echo 'PRIORITY='$$PRIORITY; echo 'ENV_FILE_ONLY='$$ENV_FILE_ONLY; echo 'COMPOSE_ONLY='$$COMPOSE_ONLY; echo 'SHELL_VAR='$$SHELL_VAR; sleep 10"
这里用 $$ 而不是 $ :因为 Compose 在解析 YAML 时会先处理 ${VAR} 占位符,如果你写 $PRIORITY,Compose 会在主机 Shell 或 .env 项目文件中寻找 PRIORITY 变量进行替换。写成 $$PRIORITY 是将单个 $ 传递给容器内的 shell,让容器内的 shell 去展开这个变量。
执行测试:
bash
# 先查看 .env.test 内容
cat .env.test
# 不带任何额外变量
docker compose up test-env
docker logs env-test
# PRIORITY=from-compose-file ← environment 覆盖了 env_file
# ENV_FILE_ONLY=only-in-env-file ← 仅存在于 env_file
# COMPOSE_ONLY=only-in-compose ← 仅存在于 environment
# SHELL_VAR= ← Shell 中未设置,为空
docker compose down
# 用 Shell 环境变量覆盖(Linux/macOS)
SHELL_VAR=from-shell docker compose up test-env
docker logs env-test
# ...
# SHELL_VAR=from-shell ← Shell 环境变量成功传入!
docker compose down
# 用命令行 -e 覆盖(最高优先级)
docker compose run --rm -e PRIORITY=from-cli test-env
# PRIORITY=from-cli ← CLI 参数覆盖一切
通过这个实验,你可以清晰地看到:environment 会覆盖 env_file 中的同名变量,而 Shell 变量和 CLI -e 参数又能覆盖 environment。在排查"变量值不对"的问题时,按这个优先级链条逐一检查即可。
4.4 清理测试容器
bash
docker rm -f env-test 2>/dev/null || true
docker compose down 2>/dev/null || true
五、安全实践:哪些不能写进 Compose 文件?
这是一个容易被忽视但极其重要的主题。以下内容永远不要硬编码在 YAML 中,也不要提交到 Git:
-
数据库密码
-
API 密钥 / Token
-
第三方服务凭证(AWS Access Key、SMTP 密码等)
-
加密证书和私钥
-
签名密钥
这些值应该通过运行时注入,Compose 中常用的安全注入方式包括:将敏感信息写入 .env 文件并加入 .gitignore;在 CI/CD 流水线中通过环境变量注入;使用 Docker Swarm 的 Secrets(Swarm 模式);在 Kubernetes 中使用 Secret + Sealed Secrets。
在 .gitignore 中添加:
bash
.env
.env.*
!.env.example # 示例文件可以提交,但不应包含真实凭证
示例模板的最佳实践 :建议在项目中提供一个
.env.example文件,包含所有必需变量的键名和占位示例值,提交到 Git 供团队成员参考。新成员克隆项目后,只需cp .env.example .env并填入真实值即可启动项目。
六、实战:为 Flask + Redis 重构多环境配置
现在我们把前面学到的知识,落地到贯穿案例的 docker-compose.yml 中。
6.1 消除硬编码的 Compose 文件
bash
# docker-compose.yml
services:
redis:
image: redis:alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb
volumes:
- redis-data:/data
networks:
- app-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
flask-app:
image: flask-redis-counter:${TAG:-2.0}
restart: unless-stopped
ports:
- "${PORT:-5000}:5000"
environment:
- FLASK_ENV=${FLASK_ENV:-production}
- REDIS_HOST=redis
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- flask-logs:/app/logs
networks:
- app-net
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
volumes:
redis-data:
flask-logs:
networks:
app-net:
driver: bridge
注意这里 REDIS_HOST=redis 没有用 ${} 占位------因为服务名 redis 是由 Compose 网络 DNS 自动解析的,跨环境都是同一个值,没必要参数化。需要参数化的是那些随环境变化的变量。
6.2 环境配置文件
.env.dev(开发环境):
bash
TAG=dev
FLASK_ENV=development
LOG_LEVEL=debug
PORT=5000
.env.prod(生产环境):
bash
TAG=2.0
FLASK_ENV=production
LOG_LEVEL=warn
PORT=80
6.3 多环境启动验证
bash
# 开发环境启动
docker compose --env-file .env.dev up -d
docker compose ps
curl http://localhost:5000
# Hello World! I have been seen 1 times.
# 查看容器内的环境变量,确认正确注入
docker compose exec flask-app env | grep FLASK_ENV
# FLASK_ENV=development
docker compose exec flask-app env | grep LOG_LEVEL
# LOG_LEVEL=debug
# 切换环境前先清理
docker compose down -v
注意 :这里用了 -v 来删除数据卷。因为开发环境和生产环境的数据应该是隔离的,清理卷能避免数据残留导致的困惑。但在生产环境中操作时,-v 会永久删除所有持久化数据,务必确认备份后再执行。
bash
# 生产环境启动
docker compose --env-file .env.prod up -d
docker compose ps
6.4 .env.example 模板
在项目根目录创建 .env.example,提供给团队成员:
bash
# ===========================================
# Flask + Redis 计数器应用 ------ 环境配置模板
# 复制此文件为 .env 并填入真实值
# ===========================================
# 镜像版本
TAG=2.0
# 运行环境:development / production
FLASK_ENV=development
# 日志级别:debug / info / warn / error
LOG_LEVEL=debug
# 宿主机端口
PORT=5000
七、Compose 配置调试技巧
当变量不生效时,如何快速定位问题?
7.1 查看最终配置
这条命令会输出变量替换后的完整配置 。如果某个 ${VAR} 没有被替换,说明变量未设置且没有默认值。这是排查配置问题的第一步,也是最重要的一步。
7.2 查看容器内的实际环境变量
bash
docker compose exec flask-app env
这能看到最终注入到容器内的所有环境变量------environment、env_file 和镜像自带的环境变量都会被列出。
7.3 常见问题排查
问题一 :变量没被替换,YAML 中直接输出 ${TAG} 字面量。
原因 :.env 项目文件不存在,或 Shell 环境中也未设置该变量,且没有提供默认值 ${VAR:-default}。Compose 找不到变量值就会保留原始占位符。解决方案是确保 .env 文件存在于 docker-compose.yml 同级目录,或显式指定 --env-file 路径。
问题二:容器内的变量值不对。
原因 :可能是 env_file 和 environment 同时设置了同名变量,而 environment 覆盖了 env_file;或者是 .env 项目文件和 env_file 文件内容不同导致混淆。解决方案是用 docker compose config 确认最终注入值,并用 docker compose exec <service> env 查看容器内实际环境变量。
问题三:敏感信息泄露到了镜像层。
原因 :在 Dockerfile 中用 ENV 硬编码了密码。docker history 可以查看所有镜像层的 ENV 值,密码就这样暴露了。解决方案是永远不要在 Dockerfile 的 ENV 中设置敏感信息,而是通过运行时环境变量注入。
八、命令速查表
九、本篇总结
-
四种环境变量来源 :
environment(直接写 YAML)、env_file(从文件加载)、Shell 环境变量 /.env项目文件(YAML 变量插值)、命令行-e(最高优先级)。 -
变量替换语法 :
${VAR}、${VAR:-default}、${VAR:+value}、${VAR:?error}------理解这些语法是编写灵活 Compose 文件的基础。 -
多环境切换 :通过
--env-file指定不同的.env文件,同一份 YAML 适配开发、测试、生产环境,无需修改 Compose 文件本身。 -
安全红线 :敏感信息永远不硬编码在 YAML 中、不提交到 Git,通过
.env文件 +.gitignore管理。提供.env.example模板方便团队协作。 -
调试三板斧 :
docker compose config(查看替换后的配置)→docker compose exec <service> env(查看容器内变量)→ 核对优先级链(CLI > environment > env_file)。
下一篇文章------第 14 篇:Compose 开发环境最佳实践:热重载与调试,我们将深入开发效率优化,用 Bind Mount 实现代码修改后秒级生效,结合 VS Code 远程调试,让你的本地开发体验从"频繁重建镜像"变成"保存即刷新"。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !