Node.js 环境配置与 12-Factor App / Environment Configuration and 12-Factor App
📅 创建时间:2026-07-28 🏷️ 标签:#Nodejs #Configuration #12Factor #dotenv #Zod #Security 📚 前置知识:[[02-async-patterns-and-error-handling]]
📋 本章目标
- 理解环境变量的分层加载机制(.env → CI → 平台运行时)
- 掌握类型安全的配置验证模式(Zod + dotenv)
- 理解 12-Factor App 的配置原则及其在现代实践中的演变
- 能够在多环境(dev/staging/prod)中安全管理密钥
- 理解 Feature Flags 与动态配置的区别
- 掌握不同部署平台(Docker/K8s/Vercel)的配置注入策略
第1部分:为什么配置管理是一个"真问题"
1.1 硬编码的代价
┌─────────────────────────────────────────────────────────────┐
│ 硬编码 vs 环境变量 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 硬编码方式(❌): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ const dbUrl = "mysql://prod:3306/myapp" │ │
│ │ const apiKey = "sk-live-abc123" │ │
│ │ │ │
│ │ 问题: │ │
│ │ • 密钥泄露到 Git 历史 │ │
│ │ • 开发/测试/生产环境需要修改源码 │ │
│ │ • 无法在不重新构建的情况下切换配置 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 环境变量方式(✅): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ const dbUrl = process.env.DATABASE_URL │ │
│ │ const apiKey = process.env.API_KEY │ │
│ │ │ │
│ │ 优势: │ │
│ │ • 敏感信息不入库 │ │
│ │ • 同一份构建产物部署到不同环境 │ │
│ │ • 运维可通过平台注入配置 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.2 环境变量的加载层次
┌─────────────────────────────────────────────────────────────┐
│ 环境变量加载优先级(从低到高) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第1层:默认值(代码中硬编码的 fallback) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ const port = process.env.PORT ?? 3000 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↑ 被覆盖 │
│ 第2层:.env 文件(本地开发,不提交到 Git) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PORT=3001 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↑ 被覆盖 │
│ 第3层:.env.local / .env.development(特定于环境) │
│ ↑ 被覆盖 │
│ 第4层:CI/CD 环境变量(GitHub Actions Secrets 等) │
│ ↑ 被覆盖 │
│ 第5层:运行时平台注入(K8s Secrets、Vercel Env、AWS SM) │
│ │
│ 原则:离代码越远,优先级越高 │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:dotenv 与现代替代方案
2.1 dotenv 基础
┌─────────────────────────────────────────────────────────────┐
│ dotenv 工作流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 项目结构: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ my-app/ │ │
│ │ ├── .env ← 实际值(gitignore) │ │
│ │ ├── .env.example ← 模板(提交到 Git) │ │
│ │ ├── .env.development ← 开发环境覆盖 │ │
│ │ ├── .env.production ← 生产环境覆盖 │ │
│ │ └── src/ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ .env 格式: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DATABASE_URL=postgresql://user:pass@localhost/db │ │
│ │ API_KEY=sk_test_abc123 │ │
│ │ NODE_ENV=development │ │
│ │ PORT=3000 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ .env.example(提交到 Git 的模板): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DATABASE_URL=postgresql://user:password@localhost/db│ │
│ │ API_KEY=your_api_key_here │ │
│ │ NODE_ENV=development │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘// 方式一:显式调用
import dotenv from 'dotenv'
dotenv.config() // 加载 .env 到 process.env
console.log(process.env.DATABASE_URL)
// 方式二:预加载(无需修改代码)
// node -r dotenv/config src/index.ts
// 或使用 tsx: tsx --env-file=.env src/index.ts
import 'dotenv/config'
// 方式三:加载特定文件
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })2.2 dotenv 的局限性
dotenv 只做一件事:把 .env 文件读入 process.env。它不验证、不提供类型、不处理多文件优先级。对于生产项目,需要在 dotenv 之上构建一层"配置对象"。
第3部分:类型安全的配置验证(核心模式)
3.1 Zod 配置验证模式
这是现代 Node.js 项目的最佳实践:
import { z } from 'zod'
import 'dotenv/config'
// 定义配置 schema —— 这是"配置的真相来源"
const ConfigSchema = z.object({
// 服务配置
PORT: z.coerce.number().int().positive().default(3000),
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
// 数据库
DATABASE_URL: z.string().url(),
DATABASE_POOL_MIN: z.coerce.number().int().min(1).default(2),
DATABASE_POOL_MAX: z.coerce.number().int().min(1).default(10),
// Redis
REDIS_URL: z.string().url().optional(),
// 第三方 API
OPENAI_API_KEY: z.string().startsWith('sk-'),
// 日志
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
// 功能开关
ENABLE_NEW_CHECKOUT: z.enum(['true', 'false']).default('false'),
})
// 在应用启动时验证(Fail Fast:配置错误立即退出)
export type Config = z.infer<typeof ConfigSchema>
export function loadConfig(): Config {
const result = ConfigSchema.safeParse(process.env)
if (!result.success) {
console.error('❌ Invalid environment variables:')
console.error(result.error.flatten().fieldErrors)
process.exit(1)
}
return result.data
}
// 全局单例
export const config = loadConfig()┌─────────────────────────────────────────────────────────────┐
│ 配置加载的 Fail Fast 模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 应用启动 │
│ ↓ │
│ ┌─────────────────┐ │
│ │ 加载 .env 文件 │ │
│ └────────┬────────┘ │
│ ↓ │
│ ┌─────────────────────┐ ❌ 验证失败 │
│ │ Zod Schema 验证 │ ──────────→ process.exit(1) │
│ │ process.env 所有值 │ 打印缺失/格式错误的字段 │
│ └────────┬────────────┘ │
│ │ ✅ 验证通过 │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 导出类型安全的 │ │
│ │ config 对象 │ │
│ └────────┬────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 应用正常运行 │ │
│ │ 所有配置都有明确类型 │ │
│ └─────────────────────┘ │
│ │
│ 好处:配置错误在启动时暴露,而非运行到某处才报错 │
│ │
└─────────────────────────────────────────────────────────────┘3.2 在 Express 中使用类型安全配置
// config.ts
export const config = loadConfig()
// app.ts
import { config } from './config'
const app = express()
app.listen(config.PORT, () => {
logger.info(`Server running on port ${config.PORT}`)
logger.info(`Environment: ${config.NODE_ENV}`)
})
// config.PORT 是 number 类型,config.NODE_ENV 是字面量联合类型
// 编辑器有完整的自动补全和类型检查第4部分:12-Factor App 配置原则
4.1 核心原则
┌─────────────────────────────────────────────────────────────┐
│ 12-Factor App 配置原则 │
├─────────────────────────────────────────────────────────────┤
│ │
│ III. 配置(Config)—— 在环境中存储配置 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "应用的配置是在不同部署环境(staging/prod/dev)之间 │ │
│ │ 唯一不同的东西。" │ │
│ │ │ │
│ │ 核心主张: │ │
│ │ 1. 配置与代码严格分离 │ │
│ │ 2. 配置存储在环境变量中(不分组为配置文件) │ │
│ │ 3. 同一份构建产物可在任意环境运行 │ │
│ │ │ │
│ │ 不推荐:把环境分组放入 config/development.json 等 │ │
│ │ → 分组文件容易泄露、难以审计、与构建产物绑定 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 在现代实践中的演变: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 12-Factor 反对按环境分组的配置文件 │ │
│ │ 但现实是:大项目往往有数十个配置项 │ │
│ │ → 折中方案:.env 文件只在本地开发使用 │ │
│ │ → 生产环境通过平台注入(K8s Secrets / Vercel Env) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘4.2 多环境策略
// 不同环境的配置文件(仅本地开发用)
// .env.development
NODE_ENV=development
DATABASE_URL=postgresql://localhost:5432/dev_db
LOG_LEVEL=debug
// .env.test
NODE_ENV=test
DATABASE_URL=postgresql://localhost:5432/test_db
LOG_LEVEL=silent
// 生产环境:不通过文件,通过 K8s ConfigMap/Secret 注入
// kubectl create secret generic app-config \
// --from-literal=DATABASE_URL=postgresql://prod-host/db第5部分:密钥管理安全实践
5.1 密钥安全的纵深防御
┌─────────────────────────────────────────────────────────────┐
│ 密钥安全的纵深防御 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第1层:.gitignore │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ .env 绝对不能提交到 Git │ │
│ │ → 提供 .env.example 作为模板 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第2层:预提交检查 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 使用 git-secrets / gitleaks 扫描敏感信息 │ │
│ │ → 防止意外提交 API Key / 密码 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第3层:Git 历史扫描 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 如果密钥曾经被提交过,即使后来删除了 │ │
│ │ Git 历史中仍然存在 │ │
│ │ → 发现泄露后立即轮换密钥,不要只删除文件 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第4层:运行时隔离 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 生产环境密钥不通过 .env 文件管理 │ │
│ │ → K8s Secrets / Vault / AWS Secrets Manager │ │
│ │ → 密钥在运行时注入,不落盘 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第5层:最小权限 + 定期轮换 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 每个服务只拥有其需要的密钥 │ │
│ │ 定期轮换所有密钥 │ │
│ │ 使用临时凭证(如 AWS IAM Role)而非长期密钥 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘5.2 Vault / 云平台密钥管理简介
┌─────────────────────────────────────────────────────────────┐
│ 密钥管理方案对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ │ 方案 │ 适用场景 │ 复杂度 │
│ ├────────────────────┼────────────────────┼──────────────┤
│ │ .env + 平台注入 │ 中小项目 │ 低 │
│ │ (Vercel/Railway) │ 全栈应用 │ │
│ ├────────────────────┼────────────────────┼──────────────┤
│ │ K8s Secrets │ K8s 集群部署 │ 中 │
│ │ + External Secrets │ 需要声明式管理 │ │
│ │ Operator │ │ │
│ ├────────────────────┼────────────────────┼──────────────┤
│ │ HashiCorp Vault │ 大型组织 │ 高 │
│ │ / Infisical │ 多服务/多团队 │ │
│ │ │ 审计/合规要求 │ │
│ ├────────────────────┼────────────────────┼──────────────┤
│ │ AWS Secrets Manager │ AWS 生态内 │ 中 │
│ │ / GCP Secret Manager│ 云原生项目 │ │
│ │
└─────────────────────────────────────────────────────────────┘第6部分:Feature Flags 与动态配置
6.1 环境变量 vs Feature Flags
┌─────────────────────────────────────────────────────────────┐
│ 配置 vs 功能开关 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 环境变量(Environment Variables): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 在应用启动时加载 │ │
│ │ • 修改需要重启应用 │ │
│ │ • 适合:数据库地址、API 密钥、运行模式 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 功能开关(Feature Flags): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 可在运行时动态切换 │ │
│ │ • 不需要重启或重新部署 │ │
│ │ • 适合:新功能灰度发布、A/B 测试、紧急功能关闭 │ │
│ │ • 方案:LaunchDarkly / Unleash / 自建 Redis-based │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 简单场景可用环境变量替代: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ENABLE_NEW_CHECKOUT=true │ │
│ │ → 适合:少量标志、不频繁变更 │ │
│ │ → 不适合:数十个标志、需要按用户分组、需要实时切换 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第7部分:框架特定配置
7.1 Next.js 环境变量
Next.js 有独特的 NEXT_PUBLIC_ 前缀约定:
┌─────────────────────────────────────────────────────────────┐
│ Next.js 环境变量 │
├─────────────────────────────────────────────────────────────┤
│ │
│ NEXT_PUBLIC_* → 浏览器端可见 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ NEXT_PUBLIC_API_URL=https://api.example.com │ │
│ │ → 在构建时内联到客户端 bundle 中 │ │
│ │ → 不要在此放密钥!任何人可以在浏览器中看到 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 普通变量 → 仅在服务端可见 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DATABASE_URL=postgresql://... │ │
│ │ → 只在 Server Components / Route Handlers / API 中 │ │
│ │ → 客户端代码中访问会得到 undefined │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
总结1:配置管理的核心
- 敏感信息不入库(
.env→.gitignore) - 配置在启动时验证(Zod Schema + Fail Fast)
- 类型安全(不直接读
process.env,通过类型化 config 对象访问) - 分层加载(默认值 → .env → CI → 平台注入)
总结2:12-Factor 的现代解读
12-Factor 反对按环境分组的配置文件,主张环境变量。现代实践中折中:本地开发用 .env 文件,生产环境通过平台注入(K8s Secrets / Vercel Env),避免配置文件进入构建产物。
总结3:最关键的一个习惯
启动应用时第一件事:加载并验证所有配置。配置错误立即 crash——不要等到请求处理到一半才发现数据库连不上。Zod Schema + process.exit(1) 是最好的配置治理。
章节测试
测试1:为什么 .env 文件不应该提交到 Git?
A. 因为文件太大 B. 因为包含敏感信息(密钥/密码),且不同环境值不同 C. 因为它会导致 Git 合并冲突 D. 因为 Node.js 不识别它
测试2:以下哪种做法能提供类型安全的配置访问?
A. 直接使用 process.env.DATABASE_URL B. 使用 Zod Schema 验证后导出类型化 config 对象 C. 使用 config npm 包 D. 把配置写在 JSON 文件中手动导入
测试3:Next.js 中的 NEXT_PUBLIC_ 前缀有什么特殊含义?
测试4:12-Factor App 为什么反对按环境分组的配置文件?
测试5:Feature Flag 和环境变量在配置管理中的角色差异是什么?
参考答案
测试1答案
答案:B。.env 文件包含数据库密码、API 密钥等敏感信息;且不同开发者和不同环境的配置值不同。应提交 .env.example(模板文件)供团队成员参考。
测试2答案
答案:B。Zod Schema 在启动时验证 process.env 的所有值,保证类型(如 z.coerce.number() 将字符串转为数字)和约束(如 z.string().url()),验证通过后导出的 config 对象拥有完整类型。
测试3答案
NEXT_PUBLIC_ 前缀的变量会在构建时被内联到客户端 JavaScript bundle 中,浏览器端可以直接访问。因此绝对不能在此类变量中放入密钥、数据库连接串等敏感信息。
测试4答案
按环境分组的配置文件(如 config/development.json)与代码存放在同一个仓库中,容易被提交到版本控制系统。且配置文件随构建产物部署,修改配置需要重新构建。而环境变量在运行时注入,实现了配置与代码的严格分离。
测试5答案
环境变量:在应用启动时加载,修改需重启。适合基础设施配置(数据库地址、API 密钥)。 Feature Flags:可在运行时动态切换,无需重启。适合业务功能灰度(新功能上线、A/B 测试)、按用户/百分比逐步放量。
相关笔记
- [[02-async-patterns-and-error-handling]] — 优雅关闭与健康检查
- [[03-express-deep-dive]] — Express 中间件设计
- [[../02-react-and-nextjs/05-nextjs-app-router-and-rendering]] — Next.js 环境变量
下一步学习
- [ ] 为你当前的项目实现 Zod 配置验证,替换直接读取
process.env - [ ] 在项目中加入
.env.example文件(如果还没有) - [ ] 检查 Git 历史是否曾经提交过密钥——如有,立即轮换
- [ ] 了解你部署平台(Vercel/Railway/K8s)的密钥管理方案
学习状态:🟡 开始学习