柴柴's TIL

Node.js 环境变量完全指南:设置方式、优先级与最佳实践

· 1 view ·

在开发 Node.js 项目时,process.env.XXX 可以在很多地方设置,令人困惑。本文从底层原理出发,系统梳理所有设置方式、优先级和最佳实践。

本质:环境变量是什么

process.env 只是操作系统进程的环境变量表,Node.js 只是读取它。设置方式多,是因为不同场景下谁能控制这张表是不同的

操作系统进程启动
    └─ 继承父进程的环境变量(shell)
        └─ 启动命令可以追加/覆盖
            └─ 代码运行时可以再修改 process.env

五种设置方式

1. 命令行直接传入(最常见)

NODE_ENV=production npm run start
# 多个变量
MRN_HARMONY=true NODE_ENV=production npm run start

2. package.json 的 scripts 里写死

{
  "scripts": {
    "start": "NODE_ENV=production node server.js",
    "start:harmony": "cross-env MRN_HARMONY=true node server.js"
  }
}

3. .env 文件(配合 dotenv)

# .env 文件
MRN_HARMONY=true
PORT=3000

代码里 require('dotenv').config(),或由框架自动加载(Next.js、Vite 等)。

4. CI/CD 平台配置

在 Jenkins、GitHub Actions 或公司内部发布平台上配置,构建/运行时自动注入。

5. Shell 全局配置

# ~/.zshrc 或 ~/.bashrc
export MRN_HARMONY=true

优先级(后覆盖前)

低 ←————————————————————————————→ 高

Shell 全局    .env 文件    package.json    命令行传入    代码里赋值
~/.zshrc      .env         scripts 里写     MY=1 npm     process.env.MY=1

注意:dotenv 默认不覆盖已存在的变量(命令行传入的优先),这是特意设计的——越靠近运行时、越具体的配置,优先级越高。


什么情况放在哪

场景 推荐方式 原因
本地开发,个人配置不同 .env.local(gitignore) 个人差异,不能提交
本地开发,团队统一默认值 .env.env.development 提交进仓库,开箱即用
生产环境密钥(AK/SK) CI/CD 平台配置 不能进代码仓库
区分不同环境(dev/test/prod) .env.production 等分环境文件 构建时由框架自动选择
临时调试、一次性测试 命令行 MY=1 npm run xxx 不污染任何配置文件
Feature Flag CI 平台 + 本地命令行 灰度控制,不同环境不同值

进阶:一个项目的典型分层

项目根目录/
├── .env                  # 基础默认值,提交 git(不含敏感信息)
├── .env.development      # 开发环境覆盖值,提交 git
├── .env.production       # 生产环境覆盖值,提交 git
├── .env.local            # 本地个人覆盖,gitignore!
└── .env.example          # 列出所有变量名但不填真实值,相当于"接口文档"

加载顺序(以 Next.js/Vite 为例,后加载的优先级高):

.env → .env.[mode] → .env.local → .env.[mode].local → 命令行

.env.example 是个好习惯——提交到 git,告诉团队有哪些变量需要配置。


构建时变量 vs 运行时变量(最容易踩坑)

运行时变量                    构建时变量
process.env.PORT             process.env.NEXT_PUBLIC_API_URL
  ↓                            ↓
服务器运行期间读取            打包时被 bundler 直接替换成字符串
可以动态改                    改了必须重新打包

前端框架对”暴露给浏览器”的变量有特殊前缀约定(因为浏览器里没有 process):

NEXT_PUBLIC_xxx    # Next.js
VITE_xxx           # Vite
REACT_APP_xxx      # CRA(已过时)

原理:打包时 bundler 把 process.env.VITE_API_URL 文本替换成实际值字符串。没加前缀的变量不会被替换,在浏览器里是 undefined


跨平台问题

# Mac/Linux 可以直接用
NODE_ENV=production node app.js

# Windows 不行!需要 cross-env
npx cross-env NODE_ENV=production node app.js

这就是为什么很多项目 package.json 里装了 cross-env


启动时校验(生产项目必备)

直接用 process.env.XXX 的问题:缺了变量运行时才报错,且报错位置很深。推荐在启动时统一校验:

// env.ts - 启动时就报错,而不是运行到某个功能才发现
import { z } from 'zod'

const schema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']),
})

export const env = schema.parse(process.env)
// 后续代码只 import env,不直接用 process.env

Docker 环境

# Dockerfile 里设默认值
ENV NODE_ENV=production

# docker run 时覆盖
# docker run -e NODE_ENV=staging my-app
# docker-compose.yml
services:
  app:
    env_file: .env.production   # 指定文件
    environment:
      - MRN_HARMONY=true        # 单独追加

安全边界(最重要的原则)

永远不要把以下内容放进代码仓库:

  • 数据库密码
  • API 密钥 / Secret
  • 私钥证书

永远不要用带 NEXT_PUBLIC_ / VITE_ 前缀的变量存敏感信息: 加了前缀 = 打进前端 bundle = 用户打开 DevTools 就能看到。


决策树

这个变量是什么?
├─ 敏感信息(密码/密钥)
│   └─ 生产:CI/KMS 注入,本地:.env.local(gitignore)
├─ 区分环境的配置(API 地址、Feature Flag)
│   ├─ 需要进浏览器?→ 加框架前缀(VITE_/NEXT_PUBLIC_)
│   └─ 仅服务端?→ .env.[environment],可以提交 git
├─ 本地个人偏好(开启某个调试功能)
│   └─ .env.local 或命令行传入
└─ 默认值 / 文档示例
    └─ .env.example(提交 git,告诉团队有哪些变量)