測驗:SSH 自動部署 – 讓 GitHub Actions 操作你的主機
共 5 題,點選答案後會立即顯示結果
1. 為什麼自動化部署建議使用 SSH 金鑰認證而非密碼認證?
2. 執行 ssh-keygen -t ed25519 後會產生哪兩個檔案?
3. 在金鑰認證的設定中,公鑰和私鑰分別應該放在哪裡?
4. 在部署腳本中,docker stop my-app || true 的 || true 有什麼作用?
5. 在 GitHub Actions workflow 中,needs: build 這個設定代表什麼意思?
一句話說明
讓 GitHub Actions 透過 SSH 連到你的主機,自動執行部署指令。
這篇要解決的問題
前幾篇我們學會了建立 Docker Image 並推送到 Registry。但 Image 在 Registry 裡不會自己跑起來——你需要「有人」登入主機,拉取 Image,然後啟動容器。
這個「有人」可以是你自己手動 SSH 進去操作,也可以是 GitHub Actions 自動幫你做。
本篇教你後者。
前置條件
在開始之前,確認你有:
- 一台可 SSH 連線的主機(VPS、自架伺服器)
- 主機已安裝 Docker
- 主機可以存取你的 Docker Registry(Docker Hub 或私有 Registry)
核心概念:SSH 金鑰認證
為什麼需要金鑰?
GitHub Actions 要連到你的主機,需要證明「我有權限連」。有兩種方式:
| 方式 | 說明 | 適合場景 |
|---|---|---|
| 密碼認證 | 輸入使用者密碼 | 不建議用於自動化 |
| 金鑰認證 | 用私鑰證明身份 | 自動化部署標準做法 |
金鑰認證的原理:
- 你產生一對金鑰:私鑰(秘密)和 公鑰(可公開)
- 把公鑰放到主機上
- 把私鑰存到 GitHub Secrets
- 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) |
設定步驟
- 到你的 GitHub Repository
- Settings > Secrets and variables > Actions
- 點 “New repository secret”
- 填入名稱和值
取得私鑰內容
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”
檢查順序:
- 私鑰內容是否完整(包含 BEGIN/END 行)
- 主機的 authorized_keys 是否有對應的公鑰
- authorized_keys 的權限是否是 600
- .ssh 目錄的權限是否是 700
Q: 容器啟動了但連不上
檢查順序:
- Port mapping 是否正確
- 防火牆是否有開放對應的 port
- 容器內的應用程式是否正常啟動(用
docker logs my-app查看)
Q: 如何測試 SSH 連線?
可以在 workflow 裡先測試連線:
script: |
echo "Connected successfully!"
whoami
docker --version
Code language: PHP (php)小結
本篇你學會了:
- 產生 SSH 金鑰對:用
ssh-keygen -t ed25519產生 - 設定 GitHub Secrets:安全儲存 SSH 連線資訊
- 使用 appleboy/ssh-action:在 workflow 裡執行 SSH 指令
- 撰寫部署腳本:拉取 Image、停止舊容器、啟動新容器
- 錯誤處理:用
|| true處理容器不存在的情況,考慮回滾策略
下一篇,我們會介紹如何讓這個流程更完善:加入健康檢查、部署通知、以及更進階的部署策略。
進階測驗:SSH 自動部署 – 讓 GitHub Actions 操作你的主機
共 5 題,包含情境題與錯誤診斷題。