本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 部署與環境分離

環境變數管理最佳實務

簡介

在開發 ExpressJS 應用時,常會遇到「開發、測試、正式」等多個執行環境。每個環境的資料庫連線、API 金鑰、日誌等設定都不盡相同,若直接寫死在程式碼裡,將導致:

  1. 部署困難:每次切換環境都需要手動改檔,易出錯。
  2. 安全風險:機密資訊(例如 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…)
  • zodjoi 建立驗證 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 載入正確的檔案。
  • zodtransform(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 或自行轉型,保持型別一致

最佳實踐

  1. 永遠使用 .env.example:提供一個範本檔,列出所有必要鍵值,讓新成員或 CI 能快速建立自己的 .env
  2. CI/CD 注入:在 GitHub Actions、GitLab CI、Jenkins 等平台,把機密設定放在 Secret,於部署腳本中寫入臨時 .env 或直接設定環境變數。
  3. 最小權限原則:只在需要的服務上放入對應的變數,例如前端服務不需要 DB 密碼。
  4. 版本控制環境檔:將 *.env.production 之類的 非機密 設定(如 PORTLOG_LEVEL)納入版本管理,保證環境一致性。

實際應用場景

A. 多租戶 SaaS 平台

  • 每個租戶都有獨立的資料庫連線資訊。
  • 使用 dotenv-flow 讀取 tenantA.envtenantB.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 -edocker-compose.ymlenvironment: 區塊注入。

C. Serverless (AWS Lambda)

  • 在 Lambda 控制台設定 Environment Variables,或使用 AWS Parameter StoreSecrets Manager
  • config.ts 仍可使用 zod 驗證,確保部署時不會缺少關鍵變數。

總結

環境變數是 ExpressJS + TypeScript 專案在「部署」與「環境分離」時不可或缺的工具。透過 dotenv-flowzod(或 joi)以及統一的 config.ts,我們可以:

  • 安全:機密資訊不會出現在程式碼庫。
  • 可維護:所有設定集中管理,變更時只需要更新 .env 或 CI/CD Secret。
  • 型別安全:在編譯階段即捕捉缺失或型別錯誤,降低執行時崩潰的風險。
  • 彈性:支援本機開發、Docker、Serverless 等多種部署方式。

只要遵守 排除 .env、使用範本檔、在程式啟動時驗證 這三大原則,就能讓你的 Express 應用在任何環境中都保持一致、可靠且安全。祝開發順利,部署無慮! 🚀