第 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
15.1 安装与版本
Section titled “15.1 安装与版本”环境变量是操作系统自带的,不用装。.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 --versiondotnet user-secrets --help15.2 五分钟概念
Section titled “15.2 五分钟概念”- 进程环境(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 不在这条链上,是程序启动后自己读进来的:
15.3 常用命令
Section titled “15.3 常用命令”看全部变量、看一个变量、在当前终端设变量、看 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 章。
15.4 典型工作流
Section titled “15.4 典型工作流”给项目加 .env 与 .env.example
Section titled “给项目加 .env 与 .env.example”- 新建
.env,写进本机的端口、数据库地址和密钥(格式见 15.5):code .env。 - 让 Git 忽略它:
echo ".env" >> .gitignore。 - 确认已被忽略,输出
.gitignore:1:.env这样的一行就对了:git check-ignore -v .env。 - 新建
.env.example,变量名和.env一样,口令和密钥换成假值或留空:code .env.example。 - Node 程序启动时读入,代码里用
process.env.PORT取:node --env-file=.env app.js。 - Python 程序加 python-dotenv,在入口文件最前面写
from dotenv import load_dotenv和load_dotenv(),再用os.environ["PORT"]取:uv add python-dotenv。 - 提交忽略规则和示例文件:
git add .gitignore .env.example,再git commit -m "加 .env.example"。 - 同事克隆后复制一份,填上自己的值:
cp .env.example .env。
在 CI 里用机密代替 .env
Section titled “在 CI 里用机密代替 .env”CI 里没有 .env,值存在仓库设置里,运行时作为环境变量交给程序:
- 仓库 Settings → Secrets and variables → Actions → 新建存储库机密,名字和
.env里的变量名相同,详见第 10 章 10.5;用命令行:gh secret set DB_PASSWORD。 - 工作流里在要用它的步骤上写:
env: { DB_PASSWORD: '${{ secrets.DB_PASSWORD }}' }。 - Node 的启动命令改成文件可有可无,本机读
.env、CI 里直接用环境变量:node --env-file-if-exists=.env app.js。 - Python 的
load_dotenv()找不到.env时什么也不做、不报错,代码不用改:uv run python app.py。 - 推送后看运行日志,机密在日志里显示为
***:gh run watch。
本机开发用 dotnet user-secrets
Section titled “本机开发用 dotnet user-secrets”机密存在用户目录下的 secrets.json 里,不在项目目录中,不会被误提交。在 *.csproj 所在目录运行:
- 给项目开启,
*.csproj里会多一行UserSecretsId:dotnet user-secrets init。 - 存一个机密,冒号表示层级:
dotnet user-secrets set "Db:Password" "dev-only-123"。 - 核对:
dotnet user-secrets list。 - ASP.NET Core 项目在 Development 环境自动读取,跳到第 7 步;控制台项目加包:
dotnet add package Microsoft.Extensions.Configuration.UserSecrets。 - 再加环境变量的包:
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables。 - 代码里
new ConfigurationBuilder().AddUserSecrets<Program>().AddEnvironmentVariables().Build(),后加的覆盖先加的,用config["Db:Password"]取:dotnet run。 - 临时用环境变量覆盖,层级的冒号写成两个下划线:
$env:Db__Password = "other"; dotnet run; $env:Db__Password = $null。 - 不再需要时清空:
dotnet user-secrets clear。
15.5 配置要点
Section titled “15.5 配置要点”| 配置项 | 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=15.6 注意事项
Section titled “15.6 注意事项”- 改了环境变量,程序里读到的还是旧值或空值。 原因:进程启动时复制一份环境,之后的修改传不进已在运行的终端和程序。处理:重开终端,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里写法完全一致。
15.7 深入阅读
Section titled “15.7 深入阅读”- 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 的位置,以及在代码里读取。
- 十二要素应用:配置:为什么把随部署环境变化的配置放进环境变量,而不写进代码。