本文 AI 產出,尚未審核
ExpressJS (TypeScript) – 部署與環境分離
環境變數管理最佳實務
簡介
在開發 ExpressJS 應用時,常會遇到「開發、測試、正式」等多個執行環境。每個環境的資料庫連線、API 金鑰、日誌等設定都不盡相同,若直接寫死在程式碼裡,將導致:
- 部署困難:每次切換環境都需要手動改檔,易出錯。
- 安全風險:機密資訊(例如 AWS Secret、JWT 金鑰)若被提交到 Git,可能洩漏給不該看到的人。
因此,環境變數 成為「環境分離」的核心工具。本文將說明在 Express + TypeScript 專案中,如何以安全、可維護的方式管理環境變數,並提供實務範例與常見陷阱的解決方案。
核心概念
1️⃣ 為什麼使用 .env 檔案?
.env 檔案是一個純文字文件,裡面以 KEY=VALUE 的形式儲存設定。配合 dotenv 套件,我們可以在程式啟動時自動把這些鍵值載入 process.env,讓程式碼只需要讀取 process.env 即可。
⚠️ 注意:
.env檔案絕不能加入版本控制,必須在.gitignore中排除。
2️⃣ 型別安全 – dotenv-flow + zod/joi
TypeScript 本身無法保證 process.env 中的變數一定存在或型別正確。常見做法是:
- 使用 dotenv-flow 支援多環境(
.env.development,.env.production…) - 用 zod 或 joi 建立驗證 schema,於程式啟動時一次檢查。
3️⃣ 統一的設定入口
將所有環境變數集中在 config.ts(或 config/index.ts)中,並以 只讀 物件輸出,讓其他模組只能透過 config 取得設定,避免直接讀取 process.env 造成散彈式的依賴。
程式碼範例
1️⃣ 安裝必備套件
npm i dotenv-flow zod
npm i -D @types/node
2️⃣ 建立多環境的 .env 檔案
# .env.development
PORT=3000
DB_HOST=localhost
DB_USER=dev_user
DB_PASS=dev_pass
JWT_SECRET=dev_secret
# .env.production
PORT=8080
DB_HOST=prod-db.example.com
DB_USER=prod_user
DB_PASS=********
JWT_SECRET=prod_secret
3️⃣ config.ts – 型別安全的設定檔
// src/config.ts
import { config as loadEnv } from 'dotenv-flow';
import { z } from 'zod';
// 先載入對應環境的 .env
loadEnv();
// 定義環境變數的驗證 schema
const envSchema = z.object({
PORT: z.string().regex(/^\d+$/).transform(Number),
DB_HOST: z.string(),
DB_USER: z.string(),
DB_PASS: z.string(),
JWT_SECRET: z.string(),
});
// 解析並驗證 process.env
const _env = envSchema.safeParse(process.env);
if (!_env.success) {
console.error('❌ 環境變數驗證失敗:', _env.error.format());
process.exit(1);
}
// 只讀的設定物件,其他檔案只需要 import 這個
export const config = Object.freeze(_env.data);
說明:
dotenv-flow會自動根據NODE_ENV載入正確的檔案。zod的transform(Number)把字串型別的PORT直接轉成number,避免在程式碼中再做一次轉型。- 若驗證失敗,程式會直接
process.exit(1),避免在錯誤的設定下上線。
4️⃣ 在 Express 入口檔使用設定
// src/server.ts
import express from 'express';
import { config } from './config';
const app = express();
app.get('/', (req, res) => {
res.send('Hello, Express with TypeScript!');
});
app.listen(config.PORT, () => {
console.log(`🚀 Server running on http://localhost:${config.PORT}`);
});
5️⃣ 使用 dotenv-flow 的自動重載(開發模式)
// src/dev-reload.ts
import { watch } from 'chokidar';
import { execSync } from 'child_process';
// 監聽 .env* 檔案變動,一旦變更自動重啟
watch(['.env*']).on('change', () => {
console.log('🔄 .env 檔案變更,重新啟動服務...');
execSync('npm run dev', { stdio: 'inherit' });
});
提示:在
package.json中加入"scripts": { "dev": "ts-node-dev --respawn src/server.ts" }讓
ts-node-dev能在檔案變更時自動重載。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 環境變數寫死在程式碼 | 例如 const DB_PASS = 'hardcoded' |
使用 config.ts 統一管理,並在 CI/CD 階段注入變數 |
未排除 .env |
.env 進入 Git,機密外洩 |
確保 .gitignore 包含 *.env* |
| 變數名稱拼寫錯誤 | process.env.DB_HOTS → undefined |
透過 schema (zod/joi) 檢查,編譯時即可捕捉 |
| 不同環境變數不一致 | 測試環境缺少 JWT_SECRET |
為每個環境建立完整的 .env.<env>,或使用 dotenv-flow 的 fallback 機制 |
| 型別不正確 | PORT 被當成字串使用算術運算 |
在 schema 中使用 transform 或自行轉型,保持型別一致 |
最佳實踐:
- 永遠使用
.env.example:提供一個範本檔,列出所有必要鍵值,讓新成員或 CI 能快速建立自己的.env。 - CI/CD 注入:在 GitHub Actions、GitLab CI、Jenkins 等平台,把機密設定放在 Secret,於部署腳本中寫入臨時
.env或直接設定環境變數。 - 最小權限原則:只在需要的服務上放入對應的變數,例如前端服務不需要 DB 密碼。
- 版本控制環境檔:將
*.env.production之類的 非機密 設定(如PORT、LOG_LEVEL)納入版本管理,保證環境一致性。
實際應用場景
A. 多租戶 SaaS 平台
- 每個租戶都有獨立的資料庫連線資訊。
- 使用 dotenv-flow 讀取
tenantA.env、tenantB.env,在中介層動態載入對應設定,避免硬編碼。
B. Docker 部署
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# 以環境變數方式傳入
ENV NODE_ENV=production
ENV PORT=8080
CMD ["node", "dist/server.js"]
- Dockerfile 中不放
.env,而是透過docker run -e或docker-compose.yml的environment:區塊注入。
C. Serverless (AWS Lambda)
- 在 Lambda 控制台設定 Environment Variables,或使用 AWS Parameter Store、Secrets Manager。
config.ts仍可使用zod驗證,確保部署時不會缺少關鍵變數。
總結
環境變數是 ExpressJS + TypeScript 專案在「部署」與「環境分離」時不可或缺的工具。透過 dotenv-flow、zod(或 joi)以及統一的 config.ts,我們可以:
- 安全:機密資訊不會出現在程式碼庫。
- 可維護:所有設定集中管理,變更時只需要更新
.env或 CI/CD Secret。 - 型別安全:在編譯階段即捕捉缺失或型別錯誤,降低執行時崩潰的風險。
- 彈性:支援本機開發、Docker、Serverless 等多種部署方式。
只要遵守 排除 .env、使用範本檔、在程式啟動時驗證 這三大原則,就能讓你的 Express 應用在任何環境中都保持一致、可靠且安全。祝開發順利,部署無慮! 🚀