使用自己的伺服器部署服務的情況下,利用GitHub做CI/CD有很多方式,例如使用定期執行每隔一段時間就上GitHub看有沒有新版本要編譯部署、透過Webhooks由GitHub通知伺服器上來拿最新的程式碼編譯部署,本文使用GitHub Actions,在儲存庫更新、收到Pull Request等情況下由GitHub Actions自動執行Workflow,自動打包原始碼、編譯、上傳到遠端伺服器、呼叫遠端腳本重啟服務。

自動化部署

本教學為在Linux Debian環境下,部署一個透過bash script控制systemd啟動Java spring boot專案,實務上可以在腳本中換成python、nodejs、docker等任何方案。

Linux基本環境

這邊是在Linux上要先做的基本操作,讓GitHub可以以最小權限原則SSH進到我們伺服器。

建立帳號

登入到測試環境的Linux上,透過以下指令建立Actions使用的帳號 github-actions,可以修改成其他名稱,但是後續教學中的指令全部要自行更改成一樣的名稱。

sudo adduser --disabled-password --gecos "" github-actions

建立SSH Key

首先要建立一組SSH Key,可以在自己的電腦上,或是有ssh環境的電腦上執行以下操作,或是透過putty等圖形界面軟體建立SSH Key。

ssh-keygen -t ed25519 -C "github-actions-deploy" -f id_ed25519_github

執行完會在~/.ssh/底下得到兩個檔案:

  • id_ed25519_github (私鑰):絕對不可外流!這是給 GitHub Actions 拿在手上的。
  • id_ed25519_github.pub (公鑰):這是給伺服器看的。

回到遠端的Linux伺服器上。

# 切換到github-actions帳號
sudo -u github-actions -i

# 建立SSH Key登入設定
mkdir ~/.ssh
chmod 700 .ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

# 將剛才的公鑰內容填入
echo '這邊填入你的公鑰內容' >>  ~/.ssh/authorized_keys

安全強化:限制 SSH 執行範圍(可選)

為了更安全,可以限制這個 SSH Key 只能執行特定的行為。在 .ssh/authorized_keys 中,在公鑰前面加上限制。

no-port-forwarding,no-agent-forwarding,no-x11-forwarding,no-pty ssh-rsa AAAAB... (公鑰)

如果佈署指令需要用到 pty (虛擬終端),則不要加 no-pty

服務運行環境

這邊是每建立一個新服務,就要在Linux上做一次的事情。本文中範例的專案名稱叫做 klabtw,因為是測試環境,所以這個服務我稱為 klabtw-test

建立工作目錄

sudo mkdir -p /opt/klabtw-test/{app,cache}

建立啟動腳本

建立一個檔案 /opt/klabtw-test/start-service.sh 填寫以下內容,如果不是使用Java,也可以自行換成其他服務。

#!/bin/bash

APP_NAME="klabtw-test"
WORK_DIR="/opt/$APP_NAME"

# 這邊填入你想指定的Java vm args
JAVA_OPTS="-Xmx1024m -Xms1024m \
  -Dspring.profiles.active=test"

# 使用exec執行,避免程式脫離Systemd控制
exec /usr/bin/java $JAVA_OPTS -jar $WORK_DIR/app/$APP_NAME.jar

建立服務單元

使用root權限建立檔案 /etc/systemd/system/klabtw-test.service

[Unit]
Description=Klabtw Service (test)
After=syslog.target

[Service]
Restart=always
RestartSec=10

User=github-actions
Group=github-actions

WorkingDirectory=/opt/klabtw-test
ExecStart=/bin/bash /opt/klabtw-test/start-service.sh

[Install]
WantedBy=multi-user.target

建立部署與重啟腳本

編輯 /opt/klabtw-test/deploy-and-restart.sh 檔案。

#!/bin/bash

APP_NAME="klabtw-test"

# 定義路徑
BASE_DIR="/opt/$APP_NAME"
CACHE_JAR="$BASE_DIR/cache/$APP_NAME.jar"
APP_JAR="$BASE_DIR/app/$APP_NAME.jar"

echo "[$(date +'%Y-%m-%d %H:%M:%S')] 開始執行部署流程..."

# 1. 檢查快取檔案是否存在
if [ ! -f "$CACHE_JAR" ]; then
    echo "錯誤: 找不到快取檔案 $CACHE_JAR"
    exit 1
fi

# 2. 搬移檔案
echo "正在搬移 JAR 檔至 app 目錄..."
mv "$CACHE_JAR" "$APP_JAR"

# 3. 重啟 Systemd 服務
echo "正在重啟 $APP_NAME 服務..."
sudo systemctl restart $APP_NAME

# 4. 驗證結果
sleep 2 # 給服務一點反應時間
if systemctl is-active --quiet $APP_NAME; then
    echo "部署成功!服務目前狀態:ACTIVE"
    systemctl status $APP_NAME --no-pager
else
    echo "部署失敗!請檢查日誌"
    exit 1
fi

設定Sudo免密碼權限

自動化腳本需要重啟 Spring 服務(或是你指定的其他服務),但我們不希望給這個帳號完整的 root 權限,也不希望它在執行systemctl時被要求輸入密碼。

sudo visudo -f /etc/sudoers.d/github-actions

輸入以下內容,然後按下 CTRL + X 再按下 Y 存檔。

# 允許 github-actions 使用者免密碼重啟特定的 systemd 服務
github-actions ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart klabtw-test, /usr/bin/systemctl status klabtw-test

依照需求可以加上以下指令,每個指令用半形逗號隔開。

  • /usr/bin/systemctl start klabtw-test
  • /usr/bin/systemctl stop klabtw-test

Github Actions環境

建立Repository

首先要有一個GitHub的儲存庫,放在個人或是組織底下都可以,程式碼跟Actions必須用同一個儲存庫,所以已經在GitHub進行版本控制的人可以直接進下一步。

設定Secrets

這一步要設定GitHub

進入我們建立好、或是已經在使用的儲存庫點選 Settings 頁籤,然後左側導覽列選擇 Secrets and variables 目錄,展開後點選 Actions。然後在 Repository secrets 那邊點選 New secret,重複三次新增以下三組資料。

NameSecret說明
REMOTE_HOST0.0.0.0你的Linux伺服器IP
REMOTE_USERgithub-actions給GitHub Actions登入的帳號
SSH_PRIVATE_KEY—–BEGIN OPENSSH PRIVATE KEY—–
abcd…
—–END OPENSSH PRIVATE KEY—–
id_ed25519_github (私鑰)

填寫Workflow

在我的需求中希望branch main更新就自動部署測試機,或者有不是Draft的PR更新也要自動部署。可以依照需求改成「只有main更新才觸發」、「Draft PR也要觸發」、「只有包含tag的main更新才要觸發(適合正式環境)」等各種功能,這種細項的調整沒辦法一個個列出來,所以我這邊提供基本概念說明GitHub Actions可以做什麼事情,細節可以交給AI幫忙寫或是修改。

name: Build and Deploy Spring App

on:
  push:
    branches:
      - main
  pull_request:
    # 必須包含 ready_for_review,按按鈕時才會啟動
    types: [opened, synchronize, reopened, ready_for_review]

jobs:
  build-and-deploy:
    # 核心邏輯:如果是直接 push 到 main,或是 PR 狀態「不是」草稿
    if: |
      github.event_name == 'push' || 
      (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
    
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'gradle'

      - name: Build with Gradle
        run: |
          chmod +x gradlew
          ./gradlew clean bootJar

      - name: Rename JAR
        run: |
          # 尋找產出的 jar 並統一命名,方便後續腳本抓取
          mv build/libs/*.jar build/libs/klabtw-test.jar

      - name: SCP to Debian Server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: "build/libs/klabtw-test.jar"
          target: "/opt/klabtw-test/cache"
          strip_components: 2
      
      - name: Execute Remote Deploy Script
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          # 這裡只執行一支腳本
          script: bash /opt/klabtw-test/deploy-and-restart.sh

測試

完成以上操作就可以在GitHub的Repository的Actions頁籤看到自動執行Workflow的進度與歷史訊息了,我們在 deploy-and-restart.sh 輸出的訊息也可以在這邊看見,如果執行失敗也可以點進去觀看原因。