跳转到内容

第 15 章 · 环境变量与 .env

一句话:环境变量是进程启动时拿到的一组“名字=值”,.env 是把这些值写在项目里的一个文本文件;端口、数据库地址、密钥这类随机器变化的配置都放在这里,不写进代码。 用它还是用别的:本机开发用 .env 加 .env.example;CI 里用平台的机密(Secrets);.NET 项目在本机用 dotnet user-secrets;分层的复杂配置放配置文件(如 appsettings.json),环境变量只放密钥和随机器变的几项。 核对版本:PowerShell 7.6、bash 5.2、Node 22.23、python-dotenv 1.2、uv 0.12、.NET SDK 10.0,2026-10

环境变量是操作系统自带的,不用装。.env 文件要由程序或工具去读,常用的几个:

  • Node 20.6 起内置 --env-file,22.21 起不再是实验功能,不用装包。
  • Python 用 python-dotenv。带 cli 的版本另有命令行 dotenv,在项目里用 uv 装(uv 见第 14 章),不用 uv 就在虚拟环境里 pip 装同名包。
  • uv 自己就能读:uv run --env-file .env …。
  • .NET 用随 SDK 安装的 dotnet user-secrets 管开发机密,不用 .env。
终端窗口
uv add "python-dotenv[cli]"
node --version
dotnet user-secrets --help
  • 进程环境(Process Environment):每个进程都有自己的一份环境变量,启动时从父进程复制过来。子进程改了自己那份,传不回父进程;父进程后来再改,也传不到已经在运行的子进程。
  • 临时与永久:在终端里 export 或 $env: 设的变量只活在这个终端和它启动的程序里,关掉就没了。永久变量分用户级和系统级:Windows 存在注册表里,Linux 写在登录时执行的文件里,只对之后新开的终端和程序生效。
  • .env 文件:只是一个约定俗成的文本文件,每行 名字=值。操作系统不读它,Shell 也不读它,要程序里的库(python-dotenv)或启动参数(node --env-file)去读,读进来后和普通环境变量没有区别。
  • 机密不进仓库:.env 里有口令和密钥,加进 .gitignore;另写一份 .env.example,只有变量名和假值,提交进仓库,告诉别人要设哪些变量。
  • 谁覆盖谁,以具体工具为准:没有通用规则。node --env-file 和 uv run --env-file 遇到已有的同名变量时,保留已有的;python-dotenv 的 load_dotenv() 默认 override=False,也保留已有的,但它的命令行 dotenv run 默认 --override,用 .env 里的值;Compose 的规则见第 13 章 13.5。

下图从左到右是变量一层层复制下去的路径。Windows 上同名变量用户级覆盖系统级,PATH 例外,两段拼起来,系统的在前。.env 不在这条链上,是程序启动后自己读进来的:

系统级、用户级、终端会话、子进程依次复制环境;.env 文件由程序自己读入,遇到同名变量多数工具不覆盖

看全部变量、看一个变量、在当前终端设变量、看 PATH,见第 4 章 4.3;下表是那张表之外的。表中 FOO 是示例名字。

任务 bash PowerShell
当前终端
查一个变量设没设 printenv FOO,没设时退出码为 1 Test-Path Env:FOO
只对一条命令设 FOO=1 node app.js $env:FOO = "1"; node app.js; $env:FOO = $null
删掉一个变量 unset FOO Remove-Item Env:FOO
永久(用户级)
永久设 echo 'export FOO=1' >> ~/.bashrc [Environment]::SetEnvironmentVariable('FOO', '1', 'User')
用 setx 永久设 — setx FOO 1
看永久值 grep FOO ~/.bashrc [Environment]::GetEnvironmentVariable('FOO', 'User')
不重开终端,读进永久值 source ~/.bashrc $env:FOO = [Environment]::GetEnvironmentVariable('FOO', 'User')
删永久变量 sed -i '/^export FOO=/d' ~/.bashrc [Environment]::SetEnvironmentVariable('FOO', [NullString]::Value, 'User')
系统级,所有用户,要管理员 echo 'FOO=1' | sudo tee -a /etc/environment [Environment]::SetEnvironmentVariable('FOO', '1', 'Machine')
读 .env 运行
Node 程序 node --env-file=.env app.js 同左
用 uv 跑 Python uv run --env-file .env python app.py 同左
任意命令,用 python-dotenv dotenv run -- python app.py 同左
把 .env 读进当前终端 set -a; source .env; set +a 无内置命令,改用上面三行带着 .env 启动程序
看 .NET 项目的开发机密 dotnet user-secrets list 同左

几点补充:

  • PowerShell 没有“只对一条命令设”的写法,只能先设、运行完再置 $null 删掉(原来有值会被一起删掉)。
  • 删永久变量要传 [NullString]::Value。传 $null 或 '' 时,PowerShell 7.6 实测只把值清空,变量名还留在注册表里。
  • setx 和 SetEnvironmentVariable 都只写注册表,当前终端里 $env:FOO 仍是旧值。
  • .env 文件不存在时,node --env-file 报错退出;文件可有可无(如 CI 里)时改用 --env-file-if-exists=.env。
  • Docker 容器从文件读变量用 docker run --env-file,见第 12 章。
  1. 新建 .env,写进本机的端口、数据库地址和密钥(格式见 15.5):code .env。
  2. 让 Git 忽略它:echo ".env" >> .gitignore。
  3. 确认已被忽略,输出 .gitignore:1:.env 这样的一行就对了:git check-ignore -v .env。
  4. 新建 .env.example,变量名和 .env 一样,口令和密钥换成假值或留空:code .env.example。
  5. Node 程序启动时读入,代码里用 process.env.PORT 取:node --env-file=.env app.js。
  6. Python 程序加 python-dotenv,在入口文件最前面写 from dotenv import load_dotenv 和 load_dotenv(),再用 os.environ["PORT"] 取:uv add python-dotenv。
  7. 提交忽略规则和示例文件:git add .gitignore .env.example,再 git commit -m "加 .env.example"。
  8. 同事克隆后复制一份,填上自己的值:cp .env.example .env。

CI 里没有 .env,值存在仓库设置里,运行时作为环境变量交给程序:

  1. 仓库 Settings → Secrets and variables → Actions → 新建存储库机密,名字和 .env 里的变量名相同,详见第 10 章 10.5;用命令行:gh secret set DB_PASSWORD。
  2. 工作流里在要用它的步骤上写:env: { DB_PASSWORD: '${{ secrets.DB_PASSWORD }}' }。
  3. Node 的启动命令改成文件可有可无,本机读 .env、CI 里直接用环境变量:node --env-file-if-exists=.env app.js。
  4. Python 的 load_dotenv() 找不到 .env 时什么也不做、不报错,代码不用改:uv run python app.py。
  5. 推送后看运行日志,机密在日志里显示为 ***:gh run watch。

机密存在用户目录下的 secrets.json 里,不在项目目录中,不会被误提交。在 *.csproj 所在目录运行:

  1. 给项目开启,*.csproj 里会多一行 UserSecretsId:dotnet user-secrets init。
  2. 存一个机密,冒号表示层级:dotnet user-secrets set "Db:Password" "dev-only-123"。
  3. 核对:dotnet user-secrets list。
  4. ASP.NET Core 项目在 Development 环境自动读取,跳到第 7 步;控制台项目加包:dotnet add package Microsoft.Extensions.Configuration.UserSecrets。
  5. 再加环境变量的包:dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables。
  6. 代码里 new ConfigurationBuilder().AddUserSecrets<Program>().AddEnvironmentVariables().Build(),后加的覆盖先加的,用 config["Db:Password"] 取:dotnet run。
  7. 临时用环境变量覆盖,层级的冒号写成两个下划线:$env:Db__Password = "other"; dotnet run; $env:Db__Password = $null。
  8. 不再需要时清空:dotnet user-secrets clear。
配置项 Windows Linux、WSL
系统级变量 注册表 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment;界面:开始菜单搜“编辑系统环境变量” /etc/environment,只能写 名字=值,不能用 $变量
用户级变量 注册表 HKCU\Environment;界面:开始菜单搜“编辑账户的环境变量” ~/.profile 登录时执行一次;~/.bashrc 每开一个 bash 执行一次
每次开 PowerShell 都设 $PROFILE,见第 4 章 4.5 同左,PowerShell 在 Linux 上也读 $PROFILE
.NET 开发机密 %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json
项目的 .env、.env.example 项目根目录 同左

macOS 的 zsh 把 ~/.profile、~/.bashrc 换成 ~/.zprofile、~/.zshrc。Windows 上改系统级变量要管理员权限。

最小 .env,只留在本机:

# 本机开发服务器的端口
PORT=3000
# 本机数据库地址,口令只用于开发库
DATABASE_URL=postgres://app:dev-only@localhost:5432/app
# 值里有空格就整体加双引号,等号两边不留空格
APP_NAME="My App"
# 第三方服务的开发密钥
API_KEY=sk-dev-1234

对应的 .env.example,提交进仓库:

# 复制成 .env 后按本机情况修改
PORT=3000
# 把 change-me 换成本机数据库的口令
DATABASE_URL=postgres://app:change-me@localhost:5432/app
# 不含机密的值可以照写
APP_NAME="My App"
# 向负责人要开发密钥,填在等号后面
API_KEY=
  • 改了环境变量,程序里读到的还是旧值或空值。 原因:进程启动时复制一份环境,之后的修改传不进已在运行的终端和程序。处理:重开终端,VS Code 要完全退出再开;或按 15.3 把永久值读进当前终端。
  • .env 出现在 git status 或远端仓库里。 原因:.gitignore 加晚了,或用过 git add -f;已跟踪的文件不受 .gitignore 影响。处理:先作废里面的密钥,再 git rm --cached .env 并提交;已 push 的按第 8 章 8.4 从历史里删。
  • source .env 报 world: command not found,或读到的值带着引号。 原因:bash 按 Shell 语法解析,含空格的值没加引号会被拆开;docker run --env-file 不去掉引号。处理:含空格的值加双引号、等号两边不留空格;给 Docker 的文件不加引号。
  • setx PATH "$env:PATH;C:\tools" 提示 WARNING: The data being saved is truncated to 1024 characters. 原因:setx 最多存 1024 个字符,$env:PATH 又是系统加用户的合并值。处理:别用 setx 改 PATH,按第 4 章 4.4 在“编辑账户的环境变量”里改。
  • 同一段代码在 Windows 上读到 process.env.Path,在 Linux 上读到 undefined。 原因:Windows 的变量名不分大小写,Linux 分,Path 和 PATH 是两个变量。处理:变量名一律全大写,代码里和 .env 里写法完全一致。
  • about_Environment_Variables:PowerShell 里读写环境变量的三种方式,Windows 上 Machine、User、Process 三个范围和持久化。
  • Node.js –env-file:.env 的格式、引号和注释的处理、多个文件的覆盖顺序,已有环境变量优先。
  • python-dotenv:load_dotenv 的 override 参数、dotenv run 命令行和 .env 文件语法。
  • 开发中安全存储应用机密:dotnet user-secrets 的命令、secrets.json 的位置,以及在代码里读取。
  • 十二要素应用:配置:为什么把随部署环境变化的配置放进环境变量,而不写进代码。