📋 目錄
手動部署的痛:要在正式機上 git pull、npm install、重啟服務,過程中出錯還要現場除錯。GitHub Actions 讓這一切自動化——你只需要專心寫 code,push 之後的一切(測試、建構、部署)全部由機器完成。這篇整理 CI/CD 的核心概念與 GitHub Actions 的實作方式。
前言:為什麼要自動化部署?
手動部署的問題不只是費時,更重要的是不穩定。
人為操作會犯錯:可能忘記跑測試、忘記重開服務、或在正式環境做了一些實驗性變更沒有回復。CI/CD(持續整合 / 持續部署)的核心思想是:把「人」從部署流程中移除,讓機器執行定義好的流程,每次都完全一致。
好處:
- 穩定:機器執行,不會因為疲憊或分心而出錯
- 快速:幾分鐘內完成過去可能需要半小時的手動流程
- 可追溯:每次 deployment 都留有記錄,出了問題可以快速回溯
- 有信心:每次 push 都經過完整測試才部署,減少正式環境的 surprise
CI/CD 整體流程圖
flowchart TD
A[💻 Developer Push Code] --> B{GitHub Repository}
B --> C[CI Job: 自動化測試]
C --> D{測試通過?}
D -->|否| E[❌ 通知 Developer]
D -->|是| F[Build Job: 建構 Docker Image]
F --> G[📦 Image 推送到 Registry]
G --> H{部署目標}
H -->|Staging| I[部署到 Staging 環境]
H -->|Production| J[⚠️ 需要 Review]
J -->|核准| K[部署到 Production 環境]
J -->|拒絕| L[⛔ 取消部署]
I --> M[✅ 部署完成]
K --> M[✅ 部署完成]
M --> N[📊 Deployment Report]
上圖是 CI/CD 流程的全貌:push 觸發測試 → 測試通過後建構 Image → 依據環境決定是否需要人工核准 → 部署 → 產生部署報告。
CI/CD 核心概念
持續整合(CI)
每次有人 push 或 pull request,自動執行:
- 執行測試(Unit、Integration)
- 執行 linter、type check
- 建構 artifact
目標是盡快發現問題,而不是等到部署時才發現。
持續部署(CD)
CI 通過之後,自動把通過測試的版本部署到某個環境:
staging(測試環境)production(正式環境)
這篇以 CD 到 Docker 容器為例,銜接前一篇 Docker 部署的內容。
GitHub Actions 基本結構
GitHub Actions 的設定檔放在專案的 .github/workflows/ 目錄下,格式是 YAML。
核心名詞
- Workflow:一個 workflow 是一個自動化流程(例:CI、CD、每小时跑一次任務)
- Job:一個 workflow 可以有多個 job,並行執行
- Step:每個 job 有一個或多個 step,依序執行
- Action:step 裡面執行的動作,可以是現成的 GitHub Action 或自訂腳本
最簡單的 workflow
# .github/workflows/ci.yml
name: CI
# 觸發條件:push 和 pull request 到 main 分支
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# 單一 job,名稱叫 test
test:
runs-on: ubuntu-latest # 使用 Ubuntu runner
steps:
# 1. 取出程式碼
- uses: actions/checkout@v4
# 2. 安裝 Node.js
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm' # 快取 node_modules,加速後續 run
# 3. 安裝依賴
- run: npm ci
# 4. 跑 type check
- run: npm run typecheck
# 5. 跑測試
- run: npm test
這個 workflow 在每次 push 或 PR 到 main 時自動執行,檢查 TypeScript 類型與測試是否通過。
建立 Docker Image 並推送到 Registry
在 CI workflow 裡加入 Docker 建構與推送:
# .github/workflows/deploy.yml
name: Build & Push Docker Image
on:
push:
branches: [main]
tags:
- 'v*' # 當 push tag(如 v1.0.0)時也觸發
env:
REGISTRY: ghcr.io # GitHub Container Registry
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # 需要這個才能推送 Image 到 GHCR
steps:
- uses: actions/checkout@v4
# 登入 Container Registry
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# 從 tag 取得 Image 版本號(如 v1.0.0 → 1.0.0)
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=sha,prefix=
# 建構並推送 Image
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
上例中 GITHUB_TOKEN 是 GitHub 自動提供的 credential,不需要另外設定。重點是 secrets.GITHUB_TOKEN 這類內建 secret,不需要手動管理。
完整的 CI/CD workflow
把測試、建構、部署串在一起:
# .github/workflows/cicd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
TAG: ${{ github.sha }}
jobs:
# Job 1:CI — 跑測試和檢查
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm test
# Job 2:建構 Docker Image(只有在 CI 成功後才執行)
build:
needs: ci # 依賴 ci job
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=gha # 使用 GitHub Actions cache 加速建構
cache-to: type=gha,mode=max
# Job 3:部署到伺服器
deploy:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Deploy to server via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
# 告訴伺服器 pull 最新 Image
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.TAG }}
# 用新 Image 重啟服務(使用 docker compose)
cd /app/my-api
docker compose pull
docker compose up -d --force-recreate
# 清理舊的 Image 節省空間
docker image prune -f
部署到正式環境的 SSH key 設定
上述 workflow 中的 secrets.SERVER_SSH_KEY 需要在 GitHub 設定:
- 在 GitHub 專案頁面,進入 Settings → Secrets and variables → Actions
- 點 New repository secret,新增:
SERVER_HOST:你的伺服器 IP 或網域SERVER_USER:SSH 登入的使用者名稱SERVER_SSH_KEY:你的 private SSH key(建議建立專門用於部署的 key,不要用個人 key)
使用 Environment 區分環境
真實專案會區分不同環境(dev、staging、production)。用 GitHub Environments 管理:
多 Environment 部署流程
flowchart LR
subgraph Build["📦 Build Stage"]
B1[Checkout Code] --> B2[Install Dependencies]
B2 --> B3[TypeScript Check]
B3 --> B4[Run Tests]
B4 --> B5[Build Docker Image]
B5 --> B6[Push to GHCR]
end
subgraph Deploy["🚀 Deploy Stage"]
D1{Is Main Branch?}
D1 -->|Yes| D2[Deploy to Staging]
D2 --> D3{Staging OK?}
D3 -->|Pass| D4[✅ Ready for Production]
D3 -->|Fail| D5[🚨 Alert Team]
D1 -->|No| D6[Deploy to Dev Preview]
D6 --> D7[🔍 Developer Reviews]
D7 --> D8{Approved?}
D8 -->|Yes| D4
D8 -->|No| D9[⛔ Reject]
end
B6 --> Deploy
style D4 fill:#22c55e,color:#fff
style D5 fill:#ef4444,color:#fff
style D9 fill:#ef4444,color:#fff
上圖說明:main 分支的變更會先部署到 staging 驗證,通過後才準備進入 production;其他分支則部署到 Dev Preview 環境供開發者審查。
jobs:
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging # 需要在 GitHub Settings → Environments 設定這個環境
steps:
- name: Deploy to staging
run: echo "Deploying to staging"
deploy-production:
needs: build
runs-on: ubuntu-latest
environment: production # 設定 require review,需要人工核准才能部署
steps:
- name: Deploy to production
run: echo "Deploying to production"
在 GitHub → Settings → Environments 可以設定:
- Required reviewers:部署前需要特定人員核准
- Environment variables:該環境專用的 secret
- Deployment branch rules:只有特定分支能部署到該環境
常見問題
Q:GitHub Actions 的免費額度用完怎麼辦?
A:GitHub Free 方案有 2000 分鐘/月的 macOS runner,Linux 和 Windows 各 2000 分鐘。公開 repository 完全免費。私人 repository 超過額度後,會暫停 workflow 或需要付費。開源專案建議設為 public,runner 分钟数无限制。
Q:workflow 失敗了怎麼除錯?
A:可以在失敗的 workflow 頁面點 Re-run all jobs,或用 Re-run jobs with SSH 在 runner 裡開一個 SSH session 進去實際查看問題。日常除錯建議先用 runs-on: ubuntu-latest 的 runner,本地測試用 act(GitHub Actions local runner)工具。
Q:可以只部署特定的 commit 或 tag 嗎?
A:可以。在 on.push 設定 conditions:
on:
push:
branches: [main]
tags:
- 'v*' # 只在 push tag 時觸發
Q:workflow 觸發太頻繁會佔用太多 minutes 嗎?
A:可以加上路徑過濾,避免無關檔案變動也觸發 workflow:
on:
push:
paths:
- 'src/**' # 只有 src/ 下的變動才觸發
- 'package.json'
- '.github/workflows/**'
總結:讓機器做機器擅長的事
CI/CD 的核心價值:把重複的、容易出錯的、沒有技術價值的部署工作交給機器,工程師把時間放在真正需要人的地方——寫 code、做判斷、除錯、優化。
這篇文章涵蓋的範圍:
- CI/CD 基本概念(CI 與 CD 的區別)
- GitHub Actions 基本結構(workflow、job、step、action)
- 完整的 CI/CD workflow(測試 → 建構 Image → 部署到伺服器)
- GitHub Environments 與環境區分
- secrets 管理與 SSH 部署
下一步:把前一篇 Docker + Express 做的 API 專案接上 GitHub Actions,親手體驗「push 到 main 分支後,幾分鐘內看到正式機上的服務自動更新」的成就感。
本篇是「前端工程師後端入門」系列第五篇。系列前四篇:《從 Node.js 原生 HTTP 到 Express 框架》、《RESTful API 設計:前端視角的 API 思維》、《Prisma ORM:前端工程師的第一堂資料庫課》、《Docker 部署入門:把你的應用打包上線》。