~/blog/wordpress-zero-downtime-ci-cd-github-actions.md
Laravel 與後端開發 · 2025 / 09 / 15

零停機部署 WordPress:GitHub Actions 進階 CI/CD 流水線實戰

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
零停機部署 WordPress:GitHub Actions 進階 CI/CD 流水線實戰
目錄 table-of-contents.md

「欸,我剛剛看到網站好像破版了,一下下又好了,怎麼回事?」電話那頭客戶的這句話,比任何 bug 回報都讓人心驚——因為那多半代表部署的瞬間被使用者撞個正著。檔案只傳到一半的網站,等於一個半成品在線上裸奔。這篇用 GitHub Actions 打造 WordPress 零停機部署流水線,讓每一次上版都無聲無息地完成。

這「一下下」的瞬間,通常就是我們正在用傳統 FTP 更新檔案的尷尬時刻。檔案一個個上傳,新舊檔案交替,網站就像在手術台上開膛剖肚,使用者一不小心就看到了血淋淋的半成品。這種體驗,不僅不專業,更有可能在關鍵時刻嚇跑你的潛在客戶。

很多開發者可能已經看過我們另一篇 GitHub Actions 的入門文章,學會了如何自動化上傳檔案。但今天,我們要談的是進階戰術:如何打造一個「零停機」(Zero-Downtime)的 CI/CD 自動部署流水線。這不只是把檔案丟上伺服器那麼簡單,而是一套確保網站更新過程「如絲般滑順」,使用者完全無感的專業作法。準備好了嗎?讓我們把手弄髒,開始搞定這個工程師的浪漫吧!

為何『手動 FTP』是專業團隊的惡夢?

在我們深入探討解決方案之前,我想先囉嗦幾句,讓我們再次正視傳統 FTP/SFTP 手動部署的根本問題。這不只是效率問題,更是風險管理的問題。

  • 更新過程中的「空窗期」: 當你上傳檔案時,尤其是檔案數量很多的時候,伺服器上的程式碼會處於一個「半新半舊」的尷尬狀態。如果使用者剛好在這幾秒或幾十秒內瀏覽網站,很可能會看到 PHP 錯誤、樣式錯亂,甚至是致命的「死亡白畫面」。
  • 人為失誤的無限可能: 你有沒有忘記上傳某個關鍵檔案的經驗?或者,上傳到錯誤的資料夾?(別騙我,我們都做過)。手動操作,就意味著失誤的機率永遠存在,而且通常發生在最不想發生的時候,比如週五下班前。
  • 缺乏版本控制與回滾機制: 如果更新後發現重大問題,你該怎麼辦?再手動把舊版本的檔案一個個傳回去嗎?這過程不僅慢,而且壓力山大。一個專業的部署流程,應該要能在一瞬間就將網站恢復到上一個穩定版本。

所以,我們追求的 CI/CD 自動部署(GitHub Actions),目標不僅僅是「快」,更是「穩」與「可靠」。

CI/CD 的真正威力:原子部署 (Atomic Deployment)

「原子部署」聽起來很高科技,但概念其實很單純。在化學中,「原子」是不可再分割的最小單位;在部署的世界裡,「原子部署」意味著整個更新過程是一個「不可分割的操作」。要嘛就是 100% 成功,要嘛就是完全沒發生,不會有中間狀態。

它是如何運作的?

想像一下你的伺服器結構,傳統作法是直接覆寫 /var/www/html 裡的檔案。而原子部署則是這樣玩的:

  1. 建立新的發佈目錄: 我們的自動化腳本不會直接碰線上正在運行的程式碼。它會在旁邊建立一個新的、以時間戳或 Git Commit HASH 命名的資料夾,例如 /var/www/releases/20250915113000/
  2. 同步最新程式碼: 將 GitHub 上的最新程式碼完整地同步到這個新的發佈目錄中。
  3. 切換符號連結 (Symbolic Link): 這是最關鍵的一步。你網站的根目錄(例如 /var/www/html)其實不是一個真正的資料夾,而是一個指向某個發佈目錄的「捷徑」(符號連結)。當新版本的程式碼準備就緒後,我們只需要一個指令,將這個捷徑從指向舊的發佈目錄(/var/www/releases/old_version/)瞬間切換到新的發佈目錄(/var/www/releases/20250915113000/)。
  4. 清理舊版本: 確認新版本運行正常後,自動化腳本可以刪除幾個版本之前的舊發佈目錄,釋放伺服器空間。

整個切換過程只在毫秒之間,使用者完全感受不到任何中斷。如果新版本有問題,回滾也只是把符號連結指回上一個版本的目錄而已,同樣是瞬間完成。這才是企業級的部署策略!

GitHub Actions 實戰:打造你的零停機部署流水線

理論說完了,來點實際的。接下來,我會帶你一步步設定 GitHub Actions,實現上面提到的原子部署。我們這次不用 FTP Action,改用更強大、更靈活的 SSH 搭配 rsync。

Step 1: 準備工作 - SSH 金鑰與 Repository Secrets

首先,我們需要讓 GitHub Actions 能夠免密碼登入你的伺服器。

  1. 產生 SSH 金鑰對: 在你的本機電腦上執行 ssh-keygen -t rsa -b 4096 -C "your_email@example.com"。這會產生 id_rsa(私鑰)和 id_rsa.pub(公鑰)兩個檔案。
  2. 在伺服器上設定公鑰:id_rsa.pub 檔案的內容,複製到你伺服器上 ~/.ssh/authorized_keys 檔案中。
  3. 在 GitHub 設定 Secrets: 進入你的專案 GitHub Repo > Settings > Secrets and variables > Actions。點擊 "New repository secret",建立以下幾個 Secrets:
    • SSH_PRIVATE_KEY:貼上你本機 id_rsa 檔案的完整內容。
    • SSH_HOST:你的伺服器 IP 或網域。
    • SSH_USER:你登入伺服器的使用者名稱(例如:ubuntu)。
    • DEPLOY_PATH:部署的根目錄,例如 /var/www/my-wordpress-site

工程師的小囉嗦: 千萬不要把私鑰直接寫在 YAML 檔案裡,這等於是把家裡鑰匙掛在門口。使用 GitHub Secrets 是唯一安全的方式。

Step 2: 編寫你的 Workflow YAML 檔案

在你的專案根目錄下建立 .github/workflows/deploy.yml 檔案,並貼上以下內容。這份設定檔包含了編譯前端資源(如果需要)以及執行原子部署的完整邏輯。

name: Deploy WordPress with Zero Downtime

on:
  push:
    branches:
      - main # 當 main 分支有更新時觸發

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: 1. Checkout Code
      uses: actions/checkout@v3

    - name: 2. Setup PHP for Composer
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        extensions: mbstring, zip
        tools: composer

    - name: 3. Install Composer Dependencies
      run: composer install --prefer-dist --no-progress --no-dev

    - name: 4. Setup Node.js for Asset Compilation
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'

    - name: 5. Install NPM Dependencies
      run: npm ci

    - name: 6. Build Frontend Assets
      run: npm run build

    - name: 7. Deploy to Server with Atomic Swap
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          set -e # 如果任何指令失敗,就立刻停止

          # 定義變數
          DEPLOY_PATH=${{ secrets.DEPLOY_PATH }}
          RELEASE_DIR="$DEPLOY_PATH/releases/$(date +%Y%m%d%H%M%S)"
          CURRENT_SYMLINK="$DEPLOY_PATH/current"

          echo "Deploying to $RELEASE_DIR"

          # 建立新的 release 資料夾
          mkdir -p $RELEASE_DIR

          # 使用 rsync 同步檔案(排除 .git 和 node_modules)
          rsync -avz --exclude='.git' --exclude='node_modules' ./ $RELEASE_DIR/

          # 處理共享檔案/資料夾,例如 wp-config.php 和 uploads
          # 假設 shared 資料夾已經在 DEPLOY_PATH 中建立好
          ln -nfs $DEPLOY_PATH/shared/wp-config.php $RELEASE_DIR/wp-config.php
          ln -nfs $DEPLOY_PATH/shared/uploads $RELEASE_DIR/wp-content/uploads

          # 原子性地切換符號連結
          ln -nfs $RELEASE_DIR $CURRENT_SYMLINK

          echo "Deployment successful! New release is live."

          # 清理舊的 releases (保留最近的 5 個)
          cd $DEPLOY_PATH/releases && ls -t | tail -n +6 | xargs -r rm -rf

Step 3: 拆解 Workflow - 這到底在幹嘛?

讓我們一步步解析這份 YAML 檔:

  • 步骤 1-3: 處理後端依賴: 我們先 Checkout 程式碼,然後設定好 PHP 環境,並執行 composer install。這對於有使用 Composer 管理 PHP 套件的現代 WordPress 開發流程非常重要。
  • 步骤 4-6: 編譯前端資源: 接著設定 Node.js 環境,安裝 npm 依賴,並執行 npm run build。這會將你的 SASS/SCSS、TypeScript/ES6+ 等原始碼編譯成瀏覽器看得懂的 CSS 和 JavaScript。部署到伺服器的應該是編譯後的結果,而不是原始碼。
  • 步骤 7: 部署魔法: 這是原子部署的核心。
    • set -e: 這是一個保險,確保腳本中任何一個指令失敗,整個部署過程就會中止,不會執行到一半。
    • 變數定義: 我們定義了部署路徑、新的 release 資料夾名稱(用時間戳命名確保唯一性)、以及符號連結的路徑。
    • rsync 同步: rsync 是個比 scp 更聰明的同步工具,它只會傳輸有變動的檔案,速度更快。我們也排除了不需要上傳的 .gitnode_modules 資料夾。
    • 共享檔案處理:wp-config.phpuploads 資料夾這種每個版本都共用、且不應被版本控制的內容,我們將它們放在一個 shared 資料夾中,然後用符號連結連到新的 release 目錄裡。
    • 原子切換: ln -nfs $RELEASE_DIR $CURRENT_SYMLINK 這一行指令就是整個流程的精髓,它原子性地將 current 這個連結指向我們剛準備好的新版本。
    • 清理舊版: 最後,一個簡單的指令幫我們保留最近的 5 個版本,自動刪除更舊的,避免佔用伺服器空間。

進階加碼:整合 WP-CLI 與程式碼品質檢查

一個真正完整的 CI/CD 流程還能做得更多。例如,在部署完成後自動執行資料庫更新。

你可以在 SSH script 的最後,加入 WP-CLI 指令:

# ...接續上面的 script...

# 原子性地切換符號連結
ln -nfs $RELEASE_DIR $CURRENT_SYMLINK

# 執行資料庫更新與快取清除
cd $CURRENT_SYMLINK
wp db upgrade
wp cache flush

echo "Deployment successful! New release is live."

# ...後續清理...

這能確保你的外掛或佈景主題需要的資料庫變動,在程式碼上線的同時就立刻生效。你甚至可以在部署前加入一個步驟,執行 PHP CodeSniffer (phpcs) 來檢查程式碼是否符合 WordPress 編碼標準,如果不符合,就直接中斷部署!這能從源頭杜絕不合格的程式碼上線。

搞定!現在開始,你只要 git pushmain 分支,GitHub Actions 就會為你執行這一整套專業、安全、零停機的部署流程。你可以泡杯咖啡,看著 Actions 的綠色勾勾亮起,然後自信地告訴客戶:「網站已更新完成!」再也不用在 FTP 的上傳進度條前膽戰心驚了。

這套流程不僅提升了你的工作效率,更重要的是,它代表了一種專業和對品質的承諾。當你的開發流程越來越穩固,你才能真正專注在創造價值,而不是處理各種部署意外。

當然,CI/CD 的世界博大精深,從多環境部署(Staging、Production)、自動化測試到容器化部署,還有很多可以探索的。如果你的團隊也想導入這樣現代化的開發流程,卻不知從何下手,或是遇到了更複雜的架構問題,歡迎與浪花科技的團隊聊聊。我們樂於分享我們的經驗,協助你打造更強健的數位產品。

延伸閱讀

// FAQ

常見問題

什麼是原子部署(Atomic Deployment)?
原子部署是指整個更新過程是不可分割的操作,要嘛 100% 成功上線,要嘛完全沒發生,不會出現新舊檔案混雜的中間狀態。它的做法是先把新版程式碼同步到一個全新的發佈目錄,待就緒後再用符號連結(symlink)瞬間切換網站根目錄的指向,因此切換只在毫秒之間完成,使用者完全無感。
為什麼手動 FTP 部署會造成網站短暫破版?
用 FTP 逐一上傳檔案時,伺服器上的程式碼會處於「半新半舊」的狀態,尤其檔案多時這個空窗期可能長達數秒到數十秒。若使用者剛好在此期間瀏覽,可能會看到 PHP 錯誤、樣式錯亂甚至白畫面。手動操作也容易漏傳檔案或上傳到錯誤目錄,且缺乏快速回滾機制。
原子部署如何達成快速回滾?
因為網站根目錄是一個指向某個發佈目錄的符號連結,回滾時只要把這個符號連結重新指回上一個穩定版本的發佈目錄即可,同樣是瞬間完成。前提是部署時保留前幾個版本的發佈目錄(例如保留最近 5 個),確認新版正常後才清理更舊的版本。
GitHub Actions 部署的 SSH 私鑰應該放在哪裡?
私鑰絕對不能直接寫在 workflow 的 YAML 檔案裡。應該存放在 GitHub Repository 的 Secrets(Settings > Secrets and variables > Actions),例如建立 SSH_PRIVATE_KEY、SSH_HOST、SSH_USER、DEPLOY_PATH 等 Secret,再於 workflow 中以 ${{ secrets.XXX }} 引用,這是唯一安全的做法。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。

$
// final.exec()

準備好讓你的網站開始為你工作了嗎?