每次部署都 SSH 進伺服器手動操作,不只浪費時間,還是出錯的根源。忘記重啟服務、在錯誤的目錄跑指令、忘記更新環境變數——這些問題只要部署流程自動化就全都消失。
GitHub Actions 讓你把整個部署流程定義成程式碼,存放在版本庫裡,可以被追蹤、被測試、被回滾。這篇文章說明一套實用的架構:push 到 main,GitHub 的 runner 幫你建 Docker image,推到 registry,再透過 SSH 讓 VPS 拉取並重啟服務。
架構概覽 1 2 3 4 5 6 7 8 9 10 11 12 13 git push main │ ▼ GitHub Actions (GitHub-hosted runner) ├─ 執行測試 ├─ 建 Docker image ├─ 推到 GitHub Container Registry (ghcr.io) │ ▼ SSH 進 VPS ├─ docker pull 新 image ├─ docker compose up -d(rolling update) └─ 清理舊 image
建 image 的工作交給 GitHub 的 runner,VPS 只做 pull 和重啟,不需要在 VPS 上安裝 build tools 或占用大量 CPU。這是關鍵設計,否則在資源有限的 VPS 上跑 Docker build 會很痛苦。
前置準備:設定 GitHub Secrets 在 GitHub 的 repository → Settings → Secrets and variables → Actions 新增以下 secrets:
VPS_HOST:伺服器 IP 或網域
VPS_USER:SSH 使用者名稱(建議用專門的 deploy 使用者,不要用 root)
VPS_SSH_KEY:SSH 私鑰內容(cat ~/.ssh/id_ed25519 的輸出)
VPS_SSH_PORT:SSH port(如果有改過預設的 22)
SSH 私鑰的對應公鑰要先加到 VPS 的 ~/.ssh/authorized_keys,確認可以手動連線後再繼續。
Workflow 檔案 在 repository 建立 .github/workflows/deploy.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 name: Build and Deploy on: push: branches: [main ] workflow_dispatch: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: | # 換成你的測試指令 docker compose -f compose.test.yml up --abort-on-container-exit --exit-code-from app docker compose -f compose.test.yml down build-and-push: needs: test runs-on: ubuntu-latest permissions: contents: read packages: write outputs: image_tag: ${{ steps.meta.outputs.tags }} steps: - uses: actions/checkout@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=sha,prefix=sha- type=raw,value=latest,enable={{is_default_branch}} - name: Build and push uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max deploy: needs: build-and-push runs-on: ubuntu-latest steps: - name: Deploy to VPS uses: appleboy/ssh-action@v1 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} port: ${{ secrets.VPS_SSH_PORT }} script: | cd /opt/myapp echo ${{ secrets.GITHUB_TOKEN }} | \ docker login ghcr.io -u ${{ github.actor }} --password-stdin docker compose pull docker compose up -d --no-deps --remove-orphans app docker image prune -f
cache-from: type=gha 讓 Docker layer cache 能跨 workflow 保留,大幅縮短後續建置時間。第一次 build 可能要幾分鐘,之後只有變動的 layer 需要重建,速度快很多。
VPS 上的準備工作 VPS 需要能讀取 ghcr.io 上的 private image。最乾淨的方式是用 GitHub 的 Personal Access Token(只需要 read:packages 權限)在 VPS 上做一次 docker login:
1 2 echo "你的_PAT" | docker login ghcr.io -u 你的GitHub帳號 --password-stdin
這個 credential 會被存到 ~/.docker/config.json,之後的 docker pull 都不需要再輸入。
VPS 上的 compose.yaml 要把 image 指向 registry:
1 2 3 4 5 services: app: image: ghcr.io/你的帳號/你的repo:latest restart: unless-stopped
回滾策略 自動部署的最大風險是 bug 上線後沒有快速回滾的方法。image tag 策略很關鍵:每次 build 除了 latest,還要打上 commit hash(上面 workflow 已包含 type=sha tag)。
1 2 docker pull ghcr.io/你的帳號/你的repo:sha-abc1234
1 2 3 4 services: app: image: ghcr.io/你的帳號/你的repo:sha-abc1234
如果 VPS 本地還有上一個版本的 image(沒被 prune 掉),甚至不需要重新 pull,直接改 tag 就能秒回滾。這是 docker image prune 只清理 dangling image 而不是所有舊 image 的原因——保留最近幾版作為緊急備用。
環境變數的處理 .env 檔不應該進版本庫,也不適合放在 GitHub Secrets 裡整包傳到 VPS(不好維護)。比較好的做法是:
環境變數直接存在 VPS 的 /opt/myapp/.env,手動管理(只有真的改變時才需要動)。敏感值(API keys、DB 密碼)在 VPS 本機,不經過 GitHub。
如果需要讓 CI 知道部分環境變數(例如不同環境的 API endpoint),才放進 GitHub Secrets,在 deploy step 裡覆蓋或補充。
關於 Self-hosted Runner 的注意事項 2026 年 3 月起,GitHub 對 private repository 的 self-hosted runner 使用引入了每分鐘 $0.002 的平台費,這讓直接在 VPS 上裝 runner 的成本效益下降了一些。對於大部分中小型專案,繼續用 GitHub-hosted runner 做 build,只用 SSH 部署到 VPS 才是更划算的架構(也就是本文採用的方式)。只有 VPS 需要直接存取 build 產物(例如特殊硬體、內網資源)時,self-hosted runner 才值得考慮。
讓 Workflow 更健壯的細節 加上 timeout 防止卡住 :
1 2 3 jobs: deploy: timeout-minutes: 15
部署前做 health check :
1 2 3 4 5 6 7 8 9 10 11 - name: Wait for service to be healthy run: | for i in {1..10}; do if curl -sf https://yourdomain.com/health; then echo "Service is healthy" exit 0 fi sleep 5 done echo "Health check failed" exit 1
失敗通知 :
1 2 3 4 5 6 7 - name: Notify on failure if: failure() uses: appleboy/telegram-action@v1 with: to: ${{ secrets.TELEGRAM_CHAT_ID }} token: ${{ secrets.TELEGRAM_TOKEN }} message: "🚨 Deploy failed on ${{ github.sha }} "
通知服務用 Telegram bot 或 Slack 都可以,重點是部署失敗要立即知道,不要等到使用者回報。
想要一個穩定的基礎設施跑自動化部署?NCSE Network 的 VPS 位於臺灣機房,NVMe SSD 讓 Docker image pull 和啟動速度更快。詳情見 ncse.tw 。
需要穩定的雲端主機?
NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。
查看 VPS 方案 →