【GitHub Actions CD 實戰】#04 SSH 自動部署:讓 GitHub Actions 操作你的主機

測驗:SSH 自動部署 – 讓 GitHub Actions 操作你的主機

共 5 題,點選答案後會立即顯示結果

1. 為什麼自動化部署建議使用 SSH 金鑰認證而非密碼認證?

  • A. 密碼認證的連線速度比較慢
  • B. 自動化流程無法輸入密碼,金鑰認證是標準做法
  • C. 密碼認證需要額外安裝套件
  • D. 金鑰認證可以同時連線多台主機

2. 執行 ssh-keygen -t ed25519 後會產生哪兩個檔案?

  • A. public.key 和 private.key
  • B. id_rsa 和 id_rsa.pub
  • C. 私鑰檔案(無副檔名)和公鑰檔案(.pub 結尾)
  • D. ssh_key.pem 和 ssh_key.crt

3. 在金鑰認證的設定中,公鑰和私鑰分別應該放在哪裡?

  • A. 公鑰放 GitHub Secrets,私鑰放主機
  • B. 公鑰放主機的 authorized_keys,私鑰放 GitHub Secrets
  • C. 公鑰和私鑰都放 GitHub Secrets
  • D. 公鑰和私鑰都放主機上

4. 在部署腳本中,docker stop my-app || true|| true 有什麼作用?

  • A. 強制停止容器,即使有程序正在執行
  • B. 只有當 docker stop 成功時才繼續執行
  • C. 設定 docker stop 的超時時間為 true
  • D. 讓指令不會因為容器不存在而失敗,確保後續指令能繼續執行

5. 在 GitHub Actions workflow 中,needs: build 這個設定代表什麼意思?

  • A. 這個 job 要等 build job 成功完成後才會執行
  • B. 這個 job 需要 build 目錄中的檔案
  • C. 這個 job 會與 build job 同時執行
  • D. 這個 job 會在 build job 失敗時執行

一句話說明

讓 GitHub Actions 透過 SSH 連到你的主機,自動執行部署指令。

這篇要解決的問題

前幾篇我們學會了建立 Docker Image 並推送到 Registry。但 Image 在 Registry 裡不會自己跑起來——你需要「有人」登入主機,拉取 Image,然後啟動容器。

這個「有人」可以是你自己手動 SSH 進去操作,也可以是 GitHub Actions 自動幫你做。

本篇教你後者。


前置條件

在開始之前,確認你有:

  • 一台可 SSH 連線的主機(VPS、自架伺服器)
  • 主機已安裝 Docker
  • 主機可以存取你的 Docker Registry(Docker Hub 或私有 Registry)

核心概念:SSH 金鑰認證

為什麼需要金鑰?

GitHub Actions 要連到你的主機,需要證明「我有權限連」。有兩種方式:

方式 說明 適合場景
密碼認證 輸入使用者密碼 不建議用於自動化
金鑰認證 用私鑰證明身份 自動化部署標準做法

金鑰認證的原理:

  1. 你產生一對金鑰:私鑰(秘密)和 公鑰(可公開)
  2. 把公鑰放到主機上
  3. 把私鑰存到 GitHub Secrets
  4. Actions 用私鑰連線,主機用公鑰驗證

步驟 1:產生 SSH 金鑰對

在你的本機執行:

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_actions_deploy
Code language: JavaScript (javascript)

逐段翻譯

ssh-keygen           # 產生 SSH 金鑰的指令
-t ed25519           # 使用 ed25519 演算法(比 RSA 更安全、更短)
-C "github-actions-deploy"  # 加個註解,方便識別這把金鑰的用途
-f ~/.ssh/github_actions_deploy  # 指定檔名,不要用預設的 id_ed25519
Code language: PHP (php)

執行後會產生兩個檔案:

  • ~/.ssh/githubactionsdeploy:私鑰(給 GitHub Actions 用)
  • ~/.ssh/githubactionsdeploy.pub:公鑰(放到主機上)

重要:當詢問 passphrase 時,直接按 Enter 留空。自動化流程無法輸入密碼。


步驟 2:設定主機接受這把金鑰

SSH 進入你的部署主機:

ssh user@your-server.com
Code language: CSS (css)

把公鑰加入 authorized_keys

# 在主機上執行
echo "你的公鑰內容" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Code language: PHP (php)

取得公鑰內容

在你的本機執行:

cat ~/.ssh/github_actions_deploy.pub
Code language: JavaScript (javascript)

會輸出類似這樣的內容:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIxxxxxx github-actions-deploy

把整行複製,貼到主機的 authorized_keys 檔案裡。


步驟 3:設定 GitHub Secrets

GitHub Secrets 是存放敏感資訊的地方。Actions 可以讀取,但不會顯示在 log 裡。

需要設定的 Secrets

Secret 名稱 內容 說明
SSH_HOST your-server.com 主機的 IP 或網域
SSH_USER deploy SSH 使用者名稱
SSH_PRIVATE_KEY 私鑰內容 整個檔案內容
SSH_PORT 22 SSH port(如果不是 22)

設定步驟

  1. 到你的 GitHub Repository
  2. Settings > Secrets and variables > Actions
  3. 點 “New repository secret”
  4. 填入名稱和值

取得私鑰內容

cat ~/.ssh/github_actions_deploy
Code language: JavaScript (javascript)

會輸出:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmU...
...
-----END OPENSSH PRIVATE KEY-----
Code language: PHP (php)

整個內容(包含 BEGIN 和 END 那兩行)都要複製。


步驟 4:撰寫 SSH 部署 Workflow

完整範例

name: Deploy to Server

on:
  push:
    branches: [main]
  workflow_dispatch:  # 允許手動觸發

env:
  IMAGE_NAME: your-dockerhub-username/your-app

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            # 拉取最新 Image
            docker pull ${{ env.IMAGE_NAME }}:latest

            # 停止並移除舊容器(如果存在)
            docker stop my-app || true
            docker rm my-app || true

            # 啟動新容器
            docker run -d \
              --name my-app \
              --restart unless-stopped \
              -p 8080:8080 \
              ${{ env.IMAGE_NAME }}:latest

            # 清理舊的 Image(可選)
            docker image prune -f
Code language: PHP (php)

逐段翻譯

uses: appleboy/ssh-action@v1.0.3  # 使用社群維護的 SSH Action
with:
  host: ${{ secrets.SSH_HOST }}   # 從 Secrets 讀取主機位址
  username: ${{ secrets.SSH_USER }}  # SSH 使用者
  key: ${{ secrets.SSH_PRIVATE_KEY }}  # SSH 私鑰
  port: ${{ secrets.SSH_PORT }}   # SSH port
  script: |                       # 要在主機上執行的指令
Code language: PHP (php)
docker stop my-app || true  # 停止容器,|| true 讓指令不會因為容器不存在而失敗
docker rm my-app || true    # 移除容器
Code language: PHP (php)
docker run -d \              # -d 在背景執行
  --name my-app \            # 指定容器名稱
  --restart unless-stopped \ # 除非手動停止,否則自動重啟
  -p 8080:8080 \             # port mapping
  ${{ env.IMAGE_NAME }}:latest
Code language: PHP (php)

常見變化

變化 1:結合 Build 和 Deploy

通常會先 build image,push 成功後才 deploy:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and Push
        run: |
          docker build -t ${{ env.IMAGE_NAME }}:latest .
          docker push ${{ env.IMAGE_NAME }}:latest

  deploy:
    needs: build  # 等 build 完成才執行
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        # ... 同上
Code language: PHP (php)

翻譯needs: build 表示這個 job 要等 build job 成功才會執行。

變化 2:多環境部署

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/develop'
    # ... 部署到測試環境

  deploy-production:
    if: github.ref == 'refs/heads/main'
    # ... 部署到正式環境
Code language: PHP (php)

翻譯:用 if 條件決定什麼分支觸發什麼部署。

變化 3:部署腳本放在主機上

有時候部署邏輯複雜,可以把腳本放在主機上:

script: |
  cd /home/deploy/scripts
  ./deploy.sh ${{ env.IMAGE_NAME }}:latest

翻譯:Actions 只負責「觸發」,實際部署邏輯由主機上的腳本處理。


錯誤處理與回滾策略

基礎版:部署失敗通知

- name: Deploy via SSH
  id: deploy
  uses: appleboy/ssh-action@v1.0.3
  # ...

- name: Notify on failure
  if: failure()
  run: |
    echo "Deployment failed!"
    # 這裡可以加 Slack/Discord 通知
Code language: PHP (php)

進階版:自動回滾

# 在主機上的部署腳本
#!/bin/bash
set -e  # 任何指令失敗就停止

IMAGE=$1
CONTAINER_NAME="my-app"

# 保存目前運行的 Image 名稱(用於回滾)
CURRENT_IMAGE=$(docker inspect --format='{{.Config.Image}}' $CONTAINER_NAME 2>/dev/null || echo "")

# 嘗試部署
docker pull $IMAGE
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true

if docker run -d --name $CONTAINER_NAME -p 8080:8080 $IMAGE; then
    echo "Deploy success!"
    # 可以加健康檢查
    sleep 10
    if curl -f http://localhost:8080/health; then
        echo "Health check passed!"
    else
        echo "Health check failed, rolling back..."
        docker stop $CONTAINER_NAME
        docker rm $CONTAINER_NAME
        docker run -d --name $CONTAINER_NAME -p 8080:8080 $CURRENT_IMAGE
    fi
else
    echo "Deploy failed, rolling back..."
    docker run -d --name $CONTAINER_NAME -p 8080:8080 $CURRENT_IMAGE
fi
Code language: PHP (php)

逐行翻譯重點

set -e  # 任何指令失敗就停止,不會繼續執行下去
Code language: JavaScript (javascript)
CURRENT_IMAGE=$(docker inspect ... || echo "")
# 取得目前容器用的 Image,如果容器不存在就設為空字串
Code language: PHP (php)
if curl -f http://localhost:8080/health; then
# curl -f 會在 HTTP 錯誤時回傳非零(失敗)
Code language: PHP (php)

安全性檢查點

設定 SSH 部署時,確認以下事項:

  • [ ] 私鑰絕對不能 commit 到 Git
  • [ ] 使用專用的部署帳號,不要用 root
  • [ ] 限制部署帳號權限,只能做部署需要的操作
  • [ ] 定期更換金鑰,尤其是有人離開團隊時
  • [ ] SSH port 不要用預設的 22(可選,增加安全性)

建議的主機設定

在主機上建立專用部署帳號:

# 建立 deploy 使用者
sudo useradd -m -s /bin/bash deploy

# 讓 deploy 可以執行 docker 指令
sudo usermod -aG docker deploy

# 設定 SSH 金鑰
sudo -u deploy mkdir -p /home/deploy/.ssh
sudo -u deploy chmod 700 /home/deploy/.ssh
# 把公鑰加進去
echo "ssh-ed25519 AAA..." | sudo -u deploy tee /home/deploy/.ssh/authorized_keys
sudo -u deploy chmod 600 /home/deploy/.ssh/authorized_keys
Code language: PHP (php)

完整 Workflow 範例:Build + Deploy

結合前幾篇的內容,這是一個完整的 CI/CD workflow:

name: CI/CD Pipeline

on:
  push:
    branches: [main]

env:
  IMAGE_NAME: your-dockerhub-username/your-app

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.IMAGE_NAME }}:latest
            ${{ env.IMAGE_NAME }}:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            docker pull ${{ env.IMAGE_NAME }}:latest
            docker stop my-app || true
            docker rm my-app || true
            docker run -d \
              --name my-app \
              --restart unless-stopped \
              -p 8080:8080 \
              ${{ env.IMAGE_NAME }}:latest
            docker image prune -f
Code language: JavaScript (javascript)

Vibe Coder 檢查點

看到 SSH 部署的 workflow 時確認:

  • [ ] Secrets 有正確設定嗎?(SSHHOST, SSHUSER, SSHPRIVATEKEY)
  • [ ] 私鑰有包含完整的 BEGIN 和 END 行嗎?
  • [ ] 主機上的 authorized_keys 有加入對應的公鑰嗎?
  • [ ] 部署腳本有處理「容器不存在」的情況嗎?(用 || true
  • [ ] 有考慮部署失敗的回滾策略嗎?

常見問題

Q: 連線失敗 “Permission denied”

檢查順序:

  1. 私鑰內容是否完整(包含 BEGIN/END 行)
  2. 主機的 authorized_keys 是否有對應的公鑰
  3. authorized_keys 的權限是否是 600
  4. .ssh 目錄的權限是否是 700

Q: 容器啟動了但連不上

檢查順序:

  1. Port mapping 是否正確
  2. 防火牆是否有開放對應的 port
  3. 容器內的應用程式是否正常啟動(用 docker logs my-app 查看)

Q: 如何測試 SSH 連線?

可以在 workflow 裡先測試連線:

script: |
  echo "Connected successfully!"
  whoami
  docker --version
Code language: PHP (php)

小結

本篇你學會了:

  1. 產生 SSH 金鑰對:用 ssh-keygen -t ed25519 產生
  2. 設定 GitHub Secrets:安全儲存 SSH 連線資訊
  3. 使用 appleboy/ssh-action:在 workflow 裡執行 SSH 指令
  4. 撰寫部署腳本:拉取 Image、停止舊容器、啟動新容器
  5. 錯誤處理:用 || true 處理容器不存在的情況,考慮回滾策略

下一篇,我們會介紹如何讓這個流程更完善:加入健康檢查、部署通知、以及更進階的部署策略。

進階測驗:SSH 自動部署 – 讓 GitHub Actions 操作你的主機

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你要為團隊設定 GitHub Actions 自動部署,需要產生 SSH 金鑰。團隊規定 passphrase 不能留空以增加安全性。這個設定可行嗎? 情境題

  • A. 可行,passphrase 可以一起存在 GitHub Secrets
  • B. 可行,appleboy/ssh-action 會自動處理 passphrase
  • C. 不可行,自動化流程無法輸入 passphrase,必須留空
  • D. 不可行,ed25519 演算法不支援 passphrase

2. 小明設定完 SSH 部署後執行 workflow,但看到 “Permission denied” 錯誤。以下是他的設定步驟,哪個最可能是問題所在? 錯誤診斷

1. 產生金鑰:ssh-keygen -t ed25519 -f ~/.ssh/deploy_key 2. 複製私鑰內容到 GitHub Secrets(SSH_PRIVATE_KEY) 3. 在主機建立 ~/.ssh/authorized_keys 檔案 4. 把公鑰內容貼到 authorized_keys 5. 設定 authorized_keys 權限為 644
  • A. 應該用 RSA 而非 ed25519 演算法
  • B. 私鑰應該放在主機,公鑰放 GitHub Secrets
  • C. authorized_keys 檔案名稱拼錯了
  • D. authorized_keys 的權限應該是 600,644 太寬鬆會被 SSH 拒絕

3. 你的部署 workflow 需要先 build image、push 到 registry,成功後才執行 SSH 部署。以下哪種設計最符合這個需求? 情境題

  • A. 把所有步驟放在同一個 job 的 steps 中依序執行
  • B. 分成 build job 和 deploy job,deploy job 設定 needs: build
  • C. 分成兩個獨立的 workflow 檔案,用 workflow_dispatch 手動觸發
  • D. 用 if: success() 條件讓 deploy job 在 build 成功時執行

4. 部署後發現新版本有嚴重 bug,你想實作自動回滾機制。以下腳本有什麼問題? 錯誤診斷

#!/bin/bash IMAGE=$1 # 部署新版本 docker stop my-app || true docker rm my-app || true docker run -d –name my-app -p 8080:8080 $IMAGE # 健康檢查 sleep 10 if ! curl -f http://localhost:8080/health; then echo “Health check failed, rolling back…” docker stop my-app docker rm my-app docker run -d –name my-app -p 8080:8080 $CURRENT_IMAGE fi
  • A. sleep 10 的等待時間太短
  • B. 健康檢查的 URL 路徑不正確
  • C. 沒有在部署前保存目前運行的 Image 名稱,$CURRENT_IMAGE 變數是空的
  • D. 回滾時應該用 docker restart 而非重新 run

5. 公司資安政策要求部署帳號不能使用 root,且只能執行部署相關操作。以下哪個設定最符合這個需求? 情境題

  • A. 使用 root 帳號但在 authorized_keys 加上 command 限制
  • B. 建立專用的 deploy 使用者,並加入 docker 群組讓它可以執行 docker 指令
  • C. 使用一般使用者帳號,每次部署時用 sudo 執行 docker 指令
  • D. 在 GitHub Actions 的 script 中先執行 su root 切換身份

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *