PM2 + uv + Bun:fnNAS 上的黃金三角部署策略
前言:為什麼需要 PM2?
當你在 fnNAS(或任何 Linux 機器)上跑長期運行的服務時,一個關鍵問題是:
「如果程式崩潰了怎麼辦?如果 NAS 重開機了,服務會自動啟動嗎?」
手寫 systemd 可以解決這個問題,但對於習慣 JavaScript/TypeScript 生態的開發者來說,PM2 是更優雅的解法——它本質上是一個 守護進程管理器 (Daemon Process Manager),雖然為 Node.js 設計,但可以管理任何腳本或執行檔(包括 Bun、Python、Go)。
第一章:PM2 核心概念
PM2 比 systemd 好用的地方
如果你用 systemd,你得自己處理 Log 輪替、崩潰重啟邏輯、環境變數載入。PM2 把這些都封裝好了:
| 功能 | PM2 | systemd |
|---|---|---|
| 自動重啟 | ✅ 內建 | 需手寫配置 |
| 熱重載 | ✅ --watch 支援 |
需搭配其他工具 |
| 日誌管理 | ✅ 內建 Log Rotate | 需整合 journald |
| 監控儀表板 | ✅ pm2 monit |
僅文字 Log |
| 叢集模式 | ✅ 內建負載平衡 | 需自己設計 |
安裝 PM2(使用 Bun)
在 fnNAS 上透過 Bun 安裝:
bun add -g pm2
第二章:PM2 + Bun 實戰配置
專案路徑選擇(重要)
在 fnNAS 上,請把服務放在 /vol1 而非 /root 或 /home:
mkdir -p /vol1/workspace/my-mcp-server
cd /vol1/workspace/my-mcp-server
啟動 Bun 應用
因為 PM2 預設跑 Node.js,要跑 Bun 需指定 Interpreter。
方式 A:指令啟動(適合快速測試)
pm2 start index.ts --interpreter bun --name "mcp-server"
方式 B:ecosystem.config.js(強烈推薦)
這是 Infrastructure as Code 的最佳實踐:
module.exports = {
apps : [{
name : "lance-db-agent",
script : "index.ts",
interpreter: "bun",
cwd: "/vol1/workspace/my-mcp-server",
watch : true, // 開發模式:檔案變更自動重啟
env: {
PORT: 3000,
NODE_ENV: "development"
},
env_production: {
PORT: 80,
NODE_ENV: "production",
watch: false
}
}]
}
啟動方式:
pm2 start ecosystem.config.js
第三章:PM2 開機自啟 (Startup Hook)
這是 PM2 的殺手級功能,解決「NAS 重開機服務沒跑起來」的痛點。
設定步驟
# 1. 確認服務都在 online 狀態
pm2 list
# 2. 凍結當前服務列表
pm2 save
# 3. 生成開機啟動腳本
pm2 startup
執行 pm2 startup 後,終端會吐出一行指令(類似):
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u root ...
複製並執行那行指令,PM2 會自動幫你生成一個 systemd service 檔來載入你的服務。
為什麼這比直接寫 systemd 好?
如果 fnOS 更新把 systemd 重置了,你只要再跑一次 pm2 startup + 貼上指令,一秒恢復,不用重寫 .service 檔。
第四章:PM2 管理 Python 應用
PM2 的一大優勢是統一管理混合技術棧。如果你同時有 Bun 專案和 Python AI 工具,不用維護兩套管理邏輯。
基礎用法:直接跑 Python 腳本
pm2 start main.py --interpreter python3 --name "my-python-script"
進階用法:配合 venv 虛擬環境(關鍵)
黃金法則:絕對不要使用 fnNAS 系統自帶的 Python (/usr/bin/python3) 來跑你的專案,那會弄髒系統。
在 PM2 裡使用 venv 的秘訣是:直接指定 venv 裡的 python 執行檔路徑。
module.exports = {
apps : [{
name : "python-rag-agent",
script : "main.py",
// 關鍵:指向 venv 裡的 python 解釋器
interpreter : "/vol1/workspace/my-python-project/venv/bin/python",
cwd: "/vol1/workspace/my-python-project",
watch : false,
env: {
PYTHONUNBUFFERED: "1", // 重要!讓 Python log 即時輸出
HF_TOKEN: "your_huggingface_token"
}
}]
}
FastAPI / Uvicorn 的正確配置
如果你跑 Web Server(FastAPI、Flask),有個常見誤區:
- ❌ 不要用
python main.py去跑開發用的 server - ✅ 應該讓 PM2 直接去跑 Uvicorn
{
name: "fastapi-server",
script: "/vol1/workspace/project/venv/bin/uvicorn",
args: "main:app --host 0.0.0.0 --port 8000",
interpreter: "none", // 因為 script 本身就是執行檔
cwd: "/vol1/workspace/project",
env: {
PYTHONUNBUFFERED: "1"
}
}
這樣 PM2 負責守護 Uvicorn,Uvicorn 負責處理 HTTP 請求,職責分離最乾淨。
第五章:uv — Python 界的 Bun
如果你熟悉 Bun 帶來的速度震撼,那你一定會愛上 uv (by Astral)。
uv 與 Bun 的類比
| 特性 | Bun (前端) | uv (Python) |
|---|---|---|
| 底層語言 | Zig | Rust |
| 速度 | 比 npm 快 10-100 倍 | 比 pip 快 10-100 倍 |
| All-in-One | Runtime + Package Manager + Bundler + Test Runner | Python Version Manager + venv + pip + pip-tools |
| 安裝方式 | 單一執行檔 | 單一執行檔 |
uv 取代了哪些工具?
以前你需要:
pyenv:管理 Python 版本venv/virtualenv:建立虛擬環境pip+pip-tools:安裝套件與鎖定版本Poetry:專案管理
現在 uv = 以上全部,而且快到飛起。
uv + PM2 的最佳實踐
雖然 uv 有 uv run main.py 指令,但在 PM2 裡,不要直接用 uv run 作為啟動指令。
最佳做法是:利用 uv 來管理環境(uv sync),然後讓 PM2 直接去執行該環境下的 Python 二進位檔。
這樣做的好處是:PM2 可以直接監控 Python Process 的 PID,而不是監控 uv 這個 Wrapper,對於訊號傳遞(Signal Handling,如平滑重啟)會更準確。
專案設置流程
cd /vol1/workspace/my-python-agent
# 初始化 uv 專案
uv init
# 新增依賴
uv add fastapi uvicorn lancedb
# 確保 .venv 目錄被建立且同步
uv sync
對應的 PM2 設定
module.exports = {
apps : [{
name : "uv-agent",
script : "main.py",
// 指向 uv 建立的虛擬環境
interpreter : "./.venv/bin/python",
cwd: "/vol1/workspace/my-python-agent",
watch: false,
env: {
PYTHONUNBUFFERED: "1",
UV_PROJECT_ENVIRONMENT: "/vol1/workspace/my-python-agent/.venv"
}
}]
}
為什麼 uv 對 fnNAS 特別友善?
-
部署速度極快:
uv sync安裝依賴比 pip/poetry 快 10-100 倍。對於 NAS 這種 CPU 較弱的設備,這非常有感。 -
磁碟空間節省:uv 使用全域快取(Global Cache)。如果你有多個 Python 專案,共用的底層套件只會存一份在 NAS 硬碟裡。
-
鎖定依賴:
uv.lock確保在 NAS 上跑的環境,跟開發時完全一致。
第六章:PM2 的競爭對手分析
在選擇進程管理工具前,了解 PM2 的定位和競爭對手很重要。
上一代遺產(Node.js 生態系內)
| 工具 | 現況 | 結論 |
|---|---|---|
| Forever | 幾乎已死(Maintenance mode),功能單純 | ❌ 不要用 |
| Nodemon | 專注於開發環境的 Watch & Reload | PM2 的 watch: true 已包含此功能 |
跨語言通才(Linux Ops/SRE 的最愛)
| 工具 | 特點 | 與 PM2 對比 |
|---|---|---|
| Supervisor | Python-based,Linux 運維界老大哥 | PM2 對 JS 開發者友善,Supervisor 對系統管理員友善 |
| Systemd | Linux 原生,最高權限 | 維護成本高,但更底層 |
降維打擊(架構典範轉移)
Docker / Kubernetes 是 PM2 最大的威脅:
「為什麼要管理進程?直接管理容器就好。」
- Docker 的
restart: always是最基礎的 PM2 - Kubernetes 的 Pod 自動修復完全取代了 PM2 的叢集模式
在雲原生架構中,PM2 的地位被邊緣化了。但在 fnNAS 這種家用設備上,PM2 仍是最佳選擇。
綜合比較表
| 維度 | PM2 | Systemd | Docker | Supervisor |
|---|---|---|---|---|
| 上手難度 | ⭐ (極低) | ⭐⭐⭐ (高) | ⭐⭐ (中) | ⭐⭐ (中) |
| 語言相依性 | 需安裝 Node/Bun | 無 (OS 內建) | 無 (自帶環境) | 需安裝 Python |
| 監控介面 | ⭐⭐⭐⭐⭐ (漂亮、即時) | ⭐ (僅文字 Log) | ⭐⭐⭐ (需搭配 Portainer) | ⭐⭐ (陽春網頁) |
| 適用場景 | 單機多服務 | 系統級服務 | 微服務/遷移 | 混合語言 |
第七章:常用維運指令 Cheatsheet
狀態監控
# 即時儀表板(左:進程列表,右:即時 Log)
pm2 monit
# 列出所有進程
pm2 list
# 查看特定服務詳情
pm2 show mcp-server
日誌管理
# 看所有 Log
pm2 logs
# 只看特定服務
pm2 logs lance-db-agent
# 清空 Log(Debug 時好用)
pm2 flush
進程管理
# 重啟所有
pm2 restart all
# 重啟特定服務
pm2 restart mcp-server
# 停止特定進程(by ID)
pm2 stop 0
# 刪除所有進程
pm2 delete all
# 平滑重載(零停機)
pm2 reload all
第八章:開發 vs 生產的策略選擇
開發階段:PM2 優先
在 fnNAS 上開發時,PM2 是最佳選擇:
- 改 Code → 自動熱重載(
watch: true) - 直接操作 Btrfs 檔案系統
pm2 logs即時查看 Debug 訊息- 不需要 Rebuild image 或設定 Bind Mount
定版後:考慮 Docker
當 MCP Server 穩定了,可以用 Docker 封裝:
- 環境隔離更完整
- 遷移到雲端(Cloudflare/Fly.io)無縫接軌
- 符合 CISSP 資安隔離標準
第九章:常用工作流
日常開發流程
cd /vol1/workspace/my-project
# 修改程式碼...
# PM2 會自動偵測檔案變更並重載
pm2 logs my-service
部署更新流程(Git + uv + PM2)
git pull # 拉取最新程式碼
uv sync # 瞬間同步 Python 環境
pm2 restart uv-agent # 重啟服務
新機器設置流程
# 1. 安裝 PM2
bun add -g pm2
# 2. 啟動你的服務
pm2 start ecosystem.config.js
# 3. 儲存當前狀態
pm2 save
# 4. 設置開機自啟
pm2 startup
# 執行輸出的指令
第十章:CI/CD 最佳解法 — GitHub Self-hosted Runner
這是一個非常好的架構問題:PM2 本身不是 CI/CD 工具(它是管執行的),但它非常適合成為 CI/CD 流程的「終點」。
考慮到您的 CISSP 背景(重視安全性)以及 fnNAS 在家中內網(沒有固定 Public IP)的特性,標準的雲端 CI/CD(如 GitHub 透過 SSH 連進來)會很難搞,因為您得在路由器上開 Port,這很危險。
這裡有一個最安全、最現代化、且完全免費的解決方案:GitHub Self-hosted Runner。
核心架構概念
這個架構的核心概念是:不要讓 GitHub「連進來」,而是派一個間諜(Runner)在您的 NAS 裡「主動去問」GitHub 有沒有新工作。
┌─────────────────────────────────────────────────────────────────┐
│ Internet │
│ │
│ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ GitHub │◄────────│ 你在任何地方 git push │ │
│ │ Actions │ └─────────────────────────────┘ │
│ └──────┬──────┘ │
│ │ │
│ │ Outbound │
│ │ Connection │
│ ▼ │
│ ┌─────────────┐ │
│ │ Self-hosted │ ← Runner 主動向外拉取任務 │
│ │ Runner │ (家中 fnNAS,無需開 Port) │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ PM2 │ ← 執行 restart / reload │
│ │ Services │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
為什麼這是給 fnNAS 的最佳解?
| 優勢 | 說明 |
|---|---|
| 零防火牆破口 | 不需要在路由器開 Port,也不用透過 Cloudflare Tunnel 暴露 SSH。Runner 是從內網向外 (Outbound) 連線到 GitHub 抓任務。 |
| 直接存取環境 | Runner 直接跑在您的 fnNAS 上,可以直接呼叫 bun、uv 和 pm2,不需要處理複雜的 SSH 權限或檔案傳輸。 |
| 效能極佳 | 因為 node_modules 或 .venv 都在本地快取,npm install / uv sync 的速度是秒級的。 |
| 完全免費 | GitHub 對 Self-hosted Runner 不收費,只收雲端 Runner 的費用。 |
Step 1: 在 fnNAS 上安裝 Runner
- 去您的 GitHub Repo 頁面 -> Settings -> Actions -> Runners -> New self-hosted runner。
- 選擇 Linux。
- 它會給您一串指令(Download, Configure, Run)。
- 關鍵技巧:在 fnNAS 上,建議把 Runner 安裝在
/vol1/github-runner(確保空間足夠)。
mkdir -p /vol1/github-runner && cd /vol1/github-runner
# ... 貼上 GitHub 給您的下載與安裝指令 ...
- 執行安裝腳本後,最後選擇 Run as service(它會自動幫您註冊成 systemd 服務,開機自啟)。
sudo ./svc.sh install
sudo ./svc.sh start
Step 2: 撰寫 Workflow (.github/workflows/deploy.yml)
在您的專案根目錄建立這個檔案。這個腳本會告訴 NAS 上的 Runner 該做什麼。
name: Deploy to fnNAS
on:
push:
branches: ["main"] # 只有推送到 main 分支時才觸發
jobs:
deploy:
runs-on: self-hosted # 關鍵!指定由您的 NAS 執行,而不是 GitHub 的雲端機器
steps:
- name: Checkout code
uses: actions/checkout@v4
# 這裡會自動把最新的 Code 拉到 Runner 的工作目錄
# ---------------------------------------------------------
# 情境 A: 如果是 Python 專案 (使用 uv)
# ---------------------------------------------------------
- name: Sync Python Environment (uv)
run: |
# 假設 uv 已經在 NAS 的全域路徑中,或指定絕對路徑
/root/.cargo/bin/uv sync
# ---------------------------------------------------------
# 情境 B: 如果是 Node/Bun 專案
# ---------------------------------------------------------
# - name: Install Dependencies (Bun)
# run: |
# /root/.bun/bin/bun install
- name: Restart Application via PM2
run: |
# 這裡需要指定絕對路徑,確保 Runner 找得到 PM2
# 並且指向您的 ecosystem.config.js
/root/.bun/bin/pm2 restart ecosystem.config.js --update-env
# 儲存當前狀態,以防 NAS 重開機
/root/.bun/bin/pm2 save
Step 3: 進階 Workflow — 混合技術棧部署
如果您的專案同時包含 Python 和 Node.js/Bun,可以使用更完整的 Workflow:
name: Deploy Mixed Stack to fnNAS
on:
push:
branches: ["main"]
workflow_dispatch: # 允許手動觸發
env:
UV_PATH: /root/.cargo/bin/uv
BUN_PATH: /root/.bun/bin/bun
PM2_PATH: /root/.bun/bin/pm2
PROJECT_DIR: /vol1/workspace/my-project
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Sync Python Environment
if: hashFiles('pyproject.toml') != ''
run: |
cd $
$ sync
echo "✅ Python dependencies synced"
- name: Install Node Dependencies
if: hashFiles('package.json') != ''
run: |
cd $
$ install
echo "✅ Node dependencies installed"
- name: Run Tests
run: |
cd $
$ test || true # 測試失敗不中斷部署
- name: Restart All Services
run: |
$ restart ecosystem.config.js --update-env
$ save
echo "✅ All services restarted"
- name: Health Check
run: |
sleep 5 # 等待服務啟動
$ list
echo "🎉 Deployment complete!"
常見雷區與解決方案(CISSP 視角)
1. 環境變數 (.env) 怎麼辦?
不要把 .env 檔 commit 到 GitHub。
作法:
- 手動在 NAS 的專案目錄
/vol1/workspace/my-project/.env建立好檔案。 - Runner 只負責拉程式碼(
git pull的概念),不會覆蓋掉您本地的.env。 - 或者,使用 GitHub Secrets,然後在
.yml裡把 Secret 寫入成.env檔:
- name: Create .env file
run: |
echo "DATABASE_URL=$" >> .env
echo "API_KEY=$" >> .env
2. 權限問題 (Permission Denied)
GitHub Runner 預設會建立一個叫 runner 的使用者(或是您執行安裝的那個使用者)。
如果您的 PM2 是用 root 跑的,但 Runner 是用普通用戶跑的,Runner 會無法重啟 PM2。
解決:在 fnNAS 這種環境,最簡單(雖然不完美)的方法是用 root 安裝 Runner(執行 ./config.sh 時允許 root)。或者,把 Runner 加入 docker/sudo 群組,並配置 sudo 免密碼執行 pm2。
# /etc/sudoers.d/runner
runner ALL=(ALL) NOPASSWD: /root/.bun/bin/pm2
3. PM2 找不到命令?
因為 Runner 是非互動式 Shell,它的 $PATH 可能跟您 SSH 進去時不一樣。
解法:在 .yml 裡盡量寫絕對路徑(例如 /root/.bun/bin/pm2 而不是只寫 pm2)。您可以在 SSH 用 which pm2 查路徑。
4. Runner 狀態監控
確保 Runner 持續運作:
# 檢查 Runner 服務狀態
sudo ./svc.sh status
# 查看 Runner 日誌
sudo journalctl -u actions.runner.* -f
# 重啟 Runner
sudo ./svc.sh stop
sudo ./svc.sh start
替代方案:PM2 Deploy
PM2 其實內建了一個部署工具,叫 pm2 deploy。
原理:您在筆電執行 pm2 deploy production -> PM2 透過 SSH 連進 NAS -> 叫 NAS git pull -> 重啟。
ecosystem.config.js 配置範例:
module.exports = {
apps: [/* ... 你的 apps 配置 ... */],
deploy: {
production: {
user: 'root',
host: '192.168.1.100', // 您的 fnNAS 內網 IP
ref: 'origin/main',
repo: '[email protected]:您的用戶名/您的專案.git',
path: '/vol1/workspace/my-project',
'pre-deploy-local': '',
'post-deploy': 'uv sync && pm2 reload ecosystem.config.js --env production',
'pre-setup': ''
}
}
}
為什麼我不推薦 PM2 Deploy 給 fnNAS:
- 這需要您在筆電能夠 SSH 連上 NAS。
- 如果您出門在外,或是在公司網路,連不回家裡的 NAS,就不能部署了。
- 相比之下,GitHub Runner 只要 NAS 有網路就能運作,您在世界任何角落 push code 到 GitHub,家裡的 NAS 就會自動更新。
CI/CD 方案比較表
| 方案 | 內網友善度 | 設置難度 | 安全性 | 遠端部署 |
|---|---|---|---|---|
| GitHub Self-hosted Runner | ⭐⭐⭐⭐⭐ | 中 | 最高 | ✅ 任何地方 |
| PM2 Deploy | ⭐⭐ | 低 | 中 | ❌ 需 SSH 連線 |
| 手動 SSH + git pull | ⭐⭐ | 最低 | 中 | ❌ 需 SSH 連線 |
| Webhook + 開 Port | ⭐ | 高 | 最低 | ✅ 但不安全 |
第十一章:結論 — 黃金三角(Bun + uv + PM2)
這個組合是目前單機高效能部署的最佳實踐:
| 角色 | 工具 | 職責 |
|---|---|---|
| JavaScript Runtime | Bun | 極速執行 TypeScript,內建套件管理 |
| Python 環境管理 | uv | 極速建立虛擬環境,統一套件管理 |
| 進程守護 | PM2 | 統一管理所有服務,監控、重啟、日誌 |
這三者的共同特點:快。
- Bun 比 Node.js 快
- uv 比 pip/poetry 快 10-100 倍
- PM2 讓你的服務「永不停機」
對於在 fnNAS 上打造 Local-first AI Agent 或 MCP Server 的開發者來說,這就是你的生存工具包。
延伸閱讀
- ← Previous
WSL 2 圖形界面完整指南:Wayland、WSLg 與 Windows 互通