本文 AI 產出,尚未審核

ExpressJS (TypeScript) – 部署與環境分離:PM2 或 Docker


簡介

在開發完 ExpressJS + TypeScript 的 API 之後,最後一步往往是把程式碼安全、穩定且可擴充地上線。
無論是單機 VPS、雲端 VM,或是容器化的微服務環境,都需要一套流程管理環境分離的機制,才能確保:

  1. 服務不中斷:程式碼更新或重啟不會導致使用者看到 5xx 錯誤。
  2. 資源可控:CPU、記憶體、日誌等資源被妥善管理,避免「一個崩潰」拖垮整個系統。
  3. 環境一致:開發、測試、正式環境的設定保持一致,減少「本機跑得好,上線卻錯」的狀況。

本篇文章將以 PM2Docker 為例,說明在 ExpressJS + TypeScript 專案中如何做到「部署」與「環境分離」,並提供實作範例、常見陷阱與最佳實踐,協助你從新手成長為能在生產環境自信部署的開發者。


核心概念

1. 為什麼要區分 PM2Docker

項目 PM2 Docker
定位 Node.js 進程管理器,負責守護、重啟、零停機升級 輕量級虛擬化平台,封裝整個執行環境(OS、套件、程式碼)
適用情境 單機或少量 VM,快速上手、資源開銷低 多服務、微服務、CI/CD、跨平台部署
學習曲線 較平緩,只需了解 ecosystem.config.js 較陡,需要 Dockerfile、映像檔、容器網路等概念
部署方式 直接在宿主機執行 pm2 start,使用 pm2 reload 零停機 建立映像檔 → docker rundocker‑compose up,容器即為一個完整單位

小結:若你的專案僅需在單台伺服器上跑數個 Node 進程,PM2 已足以應付;若你想在多台機器、雲端或 CI/CD 流程中保持環境一致性,Docker 更為合適。以下分別示範兩者的實作方式。


2. PM2 部署流程

2.1 安裝與初始化

npm i -g pm2          # 全域安裝 PM2
npm i -D ts-node     # 開發時直接使用 ts-node
npm i -D @types/pm2  # TypeScript 型別支援(可選)

2.2 建立 ecosystem.config.js

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "express-ts-api",          // 應用名稱
      script: "./src/index.ts",        // 入口檔(使用 ts-node)
      interpreter: "ts-node",          // 指定 TypeScript 執行器
      instances: "max",                // 啟動與 CPU 核心數相同的實例
      exec_mode: "cluster",            // 叢集模式,支援零停機 reload
      env: {
        NODE_ENV: "development",
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: "production",
        PORT: 8080,
      },
      watch: false,                    // 生產環境建議關閉檔案監控
      max_memory_restart: "300M",      // 記憶體超過 300MB 自動重啟
      log_date_format: "YYYY-MM-DD HH:mm Z",
    },
  ],
};

說明

  • interpreter: "ts-node" 讓 PM2 直接執行 TypeScript 檔案,省去事前編譯。
  • env / env_production 讓我們在同一份設定檔中管理不同環境的變數。
  • instances: "max" + exec_mode: "cluster" 能在多核心 CPU 上自動分散負載。

2.3 啟動與管理

# 開發環境
pm2 start ecosystem.config.js --env development

# 正式環境
pm2 start ecosystem.config.js --env production

# 零停機升級(先拉新程式碼,再 reload)
git pull origin main
pm2 reload ecosystem.config.js --env production

2.4 日誌與監控

pm2 logs               # 即時查看所有應用的 stdout / stderr
pm2 monit              # 內建簡易監控面板(CPU、記憶體、重啟次數)
pm2 save               # 將當前進程列表寫入 dump 文件,系統重啟時自動恢復
pm2 startup            # 產生系統服務腳本,讓 PM2 隨系統開機

3. Docker 部署流程

3.1 多階段建置(Multi‑Stage Build)

# Dockerfile
# ---------- Stage 1: Build ----------
FROM node:20-alpine AS builder

# 建立工作目錄
WORKDIR /app

# 複製 package.json 與 lock 檔,安裝相依
COPY package*.json ./
RUN npm ci

# 複製 TypeScript 原始碼,編譯成 JavaScript
COPY tsconfig.json .
COPY src ./src
RUN npm run build   # 假設 package.json 中有 "build": "tsc"

# ---------- Stage 2: Runtime ----------
FROM node:20-alpine AS runtime

WORKDIR /app

# 只拷貝編譯好的檔案與 production 相依
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production

# 設定環境變數(可在 docker-compose 中覆寫)
ENV NODE_ENV=production
ENV PORT=8080

EXPOSE 8080

# 使用 pm2 來管理容器內的 Node 進程
RUN npm i -g pm2
CMD ["pm2-runtime", "dist/index.js"]

重點

  • 多階段建置讓最終映像檔只保留執行時需要的檔案,顯著減少映像大小(約 30‑40 MB)。
  • pm2-runtime 為 PM2 的容器版入口,可自動偵測程式崩潰並重新啟動,並支援 Graceful reload

3.2 Docker Compose 範例

# docker-compose.yml
version: "3.9"

services:
  api:
    build: .
    container_name: express_ts_api
    restart: always                 # 容器崩潰自動重啟
    environment:
      - NODE_ENV=production
      - PORT=8080
      - DB_HOST=db
      - DB_USER=root
      - DB_PASS=example
    ports:
      - "8080:8080"
    depends_on:
      - db
    networks:
      - backend

  db:
    image: mysql:8.0
    container_name: mysql_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - backend

networks:
  backend:

volumes:
  db_data:

說明

  • restart: always 確保容器在宿主機重啟或意外崩潰時自動復原。
  • depends_onapidb 啟動後再執行。
  • 透過 environment環境變數機密資訊(如 DB 密碼)注入容器內,與本機 .env 完全分離。

3.3 在程式碼中讀取環境變數

// src/config.ts
import dotenv from "dotenv";
dotenv.config(); // 讀取根目錄的 .env(開發環境)

export const CONFIG = {
  port: Number(process.env.PORT) || 3000,
  db: {
    host: process.env.DB_HOST || "localhost",
    user: process.env.DB_USER || "root",
    password: process.env.DB_PASS || "",
    database: process.env.DB_NAME || "myapp",
  },
};

技巧:在 Docker 中不需要 .env 檔,docker‑compose.ymlenvironment 會直接覆寫 process.env;在本機開發時,只要放一個 .env 檔即可。


常見陷阱與最佳實踐

陷阱 說明 解法 / 最佳實踐
忘記編譯 TypeScript 直接把 src/*.ts 複製到容器,導致 node 無法執行 使用 multi‑stage Docker 或在 PM2 設定 interpreter: "ts-node"(僅開發)
環境變數未同步 process.env.PORT 在本機與容器中不同,導致無法啟動 把所有變數寫在 ecosystem.config.jsdocker‑compose.yml,並在程式碼中提供 預設值
日誌佔滿磁碟 PM2 或 Docker 產生大量 stdout/stderr,未設定輪替 PM2 使用 log_date_formatmax_size,Docker 可掛載外部卷並使用 logrotate
容器內部執行 npm install 每次 docker run 都重新安裝套件,建置時間變長 Dockerfile 中只在 build 階段安裝,runtime 階段使用 --only=production
零停機升級失敗 直接 docker stopdocker start 會短暫斷線 使用 PM2 zero‑downtime reload(PM2)或 Rolling Update(K8s)
映像檔太大 基礎映像使用完整的 node:20,導致映像 > 500 MB 改用 node:20-alpine,搭配 multi‑stage 減少層級
忘記 .dockerignore node_modulesdist 等被一起打包,映像變龐大 建立 .dockerignore,排除 node_modules, dist, .git, *.log

其他最佳實踐

  1. 分離設定檔:把 pm2docker-compose.env 等檔案分別放在 deploy/ 目錄,讓程式碼倉庫保持乾淨。
  2. 健康檢查:在 docker-compose.yml 加入 healthcheck,確保容器真正可接受請求才標記為 healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    
  3. 版本化映像:使用 image: myorg/express-ts-api:1.2.3,配合 CI/CD 自動 tag,避免「latest」混亂。
  4. 資源限制:在 Docker 中設定 mem_limitcpu_shares,防止單一服務佔用過多資源。
  5. 備份與回滾:PM2 pm2 save + pm2 resurrect,或 Docker docker-compose down && docker-compose up -d 搭配舊版映像回滾。

實際應用場景

場景 選擇 為什麼
小型 SaaS 初期 PM2 + Nginx 反向代理 只需要一台 VPS,PM2 提供自動重啟與零停機升級,成本最低。
多服務微服務 Docker + Docker‑Compose 每個服務(API、Worker、DB)都有自己的容器,環境一致且易於水平擴展。
CI/CD 交付 Docker + GitHub Actions Build → Tag → Push → Deploy,整個流程自動化,且每次部署都是全新映像。
高可用叢集 Docker + Kubernetes + PM2 (在容器內) Kubernetes 處理容器的自動調度、滾動升級,PM2 仍負責 Node 內部的叢集模式,兩層冗餘。
臨時測試環境 Docker Compose + .env.dev 開發人員只要 docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d 即可得到與正式環境相同的依賴與設定。

案例
某新創公司在第一階段使用 PM2 部署於 DigitalOcean 的單一 Droplet,搭配 Nginx 做 SSL termination。隨著使用者成長,改以 Docker Compose 部署至兩台 VM,將 API、Redis、PostgreSQL 分別容器化,並使用 Traefik 作為動態路由。最後,為了支援藍綠部署,他們在 Kubernetes 中保留 PM2 以維持 Node 叢集的零停機 reload,兩者相輔相成。


總結

  • PM2 為 Node.js 生態系最常見的進程管理工具,適合單機或小規模環境;透過 ecosystem.config.js 能輕鬆做到 環境分離零停機升級資源限制
  • Docker 則提供完整的執行環境封裝,配合 multi‑stage Dockerfiledocker-compose.yml,可在開發、測試、正式環境間保持 一致性,同時支援 水平擴展CI/CD
  • ExpressJS + TypeScript 專案中,兩者並非互斥,而是可以 相輔相成:容器內使用 pm2-runtime,即享有 PM2 的叢集與 graceful reload,又保有 Docker 的環境隔離與可移植性。

最後的建議:先在本機使用 PM2 完成開發與測試,確認一切穩定後,再以 Docker 打包映像,將部署流程自動化。如此一來,你的服務不僅能在任何雲端平台快速上線,也能在未來的擴容或微服務化路徑上,無縫銜接。祝開發順利,部署無慮!