ExpressJS (TypeScript) – 部署與環境分離:PM2 或 Docker
簡介
在開發完 ExpressJS + TypeScript 的 API 之後,最後一步往往是把程式碼安全、穩定且可擴充地上線。
無論是單機 VPS、雲端 VM,或是容器化的微服務環境,都需要一套流程管理與環境分離的機制,才能確保:
- 服務不中斷:程式碼更新或重啟不會導致使用者看到 5xx 錯誤。
- 資源可控:CPU、記憶體、日誌等資源被妥善管理,避免「一個崩潰」拖垮整個系統。
- 環境一致:開發、測試、正式環境的設定保持一致,減少「本機跑得好,上線卻錯」的狀況。
本篇文章將以 PM2 與 Docker 為例,說明在 ExpressJS + TypeScript 專案中如何做到「部署」與「環境分離」,並提供實作範例、常見陷阱與最佳實踐,協助你從新手成長為能在生產環境自信部署的開發者。
核心概念
1. 為什麼要區分 PM2 與 Docker?
| 項目 | PM2 | Docker |
|---|---|---|
| 定位 | Node.js 進程管理器,負責守護、重啟、零停機升級 | 輕量級虛擬化平台,封裝整個執行環境(OS、套件、程式碼) |
| 適用情境 | 單機或少量 VM,快速上手、資源開銷低 | 多服務、微服務、CI/CD、跨平台部署 |
| 學習曲線 | 較平緩,只需了解 ecosystem.config.js |
較陡,需要 Dockerfile、映像檔、容器網路等概念 |
| 部署方式 | 直接在宿主機執行 pm2 start,使用 pm2 reload 零停機 |
建立映像檔 → docker run 或 docker‑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_on讓api在db啟動後再執行。- 透過
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.yml的environment會直接覆寫process.env;在本機開發時,只要放一個.env檔即可。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 / 最佳實踐 |
|---|---|---|
| 忘記編譯 TypeScript | 直接把 src/*.ts 複製到容器,導致 node 無法執行 |
使用 multi‑stage Docker 或在 PM2 設定 interpreter: "ts-node"(僅開發) |
| 環境變數未同步 | process.env.PORT 在本機與容器中不同,導致無法啟動 |
把所有變數寫在 ecosystem.config.js 或 docker‑compose.yml,並在程式碼中提供 預設值 |
| 日誌佔滿磁碟 | PM2 或 Docker 產生大量 stdout/stderr,未設定輪替 | PM2 使用 log_date_format 與 max_size,Docker 可掛載外部卷並使用 logrotate |
容器內部執行 npm install |
每次 docker run 都重新安裝套件,建置時間變長 |
Dockerfile 中只在 build 階段安裝,runtime 階段使用 --only=production |
| 零停機升級失敗 | 直接 docker stop → docker start 會短暫斷線 |
使用 PM2 zero‑downtime reload(PM2)或 Rolling Update(K8s) |
| 映像檔太大 | 基礎映像使用完整的 node:20,導致映像 > 500 MB |
改用 node:20-alpine,搭配 multi‑stage 減少層級 |
忘記 .dockerignore |
node_modules、dist 等被一起打包,映像變龐大 |
建立 .dockerignore,排除 node_modules, dist, .git, *.log 等 |
其他最佳實踐
- 分離設定檔:把
pm2、docker-compose、.env等檔案分別放在deploy/目錄,讓程式碼倉庫保持乾淨。 - 健康檢查:在
docker-compose.yml加入healthcheck,確保容器真正可接受請求才標記為healthy。healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 5s retries: 3 - 版本化映像:使用
image: myorg/express-ts-api:1.2.3,配合 CI/CD 自動 tag,避免「latest」混亂。 - 資源限制:在 Docker 中設定
mem_limit、cpu_shares,防止單一服務佔用過多資源。 - 備份與回滾:PM2
pm2 save+pm2 resurrect,或 Dockerdocker-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 Dockerfile、
docker-compose.yml,可在開發、測試、正式環境間保持 一致性,同時支援 水平擴展 與 CI/CD。 - 在 ExpressJS + TypeScript 專案中,兩者並非互斥,而是可以 相輔相成:容器內使用
pm2-runtime,即享有 PM2 的叢集與 graceful reload,又保有 Docker 的環境隔離與可移植性。
最後的建議:先在本機使用 PM2 完成開發與測試,確認一切穩定後,再以 Docker 打包映像,將部署流程自動化。如此一來,你的服務不僅能在任何雲端平台快速上線,也能在未來的擴容或微服務化路徑上,無縫銜接。祝開發順利,部署無慮!