Docker 教學 第 5 堂:CI/CD 與 GitHub Actions 實戰
本系列為 18 小時 Docker 基礎教學講義,適合初學者,使用 Windows 電腦。 本篇為第 5 堂課,約 3 小時。
本堂課目標
前四堂課已經學過:
- Docker 的基本概念
- Image 和 Container 的操作
- Docker Compose 管理多個服務
- Dockerfile 建立自己的映像檔
- 把 image push 到 Docker Hub
第五堂課要把這些能力接到自動化流程:只要把程式碼 push 到 GitHub,就自動測試、自動 build Docker Image、自動檢查容器能不能正常啟動,最後再 push 到 Docker Hub。
這堂課結束後,你應該能做到:
- 看懂 CI/CD 的基本流程
- 建立 GitHub Actions workflow
- 使用
env、secrets、if、needs - 讓 GitHub Actions 自動跑 pytest
- 測試通過後自動 build Docker Image
- 在 CI 裡啟動容器並測
/health - 健康檢查通過後 push image 到 Docker Hub
- 看懂常見的 GitHub Actions 失敗原因
5-1 什麼是 CI/CD?(約 20 分鐘)
先講一個故事
想像你在一間餐廳工作:
- 傳統做法:廚師做完一道菜,自己端出去,自己收盤子,回來再做下一道。每件事都要手動做。
- 自動化做法:廚師做完菜放到出餐口,傳送帶自動送到客人桌上,髒盤子自動回收。廚師只需要專心做菜。
CI/CD 就是軟體開發的「傳送帶」:你專心寫程式碼,測試、打包、部署由自動化流程負責。
CI/CD 的定義
CI(Continuous Integration,持續整合):
- 開發者把程式碼 push 到 GitHub 之後,自動執行測試
- 確保每次改動都不會弄壞現有功能
- 典型流程是「push 程式碼 -> 自動跑測試 -> 告訴你有沒有通過」
CD(Continuous Delivery / Deployment,持續交付 / 持續部署):
- 測試通過後,自動把程式打包成 Docker Image
- 自動 push 到 Docker Hub 或其他 image registry
- 後續可以手動或自動部署到伺服器
完整流程可以想成:
開發者 push 程式碼
|
v
CI:自動跑測試
|
v
測試通過
|
v
CD:自動 build Docker Image
|
v
啟動容器做健康檢查
|
v
push image 到 Docker Hub
|
v
部署到伺服器沒有 CI/CD 的痛苦
| 手動流程 | CI/CD 自動化 |
|---|---|
| 自己跑測試,常常忘記跑 | 每次 push 自動跑,不會漏 |
自己打 docker build、docker push | workflow 自動 build 和 push |
| 本機可以跑,上線卻壞掉 | 每次都在固定環境測試 |
| 壞掉的 image 可能被推上去 | health check 通過才 push |
| 部署一次要很多手動步驟 | 流程可以重複、可觀察、可除錯 |
常見 CI/CD 工具
| 工具 | 說明 |
|---|---|
| GitHub Actions | GitHub 內建,本課程使用 |
| GitLab CI/CD | GitLab 內建 |
| Jenkins | 老牌工具,通常需要自己架伺服器 |
| CircleCI | 第三方 CI/CD 服務 |
本課程使用 GitHub Actions,因為大部分人都有 GitHub 帳號,而且可以直接和 repository、pull request、secret、Docker Hub 流程整合。
5-2 GitHub Actions 基礎與實用語法(約 40 分鐘)
前置準備
- 確認你有 GitHub 帳號
- 安裝 Git
- 確認 Git 可以使用:
git --version
git config --global user.name "你的名字"
git config --global user.email "你的email"GitHub Actions 的核心概念
GitHub Actions 的架構可以先記四個詞:
Repository
└── .github/workflows/
└── cicd.yml
├── Event:什麼時候觸發?
├── Job:要做哪幾個大任務?
│ ├── Step 1
│ ├── Step 2
│ └── Step 3
└── Runner:在哪一台機器上執行?| 名詞 | 說明 |
|---|---|
| Workflow | 一份自動化腳本,寫在 YAML 檔案裡 |
| Event | 觸發條件,例如 push、pull request、手動觸發 |
| Job | 一個大任務,例如 test、build、deploy |
| Step | job 裡的一個步驟 |
| Runner | GitHub 提供的虛擬機,負責執行 workflow |
第一個 Workflow:Hello GitHub Actions
建立一個 GitHub repository,例如 docker-cicd-demo,clone 到本機:
cd C:\docker-lab
git clone https://github.com/你的帳號/docker-cicd-demo.git
cd docker-cicd-demo建立 workflow 目錄:
mkdir -p .github/workflows建立 .github/workflows/hello.yml:
name: Hello GitHub Actions
on:
push:
branches: [ main ]
jobs:
say-hello:
runs-on: ubuntu-latest
steps:
- name: Say hello
run: echo "Hello GitHub Actions!"
- name: Show current directory
run: pwd
- name: Show system info
run: uname -aPush 到 GitHub:
git add .
git commit -m "add first workflow"
git push到 repository 的 Actions 分頁,就能看到 GitHub 的 runner 自動執行這份 workflow。
Workflow 語法快速拆解
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest test_app.py -vStep 有兩種常見寫法:
| 寫法 | 用途 | 範例 |
|---|---|---|
uses | 使用別人寫好的 Action | uses: actions/checkout@v4 |
run | 自己寫 shell 指令 | run: pytest test_app.py -v |
actions/checkout@v4 是最常用的 Action,功能是把 repository 裡的程式碼拉到 runner 上。沒有 checkout,runner 上通常不會有你的程式碼。
環境變數 env
Workflow 裡可以用 env 避免重複寫相同設定。
name: CI with Environment Variables
on:
push:
branches: [ main ]
env:
IMAGE_NAME: docker-cicd-demo
PYTHON_VERSION: '3.11'
jobs:
test:
runs-on: ubuntu-latest
env:
FLASK_ENV: testing
steps:
- name: Show env
run: |
echo "Image: ${{ env.IMAGE_NAME }}"
echo "Python: ${{ env.PYTHON_VERSION }}"
echo "Flask: $FLASK_ENV" | 層級 | 範圍 | 寫在哪裡 |
|---|---|---|
| Workflow 層 | 所有 job 都能用 | 最外層的 env: |
| Job 層 | 只有這個 job 能用 | job 底下的 env: |
| Step 層 | 只有這個 step 能用 | step 底下的 env: |
適合放在 env 的東西:
- image 名稱
- Python / Node 版本
- port
- 測試環境名稱
- 重複出現、但不是機密的設定
不適合放在 env 的東西:
- 密碼
- API key
- access token
- database password
這些機密資料要放在 secrets。
Secrets
CI/CD 最重要的安全規則:
密碼和 token 不要寫進 Git repository。
如果把 Docker Hub token 寫在 .github/workflows/cicd.yml,等於把推送 image 的權限放進程式碼。正確做法是放到 GitHub Secrets。
設定位置:
- 打開 GitHub repository
- 點 Settings
- 點 Secrets and variables
- 點 Actions
- 點 New repository secret
本課程會用到兩個 secret:
| Secret 名稱 | 用途 |
|---|---|
DOCKERHUB_USERNAME | Docker Hub 帳號 |
DOCKERHUB_TOKEN | Docker Hub Access Token |
在 workflow 裡這樣使用:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}GitHub 通常會把 secret 在 log 中遮成 ***,但不要故意把 secret 印出來。遮罩不是安全設計的全部,只是最後一道防線。
GitHub Context
GitHub Actions 有很多內建資訊可以使用,稱為 context。最常用的是 github context:
- name: Show GitHub context
run: |
echo "event: ${{ github.event_name }}"
echo "ref: ${{ github.ref }}"
echo "sha: ${{ github.sha }}"
echo "actor: ${{ github.actor }}" 常見欄位:
| 寫法 | 說明 |
|---|---|
${{ github.event_name }} | 觸發事件,例如 push、pull_request |
${{ github.ref }} | 目前 ref,例如 refs/heads/main |
${{ github.sha }} | 目前 commit SHA |
${{ github.actor }} | 觸發 workflow 的使用者 |
${{ github.repository }} | repository 名稱,例如 user/project |
條件判斷 if
可以根據條件決定某個 step 或 job 要不要執行。
steps:
- name: Only on main branch
if: github.ref == 'refs/heads/main'
run: echo "This is main branch"
- name: Only on pull request
if: github.event_name == 'pull_request'
run: echo "This is a pull request"
- name: Print logs on failure
if: failure()
run: echo "Something went wrong"在 CI/CD 裡常見規則:
- PR:只跑測試
- push 到 main:跑測試、build image、push image
- 失敗時:印出 log,方便除錯
多個 Job 的依賴關係 needs
預設情況下,多個 job 會平行執行。
如果 docker job 必須等 test job 通過,就要加 needs:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Running tests..."
docker:
runs-on: ubuntu-latest
needs: test
steps:
- run: echo "Building image..."執行順序:
test -> docker如果 test 失敗,docker 不會跑。這可以避免測試沒過的程式碼被 build 成 image。
permissions
Workflow 預設會拿到一個 GITHUB_TOKEN。比較好的做法是明確寫出需要的權限:
permissions:
contents: read如果只是 checkout 程式碼、跑測試、build image,通常 contents: read 就夠了。
concurrency
如果連續 push 很多次,GitHub Actions 可能同時排很多 workflow。對部署或 push image 來說,通常只需要最新的一次。
concurrency:
group: cicd-${{ github.ref }}
cancel-in-progress: true意思是:同一個分支如果已經有 workflow 在跑,新的 workflow 來了就取消舊的。
5-3 實作:完整 Flask CI/CD Pipeline(約 1 小時 30 分鐘)
目標
這一節要建立一個完整流程:
- Push 程式碼到 GitHub
- 自動跑 Flask 測試
- 測試通過後 build Docker Image
- 在 CI 裡把容器跑起來做健康檢查
- 健康檢查通過後 push 到 Docker Hub
- 用
latest和 commit SHA 產生兩個 tag - 輸出部署指令
流程長這樣:
git push
|
v
test job
|
v
docker job
|
+-- build image
+-- run container
+-- curl /health
+-- login Docker Hub
+-- push image
+-- print deploy command課堂節奏建議
這個實作不要一次貼完整 workflow。建議拆成四個 checkpoint:
| Checkpoint | 預估時間 | 目標 |
|---|---|---|
| A | 20 分鐘 | 建立 Flask 專案並本機測試 |
| B | 20 分鐘 | 建立 test job,確認 CI 綠燈 |
| C | 25 分鐘 | 加入 Docker build 和 health check,但先不 push |
| D | 25 分鐘 | 加入 Docker Hub login 和 push |
每個 checkpoint 都讓學生 push 一次、看一次 Actions log。
步驟 1:建立 Flask 專案
建立新的專案資料夾,或使用剛才的 docker-cicd-demo。
建議目錄:
docker-cicd-demo/
├── .github/
│ └── workflows/
│ └── cicd.yml
├── app.py
├── test_app.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
└── README.mdapp.py:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def hello():
return jsonify({
"message": "Hello from CI/CD!",
"version": "1.0.0"
})
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
def add(a, b):
return a + b
def is_positive(n):
return n > 0
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)test_app.py:
import pytest
from app import app, add, is_positive
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_hello(client):
response = client.get("/")
assert response.status_code == 200
data = response.get_json()
assert "message" in data
assert data["version"] == "1.0.0"
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_is_positive():
assert is_positive(1) is True
assert is_positive(-1) is False
assert is_positive(0) is Falserequirements.txt:
flask
pytestDockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"].dockerignore:
.git
.github
__pycache__
*.pyc
venv/
.venv/
.env
*.md這裡不要忽略 test_*.py,因為我們等一下會在 Docker build 前跑 pytest。真正 production image 是否要包含測試檔,要看專案策略。初學階段先保持簡單。
步驟 2:本機測試
先在本機確認 Flask 專案可以測試:
pip install -r requirements.txt
pytest test_app.py -v預期結果:
test_app.py::test_hello PASSED
test_app.py::test_health PASSED
test_app.py::test_add PASSED
test_app.py::test_is_positive PASSED再測 Docker Image:
docker build -t docker-cicd-demo:local .
docker run -d --name cicd-local -p 5000:5000 docker-cicd-demo:local
curl http://localhost:5000/
curl http://localhost:5000/health
docker rm -f cicd-local如果 Windows PowerShell 的 curl 行為怪怪的,可以改用:
Invoke-WebRequest http://localhost:5000/health步驟 3:建立第一版 workflow,只跑測試
建立 .github/workflows/cicd.yml:
name: Flask CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
permissions:
contents: read
env:
PYTHON_VERSION: '3.11'
IMAGE_NAME: docker-cicd-demo
jobs:
test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run pytest
run: pytest test_app.py -vPush 到 GitHub:
git add .
git commit -m "add flask test workflow"
git push到 Actions 頁面確認:
- workflow 有觸發
Run testsjob 成功- pytest log 看得到 4 個測試
步驟 4:故意讓測試失敗,練習看 log
把 app.py 的 version 改掉:
"version": "2.0.0"但 test_app.py 還是期待 1.0.0。
Push:
git add .
git commit -m "break version test"
git push到 Actions 頁面,你應該會看到紅色失敗。點進 log,找到類似訊息:
E AssertionError: assert '2.0.0' == '1.0.0'除錯時照這個順序:
- 失敗的是哪個 job?
- 失敗的是哪個 step?
- 第一個真正的錯誤訊息是什麼?
- 要修程式,還是要修測試?
修回來:
"version": "1.0.0"再 push 一次,確認 CI 回到綠色。
步驟 5:加入 Docker build,但先不 push
接著加入第二個 job。先只 build,不登入 Docker Hub、不 push。
docker:
name: Build Docker image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build image locally
run: docker build -t ${{ env.IMAGE_NAME }}:ci .完整 workflow 會變成:
name: Flask CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
permissions:
contents: read
env:
PYTHON_VERSION: '3.11'
IMAGE_NAME: docker-cicd-demo
jobs:
test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run pytest
run: pytest test_app.py -v
docker:
name: Build Docker image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build image locally
run: docker build -t ${{ env.IMAGE_NAME }}:ci .Push 後觀察:
Run tests
|
v
Build Docker image這個 checkpoint 的教學重點:
needs: test讓 docker job 等測試通過if讓 PR 不會 build image- 先 build local image,確認 Dockerfile 沒問題
步驟 6:在 CI 裡啟動容器做健康檢查
Image build 成功不代表服務真的能跑。
常見問題:
- Dockerfile build 成功,但啟動時找不到檔案
- Flask port 設錯
- app 啟動後立刻 crash
/healthendpoint 壞掉
所以 build 後要把容器真的跑起來測。
在 docker job 後面加:
- name: Run container
run: docker run -d --name app -p 5000:5000 ${{ env.IMAGE_NAME }}:ci
- name: Check health endpoint
run: |
for i in 1 2 3 4 5; do
curl --fail http://localhost:5000/health && exit 0
echo "App is not ready yet..."
sleep 3
done
exit 1
- name: Show container logs on failure
if: failure()
run: docker logs app
- name: Stop container
if: always()
run: docker rm -f app幾個重點:
| 寫法 | 用途 |
|---|---|
curl --fail | HTTP 不是 2xx/3xx 時讓 step 失敗 |
| retry loop | app 啟動需要幾秒時,比固定 sleep 更穩 |
if: failure() | 只有前面失敗時才印 log |
if: always() | 不管成功失敗都清掉容器 |
這個設計比「只 build image」更可靠。
步驟 7:設定 Docker Hub Access Token
到 Docker Hub 建立 Access Token:
- 登入 Docker Hub
- 點右上角帳號
- 點 Account settings
- 點 Security
- 點 New access token
- 輸入名稱,例如
github-actions-docker-course - 建立後複製 token
接著到 GitHub repository 設定 secrets:
- Repository -> Settings
- Secrets and variables
- Actions
- New repository secret
- 新增:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
Docker Hub token 只會顯示一次。如果忘記複製,就刪掉重建。
步驟 8:完整 workflow:build、health check、push
.github/workflows/cicd.yml:
name: Flask CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
permissions:
contents: read
concurrency:
group: cicd-${{ github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: '3.11'
IMAGE_NAME: docker-cicd-demo
jobs:
test:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run pytest
run: pytest test_app.py -v
docker:
name: Build and push Docker image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create image tags
id: meta
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "image=${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
- name: Build image locally
run: |
docker build \
-t ${{ steps.meta.outputs.image }}:latest \
-t ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha_short }} \
.
- name: Run container
run: docker run -d --name app -p 5000:5000 ${{ steps.meta.outputs.image }}:latest
- name: Check health endpoint
run: |
for i in 1 2 3 4 5; do
curl --fail http://localhost:5000/health && exit 0
echo "App is not ready yet..."
sleep 3
done
exit 1
- name: Show container logs on failure
if: failure()
run: docker logs app
- name: Stop container
if: always()
run: docker rm -f app
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push image
run: |
docker push ${{ steps.meta.outputs.image }}:latest
docker push ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha_short }}
- name: Print deployment commands
run: |
echo "Docker image pushed successfully."
echo "Image: ${{ steps.meta.outputs.image }}"
echo "Tags: latest, ${{ steps.meta.outputs.sha_short }}"
echo ""
echo "Deploy with:"
echo "docker pull ${{ steps.meta.outputs.image }}:latest"
echo "docker run -d -p 5000:5000 ${{ steps.meta.outputs.image }}:latest" 步驟 9:Push 並觀察完整流程
git add .
git commit -m "add complete CI/CD pipeline"
git push到 GitHub Actions 頁面觀察:
test(跑測試)
|
v
docker(build + health check + push)
|
v
Docker Hub 上出現新的映像檔步驟 10:驗證 Docker Hub 上的映像檔
docker pull 你的帳號/docker-cicd-demo:latest
docker run -d --name cicd-test -p 5000:5000 你的帳號/docker-cicd-demo:latest
curl http://localhost:5000
curl http://localhost:5000/health
docker rm -f cicd-test如果看到:
{"status":"healthy"}表示這個 image 從 build、push、pull、run 都成功了。
課堂討論:為什麼 health check 要放在 push 前?
如果流程是這樣:
build image -> push image -> run container test那就有機會把壞掉的 image 推到 Docker Hub。
比較好的流程是:
build image -> run container test -> push image先確認 image 能跑,再推到遠端 registry。
為什麼要同時使用 latest 和 commit SHA?
latest 很方便,但它不是固定版本。今天的 latest 和明天的 latest 可能不是同一個 image。
| Tag | 優點 | 缺點 |
|---|---|---|
latest | 好記、適合快速測試 | 不知道精確版本 |
| commit SHA | 每次 push 都有固定版本 | 比較不好手打 |
實務上常見做法是兩個都推:
yourname/docker-cicd-demo:latest
yourname/docker-cicd-demo:a1b2c3d要快速測試可以用 latest。要回滾或追查問題時,用 commit SHA tag 會清楚很多。
5-4 實用技巧、常見錯誤與練習(約 30 分鐘)
Badge:在 README 顯示 CI 狀態
在 README.md 加上一行,就能顯示 CI 是通過還是失敗:
效果是別人一進 repository 就能看到目前 CI 狀態。
如果 repo 名稱或 workflow 檔名不同,要記得修改 URL。
快取依賴加速 CI
每次 CI 都重新 pip install 會比較慢,可以加上快取:
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip- 第一次跑會正常安裝,之後只要 requirements.txt 沒變,就會用快取。
多版本測試 Matrix
想同時在多個 Python 版本上測試,可以使用 matrix:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- run: pytest test_app.py -v這會同時跑 3 個 job:
- Python 3.10
- Python 3.11
- Python 3.12
如果你的專案是套件或 library,matrix 很有用。如果你的專案是固定部署到某個 Python 版本的 Web app,就不一定需要一開始就做 matrix。
上傳測試報告 Artifact
有時候 CI 產生的檔案想下載,例如:
- 測試報告
- coverage 報告
- build log
- 打包後的檔案
可以使用 artifact。
先讓 pytest 輸出 junit 報告:
- name: Run pytest
run: pytest test_app.py -v --junitxml=test-results.xml再上傳:
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.xmlif: always() 表示測試失敗時也要上傳報告。
常見錯誤 1:YAML 縮排錯誤
錯誤範例:
jobs:
test:
runs-on: ubuntu-latest正確範例:
jobs:
test:
runs-on: ubuntu-latestYAML 非常依賴縮排。
建議:
- 一律用空白,不要用 Tab
- 同一層縮排保持一致
- VS Code 安裝 YAML extension
常見錯誤 2:Secret 名稱打錯
workflow 寫:
${{ secrets.DOCKERHUB_TOKEN }}但 GitHub 設的是:
DOCKER_HUB_TOKEN這樣登入會失敗。
排查方式:
- 檢查 GitHub Secrets 名稱
- 檢查 workflow 裡的拼字
- 確認 token 沒過期
- 確認 Docker Hub 帳號名稱正確
常見錯誤 3:Image 名稱不合法
Docker image 名稱通常要小寫。
錯誤:
MyName/Docker-CICD-Demo建議:
myname/docker-cicd-demo如果你的 Docker Hub 帳號有大寫,實務上建議在 workflow 裡仍然用小寫 namespace。
常見錯誤 4:容器啟動太慢,健康檢查失敗
不要只固定睡很久,改用 retry:
- name: Check health endpoint
run: |
for i in 1 2 3 4 5; do
curl --fail http://localhost:5000/health && exit 0
echo "App is not ready yet..."
sleep 3
done
exit 1 常見錯誤 5:本機可以跑,CI 不能跑
常見原因:
| 現象 | 可能原因 |
|---|---|
| 找不到檔案 | 檔案沒有 commit |
ModuleNotFoundError | requirements.txt 漏套件 |
| 測試路徑錯誤 | workflow 的工作目錄不對 |
| Docker build 找不到 Dockerfile | Dockerfile 名稱或位置錯 |
curl 連不上 | app 沒有 listen 0.0.0.0 |
Flask 在容器裡一定要:
app.run(host="0.0.0.0", port=5000)如果寫 127.0.0.1,容器外可能連不到。
除錯 Checklist
看到 GitHub Actions 失敗時,照這個順序查:
- 失敗的是哪個 job?
- 失敗的是哪個 step?
- 第一個 error message 是什麼?
- 是測試失敗、安裝失敗、Docker build 失敗,還是 Docker login 失敗?
- 本機能不能重現?
- 是程式問題、設定問題,還是 secret/token 問題?
課堂練習
練習 1:修改 workflow 觸發條件
修改 hello.yml,讓它在建立 Pull Request 時也會觸發。
練習 2:加上 README Badge
在 README.md 加上 CI badge,確認 repository 首頁看得到目前 workflow 狀態。
練習 3:讓 CI 抓到壞掉的 Dockerfile
故意把 Dockerfile 的啟動指令改錯:
CMD ["python", "missing.py"]Push 後觀察:
- Docker build 會不會成功?
- 容器健康檢查會不會失敗?
docker logs app印出什麼?
修回正確版本:
CMD ["python", "app.py"]練習 4:多版本測試
把 test job 改成 matrix,讓它同時測:
- Python 3.10
- Python 3.11
- Python 3.12
觀察 Actions 頁面會出現幾個 job。
練習 5:驗證 Docker Hub
到 Docker Hub 確認你的映像檔有自動上傳,試著在本機 pull 下來跑:
docker pull 你的帳號/docker-cicd-demo:latest
docker run -d --name final-test -p 5000:5000 你的帳號/docker-cicd-demo:latest
curl http://localhost:5000/health
docker rm -f final-test下課前檢查
確認每個人至少完成:
- GitHub Actions 有一個綠色的 CI/CD workflow
- Docker Hub 上看得到自己 push 的 image
- 本機可以
docker pull並跑起來 - README 有 CI badge
- 知道 GitHub Secrets 在哪裡設定
- 看得懂失敗 log 的第一個 error message
如果這些都完成,第五堂課就把 Dockerfile、Docker Hub、GitHub Actions 串成了一個完整的自動化流程。