Docker DevOps VPS GitHub Actions CI/CD 自動化部署

用 GitHub Actions 自動部署到 VPS:push 完程式碼,伺服器自己更新

手動 SSH 進伺服器、git pull、重啟服務——這套部署流程在 2026 年可以完全自動化。本文說明如何用 GitHub Actions 建立 CI/CD pipeline,push 到 main 就自動測試、打包 Docker image 並部署到 VPS,含 zero-downtime 與回滾策略。

每次部署都 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 # 需要有權限推到 ghcr.io

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

# 登入 ghcr.io(VPS 上需要有讀取權限)
echo ${{ secrets.GITHUB_TOKEN }} | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin

# 拉取新 image
docker compose pull

# Zero-downtime rolling update
docker compose up -d --no-deps --remove-orphans app

# 清理不再使用的 image(避免磁碟慢慢被吃滿)
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
# 在 VPS 上執行(一次性設定)
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
# 回滾到特定 commit
docker pull ghcr.io/你的帳號/你的repo:sha-abc1234
1
2
3
4
# 臨時把 compose.yaml 改成指定版本
services:
app:
image: ghcr.io/你的帳號/你的repo:sha-abc1234
1
docker compose up -d

如果 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 方案 →