环境变量与配置管理:12-Factor 与 .env 实践

应用的配置是指"会随部署环境变化"的一切:数据库连接串、外部服务凭据、每套部署的规范主机名等。12-Factor 第 3 条的核心主张是:配置必须与代码严格分离,存储到环境变量中。判断标准很朴素——"这份代码能否在任意时刻开源,而不会泄露任何凭据?"

一、为什么要用环境变量

12-Factor 文档指出,把配置写成代码常量违反了十二要素原则:配置因部署而异,代码不变。把配置放进环境变量有三个好处:

  1. 不同部署间无需改代码即可切换配置(staging / production / 本地);
  2. 几乎不可能被误提交进代码仓库
  3. 环境变量是语言与操作系统无关的标准。
# 示例:为一次部署注入配置
export DATABASE_URL="postgres://user:pass@db-host:5432/app"
export SMTP_HOST="smtp.example.com"
./bin/app

二、.env 文件的正确用法

环境变量在命令行里写起来麻烦,因此出现了 .env 约定:把键值对放进文件,由框架或工具加载进环境。Node.js、Docker Compose、Laravel 等生态都广泛支持。

# .env(不提交进版本库)
DATABASE_URL=postgres://user:pass@db-host:5432/app
SMTP_HOST=smtp.example.com
APP_DEBUG=false

关键纪律:

  • .env 必须加入 .gitignore绝不提交
  • 仓库中提供一个 .env.example 模板,只含键名与占位值;
  • 生产环境优先用真正的环境变量或密钥管理,.env 更适合本地与 CI。

配置的分层与优先级

实际项目里配置来源往往不止 .env 一处。按"越靠后越优先"的顺序,常见来源从低到高是:代码内默认值 → .env 文件 → Shell 环境变量 → 部署平台注入(Docker Compose、Kubernetes ConfigMap/Secret、云平台环境变量)。这样分层之后,开发时用 .env 填本地值,CI 用平台变量覆盖测试库地址,生产环境再注入真实凭据,代码与构建产物完全不用动。

来源 优先级 典型用途
代码内默认值 最低 仅作兜底,不得包含敏感信息
.env 文件 本地开发与本地测试
Shell / 部署平台变量 CI、staging、production 注入
密钥管理系统 最高 数据库口令、API Key 等机密

一个常见的坑是"本地能跑、部署后连不上数据库"。多数情况下是因为本地 .env 里的旧变量覆盖了平台新注入的值。Node.js 的 dotenv 默认不覆盖已有环境变量,Laravel 的 env() 也只在两者都缺失时才取默认值——正是为了保住"平台变量优先"的行为。理解这条优先级规则,能省下大量排障时间。

三、12-Factor 对"环境分组"的警告

12-Factor 明确反对把配置打包成命名环境组(如 developmentstagingproduction 三段式)。因为随着部署增多,会出现 staging-2joes-staging 这样的组合爆炸,配置管理变得脆弱。正确做法是:每个环境变量都是独立、正交的粒度控制,按部署独立管理,随应用自然扩展。

四、机密(Secrets)管理

配置里最敏感的是密钥与凭据。不同场景有不同做法:

Docker Compose 生产部署用顶层 secrets 把文件只读挂载进容器,避免写进镜像:

services:
  web:
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

Docker Compose 生产部署

CI/CD 流水线把密钥存在平台的安全存储中(如 GitHub Actions Secrets),而不是写死在 workflow 文件:

env:
  DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
  HOST: ${{ secrets.HOST }}

更进一步的方案是用 OIDC 短时令牌替代长期密钥,参考Docker 支持 GitHub Actions OIDC

本地开发.env + .env.example 已足够;高合规场景可引入 Vault 等密钥管理服务,并把密钥轮换纳入流程。

五、与部署流水线衔接

配置管理是CI/CD 流水线的关键一环:构建产物应"环境无关",运行时才注入配置。这样同一份镜像可以推到 staging 与 production 而无需重建。相关实践还可见GitHub Actions CI/CD 配置GitLab CI/CD 最佳实践

一个真实的迁移案例

假设一个团队同时维护 Node.js API 与 Laravel 后台。早期他们把数据库口令写死在各自的 config/database.jsconfig/database.php 里,上线两个月后源码仓库被误设为公开,凭据全部暴露,只能连夜换库并重发所有密钥。重构后的做法是:仓库里只保留 .env.example,键名与占位值齐全;本地开发用 .env;CI 里通过 GitHub Actions Secrets 注入 DATABASE_URLAPP_KEY;生产环境由部署平台以 Secret 形式注入。改造完成后,同一份镜像从 staging 推到 production 不再需要重建,回滚只需切换镜像标签,凭据的暴露面从"整份代码库"收窄到"单一平台权限"。

六、常见误区

  • 把密钥写进配置文件再提交:即使后来删掉,历史记录里仍有;
  • 环境变量全塞在一个文件:违背"独立粒度控制"原则;
  • 日志打印环境变量:凭据会随日志泄露,务必脱敏;
  • 容器镜像内置密钥:镜像会被共享、拉取,密钥等于公开。

上线前的检查清单

把下面几条过一遍,多数配置事故都能提前拦住:.gitignore 是否覆盖 .env*.pemconfig/*.local.* 等敏感文件;仓库里是否只有 .env.example 且不含任何真实值;生产凭据是否来自环境变量或密钥管理而非代码常量;日志框架是否对 passwordtokenAuthorization 等字段脱敏;密钥轮换是否写进排期,而不是"想起来才换"。这些检查不需要额外工具,几分钟就能完成,回报却是一整套凭据不再裸奔。

16IDC 观察

配置管理看似琐碎,却是"环境一致性"的基础。把配置与代码分离、用环境变量注入、密钥走专门通道,这三件事做好后,一套代码才能在开发、测试、生产之间稳定迁移,这也是容器化和 CI/CD 能成立的前提。对独立站和中小团队,从 .env + 平台 Secrets 起步完全够用,不必一开始就上重型密钥管理基础设施。

参考:https://12factor.net/confighttps://github.com/motdotla/dotenvhttps://docs.docker.com/compose/how-tos/use-secrets/

原文来源:https://12factor.net/config