【前端搞全栈】我的环境变量管理最佳实践

0 阅读10分钟

.env 文件满天飞,到一个命令统一管理 —— 我在 MorphixAI 开发中踩过的坑和最终方案


背景

先交代一下上下文。

我在做一个叫 MorphixAI 的项目 —— 一个 AI 驱动的个人工作台。它的核心思路是把你散落在 GitHub、Jira、Notion、邮件、日历等平台的工作数据聚合起来,让 AI 帮你理解上下文、管理任务、执行操作。

这个项目的技术栈比较多样:

子项目技术栈用途
morphicai-apiExpress + TypeScript后端 API
morphicai-webNext.js 15 + React 19Web 前端
morphicai-app-shellVite + Ionic + Capacitor跨平台 App Shell
morphicai-nativeReact Native + ExpoiOS/Android 客户端
openclaw-morphixaiNode.jsMCP Server(开源)
morphixai-codeNode.jsCLI 工具(开源)

6 个子项目,3 种前端框架,部署在 Zeabur 上。项目推进节奏比较快,如果在环境变量这种「基础设施」问题上反复踩坑,那真的太浪费时间了。

这篇文章想分享的就是:在这种多项目架构下,我是怎么一步步理顺环境变量管理的,以及最终沉淀出的方案和工具。

morphix-blog-01-architecture.png


阶段一:.env 文件管理

最早的做法和大多数项目一样 —— 每个项目根目录放一个 .env 文件,里面写满各种密钥和配置。

SUPABASE_URL=https://xxx.supabase.co
SUPABASE_KEY=eyJhbGciOi...
OPENAI_API_KEY=sk-...

.env 加到 .gitignore 里,然后写一个 .env.example 提交到 git。

morphix-blog-02-env-scattered.png 项目早期是够用的。但随着子项目越来越多,问题集中暴露了:

多项目重复配置。比如 SUPABASE_URLSUPABASE_KEY,6 个项目都要用,值完全一样。但每个项目各自维护一份 .env,改一个值得跑到 6 个目录里挨个改,漏一个就是线上问题。

密钥安全无法保障。项目里有 OpenAI API Key、Supabase Service Key 这些直接关联费用的密钥。OpenAI 的 key 是实打实按 token 计费的,泄露了就是真金白银的损失。.env 文件虽然加了 .gitignore,但它就是一个明文文件,躺在本地磁盘上。如果项目需要和其他开发者协作,你没办法做到精细的权限控制。


阶段二:引入 Infisical

意识到 .env 文件管理不住之后,我们引入了 Infisical。

Infisical 是什么

Infisical 是一个开源的密钥管理平台,你可以理解为专门给开发者设计的密钥保险箱 —— 一个地方存所有密钥,所有环境、所有项目从这里统一拉取。

核心能力:

能力说明
多环境管理dev / staging / prod 各一套,互不干扰
项目 + Folder 隔离一个项目下可以按 /ai/frontend 等路径分组
两种认证方式本地用 CLI 登录(infisical login),CI/Docker 用 Machine Identity
SDK 集成Node.js / Python / Go SDK,代码里直接拉取
CLI 工具infisical run -- npm start 一行搞定注入
权限控制按人、按角色、按环境控制访问

市面上做密钥管理的工具不少,比如 HashiCorp Vault。但 Vault 是企业级方案,部署和维护成本都高。Infisical 卡在一个很好的位置 —— 比 .env 文件规范,比 Vault 轻量,有开源版可以自部署,也有云服务直接用。

我们怎么用的

本地开发通过 Infisical CLI 拉取密钥:

infisical login          # 一次性登录
infisical run --env=dev --path=/ai -- next dev   # 拉取密钥并启动

生产部署走 GitHub Actions。在 CI 构建阶段,先通过 Infisical CLI 动态拉取密钥,生成 .env 文件,再执行 Docker 构建:

# GitHub Actions 构建流程
steps:
  - name:  Infisical 拉取密钥
    run: infisical export --env=prod --path=/ai --format=dotenv > .env
  - name: 构建 Docker 镜像
    run: docker build .

这解决了两个核心问题:密钥有了统一的来源(single source of truth),以及密钥的访问可以通过权限控制来管理。

关于协作安全,这里说一下实际情况。Infisical 的权限控制是管理层面的 —— 你可以控制谁能在 Infisical 管理界面上看到哪些密钥。但只要项目跑起来了,环境变量已经注入到 process.env 里,技术上是可以读取的。所以 Infisical 的价值不是「绝对防泄露」,而是降低密钥暴露面 —— 密钥不再以明文文件的形式存在,不需要在聊天工具里传来传去,访问权限可以集中管控和审计,需要的时候随时收回。

还有什么不够顺畅

  • .env 文件构建到 Docker 镜像中有安全隐患。CI 里先 infisical export 生成 .env,再 docker build,密钥就被烘焙进了镜像。任何能拉到这个镜像的人都能看到里面的密钥
  • Docker 镜像里装 Infisical CLI 麻烦。Alpine 镜像装 CLI 有二进制依赖问题,镜像体积也会增大
  • 本地覆盖不方便infisical run 注入远程密钥后,想把某个 URL 临时指向 localhost 调试,没有优雅的覆盖方式

阶段三:迁移到 Zeabur,催生 morphix-env

转折点是把部署从 GitHub Actions 迁移到了 Zeabur。

Zeabur 是一个国内团队做的 PaaS 部署平台,类似 Vercel / Railway。它提供了一个很方便的功能 —— 自动识别项目中的 Dockerfile,从 GitHub 仓库拉代码直接构建和部署。不需要自己写 CI 流程,推代码就自动部署。

但这也意味着,我们没有办法在 Docker 构建之前插入额外的步骤了。之前在 GitHub Actions 里「先 infisical export 拉密钥生成 .env,再 docker build」的方式,在 Zeabur 上行不通 —— 它直接构建你的 Dockerfile,没有地方执行预处理脚本。

而且回过头想,之前的方式其实也有问题:先拉取密钥生成 .env 文件,再构建到 Docker 镜像里,这本身就不安全。密钥被烘焙进了镜像,任何能拉到镜像的人都能看到。

这里需要区分两类环境变量:

  • 前端公开变量(如 SUPABASE_URLSUPABASE_ANON_KEY)—— 这些本来就会出现在浏览器端的 JS bundle 里,编译进产物没有安全问题
  • 服务端密钥(如 OPENAI_API_KEYSUPABASE_SERVICE_KEY)—— 这些绝对不能固化到镜像里,只应该在运行时使用

所以我们真正需要的是:

  1. 不依赖特定的 CI/CD 平台 —— 不管是 GitHub Actions 还是 Zeabur,都能用
  2. 密钥按需动态拉取 —— 构建时需要就在构建时拉,运行时需要就在运行时拉,但不提前生成 .env 文件、不固化到镜像里
  3. 不需要在 Docker 镜像里装 Infisical CLI —— 用轻量的 Node.js SDK 就行
  4. 本地开发能方便地覆盖 —— .env.local 优先

于是就有了 morphix-env。


morphix-env:最终方案

核心设计

morphix-env run -- next dev

这一行命令背后做了五件事:

1. 读取配置文件 mx-env.config.json
2. 从 Infisical 按需拉取密钥(自动选择 SDK 或 CLI)
3. 如果配置了 envPrefix,自动给变量加前缀
4. 加载 .env.local 覆盖(本地开发自定义)
5. 启动子命令,继承完整的 process.env

不管是 npm run dev(本地开发)、npm run build(Docker 构建阶段)、还是 npm start(生产运行),都走同一个命令。密钥在命令执行的那一刻从 Infisical 拉取,不需要提前准备任何文件。

morphix-blog-03-morphix-env-flow.png

设计决策一:变量优先级

┌──────────────────────────────────────────┐
│  .env.local                   ← 最高优先  │
│  开发者的本地覆盖,永远优先               │
├──────────────────────────────────────────┤
│  Infisical secrets             ← 中优先   │
│  远程拉取,不覆盖已有值                   │
├──────────────────────────────────────────┤
│  process.env                   ← 最低优先  │
│  Docker ENV、CI 变量、shell exports       │
└──────────────────────────────────────────┘

这意味着:

  • 远程密钥管理是底座,保证所有项目用同一套配置
  • 本地想改个 API 地址调试?改 .env.local 就行,不影响远程配置
  • Docker/CI 中已有的 process.env 作为最后兜底

设计决策二:自动识别认证方式

有 INFISICAL_CLIENT_ID 环境变量?
  → 用 SDK(Machine Identity)—— Docker / 部署平台场景
没有?
  → 本地装了 infisical CLI?
    → 用 CLI(用户登录态)—— 本地开发
  → 也没有?
    → 跳过 Infisical,只用本地文件

开发者不需要关心当前是用 SDK 还是 CLI —— 工具自动判断。本地开发跑一次 infisical login,之后 pnpm dev 就自动拉取。Docker 里设几个环境变量就行,不需要装 CLI 二进制。

设计决策三:envPrefix

Infisical 里存的是通用的变量名(如 SUPABASE_URL),但不同前端框架要求不同前缀。在配置中声明 envPrefix,拉取时自动转换:

{
  "infisical": {
    "paths": ["/frontend"],
    "envPrefix": "VITE_"
  }
}

Infisical 里只维护一份变量,不同项目按需配置前缀:

项目envPrefixSUPABASE_URL 变为
morphicai-api不配置SUPABASE_URL(原样)
morphicai-webNEXT_PUBLIC_NEXT_PUBLIC_SUPABASE_URL
morphicai-app-shellVITE_VITE_SUPABASE_URL

实际使用

配置文件

// mx-env.config.json(提交到 git,不含密钥)
{
  "infisical": {
    "paths": ["/frontend"],
    "envPrefix": "VITE_"
  },
  "envFiles": [".env.local"]
}

package.json

{
  "scripts": {
    "dev": "morphix-env run --env dev -- vite",
    "build": "morphix-env run -- vite build",
    "start": "morphix-env run -- node server/index.js"
  }
}

Dockerfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

COPY . .
RUN npm run build

# 运行阶段
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/package.json ./
COPY --from=builder /app/package-lock.json ./
RUN npm ci --omit=dev
CMD ["npm", "start"]

有一点需要说明:morphix-env 要连接 Infisical 拉取密钥,本身还是需要几个认证信息。这几个变量需要在部署平台(如 Zeabur)上配置为环境变量:

变量说明
INFISICAL_CLIENT_IDMachine Identity 的 ID
INFISICAL_CLIENT_SECRETMachine Identity 的密钥
INFISICAL_PROJECT_IDInfisical 项目 ID
DEPLOY_ENV环境标识(dev / prod)

这些是「拉取密钥的钥匙」,数量很少且固定,只需要在平台上配一次,永久生效。剩下的几十上百个业务密钥全部从 Infisical 动态拉取,不需要在部署平台上逐个配置。后续新增环境变量,只需要去 Infisical 管理平台上加一条,所有项目下次启动时自动生效,不需要改任何代码或部署配置。

注意 morphix-env 必须在 dependencies(不是 devDependencies),因为 start 脚本在运行阶段也需要它。


适用场景

什么时候你该考虑类似的方案?

  • 项目有 2 个以上环境(dev/staging/prod)
  • 项目中有关联费用的密钥(OpenAI Key、云服务 Key 等),需要管控访问
  • 在用 Docker 部署PaaS 平台(环境变量传递链路变长)
  • 多个子项目共享同一批密钥
  • 前后端项目需要不同的变量前缀

如果以上中了 3 个,值得花半天时间理一理。


总结

环境变量管理不是什么高深的技术问题,但它确实是一个「不解决就一直烦你」的工程问题。

我的经验是:

  1. 密钥必须有一个 single source of truth —— 我们选了 Infisical,你也可以选其他方案,关键是「一处修改,处处生效」
  2. 本地开发必须能覆盖 —— 远程配置是底座,但开发者需要灵活性
  3. 密钥按需拉取,而不是提前生成文件 —— 减少中间环节,降低泄露面
  4. 工具能跑在所有环境 —— 本地、CI、Docker、PaaS,一套配置搞定

morphix-env 就是按这些原则写的,目前在 MorphixAI 的 6 个子项目中都在用。核心代码 300 行左右,但确实帮我省了不少时间。

开源在 npm 上:

npm install morphix-env

GitHub: github.com/Morphicai/m…

如果你也在多项目架构下被环境变量折磨过,欢迎试试。有问题可以直接提 issue。


如果觉得有帮助,点个赞或者收藏一下。后续我会继续分享 MorphixAI 开发过程中的工程实践,包括多端 SDK 通信、AI Agent 架构设计等内容。