Skip to content

用 GitHub Actions 把 Node.js 專案自動部署到 VPS(PM2 實作)

用 GitHub Actions 把 Node.js 專案自動部署到 VPS(PM2 實作)

還記得我剛轉職寫程式那陣子,每次改完功能要上線,都是手動 SSH 連進主機、git pullnpm install、再手動重啟服務⋯⋯一個流程跑下來十幾分鐘,還常常因為少打一個指令而出包。那種「我到底有沒有忘記哪一步」的焦慮感,我相信很多剛開始碰部署的朋友都懂。

這篇就要來解決這件事:用 GitHub Actions 把整個部署流程自動化,讓你「git push 之後喝口水,回來服務就已經更新好了」。

先講清楚定位:這篇用 PM2 來管理 Node.js 程序,目的是讓你先理解「自動部署到底在做哪些事」。實務上業界更常用 Docker 容器化來做,但那是另一個層次的故事。先把基礎觀念搞懂,之後學 Docker 會輕鬆很多。

整篇分成三段:環境的金鑰準備、撰寫 workflow、以及一些容易踩雷的補充。

先搞懂:自動部署的本質是什麼

在動手前,我想先帶大家建立一個畫面感。所謂的「自動部署」,其實沒有那麼神秘,它就是把你平常手動做的事,交給一台 GitHub 提供的臨時電腦(Runner)幫你跑一遍:

  1. 把最新的程式碼抓下來
  2. 安裝套件、做必要的建置
  3. 透過 SSH 連到你的主機,把檔案傳過去
  4. 重啟服務

關鍵在第 3 步:GitHub 的 Runner 是一台陌生的機器,它憑什麼能登入你的主機?答案就是 SSH 金鑰。我們要做的,就是發一把「鑰匙」給它,並告訴主機「拿這把鑰匙來的都是自己人」。理解了這層關係,後面的設定就不會只是照抄指令了。

第一步:準備 SSH 金鑰並登記到主機

這篇假設你的主機已經裝好 Node.js 與 PM2。如果還沒,建議先把環境跑起來再回來。

我們要做的事可以拆成兩半:產生一把鑰匙(公私鑰),然後把公鑰登記在主機、把私鑰交給 GitHub

產生金鑰

在本機(或主機)執行,記得把 Email 換成自己的:

bash
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

接著它會問你幾個問題,新手照著做就好:

  • 問你要存哪 → 直接按 Enter,預設存在 ~/.ssh/id_rsa
  • 問你要不要設密碼短語(passphrase)→ 這裡直接按 Enter 留空

特別提醒:雖然設 passphrase 安全性更高,但在這個自動化流程裡,有密碼短語的金鑰會卡住 workflow(因為 Runner 沒辦法在跑到一半時手動輸入密碼)。想用有密碼的金鑰,得額外調整 workflow,新手先留空最省事。

跑完後你會得到一對檔案,可以這樣確認:

bash
cat ~/.ssh/id_rsa      # 私鑰,這把要保密,等等給 GitHub
cat ~/.ssh/id_rsa.pub  # 公鑰,這把要登記到主機

私鑰就像你家鑰匙,千萬別貼到公開的地方。公鑰則像門鎖的形狀,可以大方公開。

把公鑰登記到主機

把公鑰加進主機的授權清單,並設定權限(這步常被忽略,權限不對 SSH 會直接拒絕連線):

bash
# 把公鑰內容加進授權清單(引號內換成你 id_rsa.pub 的內容)
echo 'ssh-rsa AAAA...你的公鑰...' >> ~/.ssh/authorized_keys

# 設定正確權限
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

把私鑰交給 GitHub Secrets

私鑰不能直接寫在 workflow 檔案裡(那等於把家門鑰匙貼在 GitHub 上給全世界看),要放進 Secrets 這個加密保險箱:

  1. 打開儲存庫 → Settings → 左側 Secrets and variablesActionsNew repository secret
  2. 名稱填 DEPLOY_SSH_KEY,內容貼上完整的私鑰(cat ~/.ssh/id_rsa 的全部輸出,包含開頭結尾那兩行 -----BEGIN... / -----END...

順帶把主機位置也存成 secret,名稱 HOSTNAME,內容是你的 IP 或網域。之後需要用到的環境變數(資料庫連線字串、PORT 等)都用同樣方式加進來,避免硬寫在程式碼裡。

第二步:撰寫 GitHub Actions workflow

在專案根目錄建立 .github/workflows/deploy.yml。GitHub 會自動掃描這個資料夾,把裡面的 yml 當成自動化任務來執行。

yml
name: 部署 PM2 專案

on:
  push:
    branches:
      - main   # 推到哪個分支就部署,依你的習慣調整

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 取得程式碼
        uses: actions/checkout@v4

      - name: 安裝 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: 安裝套件與建置
        run: |
          npm ci
          npm run build --if-present

      - name: 產生 .env 檔
        run: |
          echo "PORT=${{ secrets.ENV_PORT }}" >> .env
          echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env

      - name: 載入 SSH 私鑰
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}

      - name: 信任主機(寫入 known_hosts)
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan ${{ secrets.HOSTNAME }} >> ~/.ssh/known_hosts

      - name: 傳輸檔案到主機
        run: rsync -avz --delete ./ root@${{ secrets.HOSTNAME }}:~/app/

      - name: 重啟 PM2
        run: ssh root@${{ secrets.HOSTNAME }} 'cd ~/app && pm2 restart www --update-env'

讓我用白話拆解這幾個關鍵步驟,理解了你就能自己改:

  • 取得程式碼 / 安裝 Node.js:在那台臨時 Runner 上把專案環境準備好。這裡我把版本更新到 @v4 與 Node 20,原文是比較舊的 @v1 與 Node 18,舊版本現在會跳警告。
  • npm ci:比 npm install 更適合 CI 環境,它會嚴格依照 package-lock.json 安裝,速度快又穩定。
  • 產生 .env 檔:把放在 Secrets 的環境變數寫成 .env,等等隨檔案一起傳到主機。
  • ssh-keyscan:第一次連線時 SSH 會問「你信任這台主機嗎」,在自動化流程裡沒人能回答,所以我們預先把主機指紋寫進 known_hosts,等於先簽好信任書。
  • rsync --delete:只傳「有變動」的檔案,比整包覆蓋快很多;--delete 會把主機上多餘的舊檔清掉,讓兩邊保持一致。
  • pm2 restart --update-env:連進主機重啟服務,--update-env 確保剛剛更新的環境變數會被吃進去。

第三步:看結果與踩雷補充

設定完成後,每次 push 到指定分支,到儲存庫上方的 Actions 分頁就能看到部署紀錄:

  • 綠色勾勾 = 成功,點進去能看到每一步的詳細 log
  • 紅色叉叉 = 失敗,一樣點進去看 log,錯誤訊息通常會直接告訴你卡在哪一步

第一次跑十之八九不會一次過,這很正常,我自己當年也是紅了好幾次才綠。最常見的雷有三個:私鑰沒貼完整(少了開頭結尾)、主機端 authorized_keys 權限不對、以及 rsync 路徑寫錯。對著 log 一步步排查就好,別氣餒。

最後兩個提醒。第一,GitHub Actions 不是無限免費,公開儲存庫免費,私有儲存庫每月有額度,用量可以在「個人設定 → Billing」查到,一般個人專案幾乎用不完。第二,前面說過,這套 PM2 流程是拿來理解原理用的,等你熟了,下一步可以把它換成 Docker,部署會更乾淨、更好搬家。


對了,如果你也在練習把專案部署上線,順手分享一個我自己做、同樣是「靜態建置後部署到雲端」的小成品:QR Code Studio,純靜態、開箱即用,當作部署練習的目標專案也很合適。希望這篇有幫你把「自動部署」這道坎踏過去,我們下篇見!