2025 部落格 CI/CD 部署實戰與心得

這篇紀錄我把這個用 Hexo 建置的部落格從「自寫 Webhook」轉換到 GitLab CI/CD,以及中間 Nginx / Certbot 踩雷的調整心得。

部署方式的回顧

  • 一開始是用自己寫的 Webhook(Node.js + Express):

    • 收到 GitLab Push 事件 → 直接 git pull && hexo generate
    • 簡單、快速上線,但 安全性與可維護性差
  • 後來切換成 GitLab CI/CD

    • 使用 node:20-alpine 容器建置 → npx hexo generate
    • 部署時用 rsync --backup,並保留最近 10 次 備份。
    • deploy 使用者只擁有對 /usr/share/nginx/html/myblog 的權限,安全許多。
  • Nginx server block 踩雷:

    • 曾經 root 指到 /myblog/public,導致頁面只出現文字、樣式 404。
    • 最後拆成 /etc/nginx/conf.d/hazel.style.conf,domain 與 root 一一對應。
  • Certbot 續期問題:

    • 因為 flex.hazel.style 沒有 A 記錄,續期失敗。
    • 解法是重新簽發時 移除不存在的子網域,只保留 hazel.stylewww.hazel.style

GitLab CI/CD Pipeline 設定

這次的核心是 .gitlab-ci.yml,分成兩個 stage:

  1. build → 負責在乾淨的 Node 環境產生 Hexo 靜態檔。
  2. deploy → 用 SSH + rsync 部署到伺服器,並自動保留備份。

gitlab-ci.yaml 如下:

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
stages: [build, deploy]

workflow:
rules:
- if: $CI_COMMIT_BRANCH == "master"

cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/

# 1) Build:產生 Hexo 靜態檔
build:
stage: build
image: node:20-alpine
variables:
HEXO_ENV: production
script:
- npm install
- npx hexo clean
- npx hexo generate
artifacts:
name: "hexo-public-$CI_COMMIT_SHORT_SHA"
expire_in: 7 days
paths:
- public/

# 2) Deploy:透過 SSH + rsync 備份後部署
deploy:
stage: deploy
image: alpine:3.20
needs: ["build"]
environment:
name: production
url: https://hazel.style
before_script:
- apk add --no-cache openssh-client rsync coreutils
- echo "$SSH_PRIVATE_KEY" | base64 -d > id_rsa
- chmod 600 id_rsa
- eval $(ssh-agent -s)
- ssh-add id_rsa
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts
script:
- export DEPLOY_PATH="${DEPLOY_PATH:-/usr/share/nginx/html/myblog}"
- ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "mkdir -p /usr/share/nginx/html/.trash"
- rsync -avz --delete --backup \
--backup-dir="/usr/share/nginx/html/.trash/$CI_PIPELINE_ID" \
-e "ssh -p $SSH_PORT -i id_rsa" \
public/ "$SSH_USER@$SSH_HOST:$DEPLOY_PATH/"
# 保留最近 10 次備份
- ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
"cd /usr/share/nginx/html/.trash && ls -1t | tail -n +11 | xargs rm -rf"
rules:
- if: $CI_COMMIT_BRANCH == "master"

設定細節說明

  1. Artifacts: build job 產生的 /public 會存成 artifacts,給 deploy job 用。

  2. SSH Key: 金鑰存在 GitLab Variables,Pipeline 裡還原,再用 ssh-agent 加入。

  3. rsync 備份機制: 每次 deploy 都會把舊版放進 .trash/$CI_PIPELINE_ID。
    自動刪掉多於 10 份,避免磁碟爆滿。

  4. 安全性: deploy 使用者只管理 /usr/share/nginx/html/myblog 的內容,權限分明。

Webhook vs. CI/CD

面向 自寫 Webhook GitLab CI/CD
安全性 無驗證,任何人可打;若 root 執行 = 高風險 部署帳號最小權限,金鑰存在 CI Variables
穩定性 沒鎖、沒佇列,併發可能互相覆蓋 pipeline 階段分明,job 可重試
可重現性 VM 直接跑,Node/套件版本易漂移 容器化環境固定 node 版本,產出穩定
回滾備份 沒有 → 出事只能手動復原 rsync --backup + .trash,自動保留 10 份
可觀測性 全靠 console.log GitLab job logs,可追蹤歷史 pipeline
成本與便利 簡單快速上線 前期需設定,但長期維護更安心

改動成果

  • 成功用 rsync 做出 部署即備份,只保留 10 個版本,減少磁碟占用。
  • Nginx 設定拆分,解決 conflicting server name
  • Certbot 續期確認通過,renew --dry-run 綠燈。
  • 部署流程從「SSH 上伺服器手動測」→「自動化 CI/CD」,效率提升。

後續規劃

✏️ 完善 GitLab CI/CD,增加「一鍵回滾到上一個 pipeline」的 script。
✏️ 部署後加上自動化 smoke test(檢查首頁 / CSS / JS 是否正常)。
✏️ 加上即時翻譯套件 ✅