Ian Chou's Blog

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),有個常見誤區:

{
  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 取代了哪些工具?

以前你需要:

現在 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 特別友善?

  1. 部署速度極快uv sync 安裝依賴比 pip/poetry 快 10-100 倍。對於 NAS 這種 CPU 較弱的設備,這非常有感。

  2. 磁碟空間節省:uv 使用全域快取(Global Cache)。如果你有多個 Python 專案,共用的底層套件只會存一份在 NAS 硬碟裡。

  3. 鎖定依賴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 最大的威脅:

「為什麼要管理進程?直接管理容器就好。」

在雲原生架構中,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 是最佳選擇:

定版後:考慮 Docker

當 MCP Server 穩定了,可以用 Docker 封裝:


第九章:常用工作流

日常開發流程

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 上,可以直接呼叫 bunuvpm2,不需要處理複雜的 SSH 權限或檔案傳輸。
效能極佳 因為 node_modules.venv 都在本地快取,npm install / uv sync 的速度是秒級的。
完全免費 GitHub 對 Self-hosted Runner 不收費,只收雲端 Runner 的費用。

Step 1: 在 fnNAS 上安裝 Runner

  1. 去您的 GitHub Repo 頁面 -> Settings -> Actions -> Runners -> New self-hosted runner
  2. 選擇 Linux
  3. 它會給您一串指令(Download, Configure, Run)。
  4. 關鍵技巧:在 fnNAS 上,建議把 Runner 安裝在 /vol1/github-runner(確保空間足夠)。
mkdir -p /vol1/github-runner && cd /vol1/github-runner
# ... 貼上 GitHub 給您的下載與安裝指令 ...
  1. 執行安裝腳本後,最後選擇 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。

作法

  1. 手動在 NAS 的專案目錄 /vol1/workspace/my-project/.env 建立好檔案。
  2. Runner 只負責拉程式碼(git pull 的概念),不會覆蓋掉您本地的 .env
  3. 或者,使用 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

  1. 這需要您在筆電能夠 SSH 連上 NAS
  2. 如果您出門在外,或是在公司網路,連不回家裡的 NAS,就不能部署了。
  3. 相比之下,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 統一管理所有服務,監控、重啟、日誌

這三者的共同特點:

對於在 fnNAS 上打造 Local-first AI AgentMCP Server 的開發者來說,這就是你的生存工具包。


延伸閱讀