本文承接從零開始的 Docker 容器化指南,主要討論如何實現全自動化的 Docker 部署流程。透過整合 GitHub Actions、Docker Hub 和自動化工具,我們可以實現程式碼推送後自動更新生產環境的目標。

目錄

自動化部署概述

完整的自動化部署流程包含以下步驟:

  1. 開發者推送程式碼到 GitHub
  2. GitHub Actions 自動執行測試和建置
  3. 建立新的 Docker 映像檔並推送至 Docker Hub
  4. 生產環境自動偵測並更新容器
  5. 健康檢查確認部署狀態

部署流程圖

graph TD
    A[開發者推送程式碼] --> B[GitHub Actions 觸發]
    B --> C[執行測試]
    C --> D[建立 Docker 映像檔]
    D --> E[推送至 Docker Hub]
    E --> F[生產環境更新]
    F --> G[健康檢查]
    G -->|成功| H[部署完成]
    G -->|失敗| I[自動回滾]

GitHub Actions 設定

1. 基礎設定

建立 .github/workflows/deploy.yml 檔案:

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
name: Deploy to Production

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

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

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

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
user/myapp:latest
user/myapp:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

# 更新快取
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

2. 部署到伺服器

添加遠端部署步驟:

1
2
3
4
5
6
7
8
9
- name: Deploy to Production
uses: appleboy/ssh-action@v1.0.3 # 建議鎖定版本,避免 @master 因上游更新破壞工作流或引入供應鏈風險
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /path/to/project
./deploy.sh ${{ github.sha }}

自動更新容器設定

1. Watchtower 設定

Watchtower 可以自動監測和更新 Docker 容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
# docker-compose.yml
services:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_NOTIFICATION_URL=slack://hook:xxxx-yyyy-zzzz@webhook # shoutrrr 格式,非直接貼 webhook URL;token 取自 Slack Incoming Webhook URL 末段
labels:
- "com.centurylinklabs.watchtower.enable=true"
command: --interval 300 # 每5分鐘檢查一次

2. 容器標籤設定

為需要自動更新的服務加上標籤:

1
2
3
4
services:
api:
labels:
- "com.centurylinklabs.watchtower.enable=true"

零停機部署策略

1. 藍綠部署

建立部署腳本 deploy.sh

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
#!/bin/bash

# 設定變數
APP_NAME="myapp"
BLUE_PORT="8080"
GREEN_PORT="8081"
PROXY_PORT="80"
VERSION=$1

# 獲取當前活動的部署顏色
ACTIVE_COLOR=$(docker ps --filter "name=${APP_NAME}-" --format "{{.Names}}" | grep -o 'blue\|green')
if [ "$ACTIVE_COLOR" == "blue" ]; then
NEW_COLOR="green"
NEW_PORT=$GREEN_PORT
else
NEW_COLOR="blue"
NEW_PORT=$BLUE_PORT
fi

echo "Starting $NEW_COLOR deployment..."

# 部署新版本
docker-compose -f docker-compose.$NEW_COLOR.yml up -d --build

# 等待服務啟動
sleep 10

# 健康檢查
if curl -f "http://localhost:$NEW_PORT/health"; then
# 切換流量:將新顏色與埠號寫入環境變數,讓 proxy 設定動態指向正確上游
echo "Switching traffic to $NEW_COLOR deployment (port $NEW_PORT)"
OLD_COLOR=$ACTIVE_COLOR # 先保存舊顏色,之後才能移除
export ACTIVE_COLOR=$NEW_COLOR
export ACTIVE_PORT=$NEW_PORT
docker-compose -f docker-compose.proxy.yml up -d --force-recreate
# 注意:docker-compose.proxy.yml 需透過 ${ACTIVE_PORT} 或 ${ACTIVE_COLOR}
# 引用上述變數,才能真正把流量導到新版本(例如 nginx upstream 指向 app_${ACTIVE_COLOR}:${ACTIVE_PORT})

# 移除舊版本
if [ -n "$OLD_COLOR" ]; then
docker-compose -f docker-compose.$OLD_COLOR.yml down
fi

echo "Deployment successful!"
else
echo "Health check failed! Rolling back..."
docker-compose -f docker-compose.$NEW_COLOR.yml down
exit 1
fi

2. 健康檢查設定

在服務中設定健康檢查:

1
2
3
4
5
6
7
8
services:
api:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

備份與還原機制

1. 自動備份設定

建立備份腳本 backup.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash

# 設定變數
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# 建立備份目錄
mkdir -p $BACKUP_DIR

# 備份 Docker Compose 原始設定檔(直接複製原始檔案,而非 docker-compose config 的輸出)
# 注意:docker-compose config 輸出的是「展開後」的設定(環境變數已內聯、路徑被正規化),
# 把它拿去 restore 會導致相對路徑或 ${VAR} 引用失效;應備份原始 docker-compose.yml
cp docker-compose.yml $BACKUP_DIR/docker-compose-$TIMESTAMP.yml

# 備份容器資料(MySQL 範例;若使用 SQL Server,改用 sqlcmd 或直接備份 .bak 檔)
docker-compose exec -T db sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases' > $BACKUP_DIR/db-$TIMESTAMP.sql

# 保留最近 7 天的備份
find $BACKUP_DIR -type f -mtime +7 -delete

2. 還原流程

建立還原腳本 restore.sh

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
#!/bin/bash

if [ -z "$1" ]; then
echo "Please provide backup timestamp"
exit 1
fi

TIMESTAMP=$1
BACKUP_DIR="/backups"

# 檢查備份檔案是否存在
if [ ! -f "$BACKUP_DIR/docker-compose-$TIMESTAMP.yml" ] ||
[ ! -f "$BACKUP_DIR/db-$TIMESTAMP.sql" ]; then
echo "Backup files not found"
exit 1
fi

# 停止現有服務
docker-compose down

# 還原設定檔
cp $BACKUP_DIR/docker-compose-$TIMESTAMP.yml docker-compose.yml

# 啟動服務
docker-compose up -d

# 還原資料庫
docker-compose exec -T db sh -c "mysql -u root -p\"$MYSQL_ROOT_PASSWORD\"" < $BACKUP_DIR/db-$TIMESTAMP.sql

監控與告警設定

1. Prometheus 設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# docker-compose.yml
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"

grafana:
image: grafana/grafana
ports:
- "3000:3000"
depends_on:
- prometheus

2. 告警設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# prometheus.yml
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093

rule_files:
- alert.rules.yml

scrape_configs:
- job_name: 'docker'
static_configs:
- targets: ['host.docker.internal:9323']
# Prometheus 跑在容器裡時,localhost 指的是容器本身,不是 Docker host。
# 需改用 host.docker.internal(Linux 上須在 compose 裡加 extra_hosts: ["host.docker.internal:host-gateway"])。
# 另外 Docker daemon 的 metrics endpoint 預設關閉,需在 /etc/docker/daemon.json 加上:
# { "metrics-addr": "0.0.0.0:9323", "experimental": true }
# 然後重啟 Docker daemon 才會開放此埠。

故障排除與回滾流程

1. 常見問題處理

問題 優先確認項目
映像檔拉取失敗 Registry 憑證是否過期(docker login);網路是否可達 Docker Hub
容器啟動失敗 docker logs <container> 查啟動錯誤;環境變數是否齊全
健康檢查失敗 服務實際監聽埠是否正確;start_period 是否太短
資料庫連線問題 容器 network 是否同一個;連線字串主機名是否用服務名稱而非 localhost

2. 回滾流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# rollback.sh

VERSION=$1

if [ -z "$VERSION" ]; then
echo "Please specify version to rollback"
exit 1
fi

# 設定版本(compose 檔需以 ${APP_VERSION} 引用此變數,才能拉取指定版本映像檔)
export APP_VERSION=$VERSION

# 拉取指定版本映像檔
docker-compose pull

# 停止並重新啟動服務
docker-compose down
docker-compose up -d

最佳實踐建議

  • 版本標籤:映像檔同時打 latestgit sha 標籤,方便回滾到任意 commit。定期執行 docker image prune 清理無用層。
  • Actions 版本釘牢:所有 uses: 鎖定至 major 版本(如 @v4)或 commit SHA,避免上游靜默破壞工作流。
  • Secrets 不落地:資料庫密碼、Registry token 只放 GitHub Secrets 或 Vault,不寫進 compose 檔。
  • 映像檔瘦身:多階段建置(multi-stage build)分離 build 與 runtime,runtime stage 改用 -slimdistroless 基底映像檔,通常可將映像檔大小砍半以上。
  • 快取層序:Dockerfile 把最少變動的層(安裝套件)放前、最常變動的層(複製應用程式碼)放後,BuildKit 快取命中率更高。

結論

上述流程把「推程式碼 → 測試 → 建置 → 部署 → 監控」串成一條無人值守的管線。實際落地時,藍綠切換腳本需要三個對應的 compose 檔(blue / green / proxy),Watchtower Slack 通知需要用 shoutrrr 格式的 URL,回滾腳本需要在 docker-compose pull 之前先 export 版本變數,Prometheus 的 alert.rules.yml 也需要自行定義閾值——這些都是直接抄範例會踩到的坑,對照本文的注解調整即可。

參考資源