第 4 章:CD — 自動 build Docker image 並推送到 GHCR
這一章在做什麼? 我們要讓 GitHub 在每次 push 到 main 時,自動 build 出 Docker image 並推送到 GHCR(GitHub 的容器倉庫)。做完這章,你的每一次提交都會在雲端產生一顆「可被部署的 image」,之後第 6、8 章的雲端與 K8s 就從這裡拉取它。
為什麼需要把 image 推到 registry? 因為「部署」的本質,就是某台雲端機器去拉一顆 image 來跑。那顆 image 必須放在一個大家都拿得到的地方——這個地方就叫 container registry。
前置:第 3 章的
ci.yml已在運作、repo 已在 GitHub。本章新增code/.github/workflows/cd.yml。
步驟 1:先搞懂 registry 與 GHCR
Container Registry(容器倉庫)
存放容器 image 的「倉庫」。你把 build 好的 image push 上去,部署目標再從它 pull 下來跑。
🔁 前端類比:就像 npm registry 存放 JS 套件——你
npm publish上去、別人npm install下來。Registry 是「存放 image 的 npm」,docker push/docker pull就是 publish/install。
GHCR(GitHub Container Registry)
GitHub 自家的 registry,網址前綴是
ghcr.io。它和你的 repo 整合得很好:公開 image 免費、可用 GitHub 內建的權杖登入、還能自動把 image 連結回原始碼 repo(就是第 2 章那個 OCIimage.source標籤的功用)。
image 的完整名稱長這樣:
ghcr.io/<你的帳號>/no-logic-trade:latest
└─registry─┘└──擁有者──┘└──image 名──┘└tag┘
步驟 2:認識會用到的「零件」
這份 workflow 會用到幾個官方 action 與內建變數,先有概念:
| 零件 | 作用 |
|---|---|
secrets.GITHUB_TOKEN |
GitHub 自動提供的暫時權杖,不用你手動建立。配合權限設定就能推 GHCR。 |
permissions: packages: write |
明確授予這個 workflow「寫入 GHCR」的權限(預設沒有)。 |
docker/login-action |
用上面的權杖登入 ghcr.io。 |
docker/metadata-action |
自動產生 image 標籤(tags)與 OCI labels,例如 sha-xxxx、latest。 |
docker/build-push-action |
一步完成 build + push,還能用 GitHub 的快取加速。 |
github.repository |
內建變數,值是 擁有者/repo,剛好當 image 名。 |
步驟 3:寫 CD workflow
建立 code/.github/workflows/cd.yml:
# code/.github/workflows/cd.yml
name: CD
on:
push:
branches: [ main ] # 推到 main 才 build & push image
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # 例如 yourname/no-logic-trade
jobs:
build-and-push:
name: Build 並推送到 GHCR
runs-on: ubuntu-latest
# 關鍵:授予寫入 GHCR 的權限
permissions:
contents: read
packages: write
steps:
- name: 取出原始碼
uses: actions/checkout@v4
- name: 設定 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 登入 GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 產生標籤與 metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=long
type=raw,value=latest,enable={{is_default_branch}}
- name: Build 並推送
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
逐段解釋:
on: push: branches: [ main ]:只在 push 到main時跑(PR 階段由第 3 章的 CI 負責測試,這裡負責產生正式 image)。env::定義整個 workflow 共用的環境變數。REGISTRY: ghcr.io、IMAGE_NAME: ${{ github.repository }}:組出 image 名稱ghcr.io/擁有者/repo。
permissions: packages: write(最關鍵的一行區塊):預設GITHUB_TOKEN不能寫 GHCR,必須在這裡明確開權限。少了它,push 會出現denied: permission_denied。uses: docker/setup-buildx-action@v3:啟用 Buildx(進階的 Docker build 引擎,支援跨平台與快取)。uses: docker/login-action@v3+with::登入ghcr.io。username: ${{ github.actor }}:觸發者的帳號。password: ${{ secrets.GITHUB_TOKEN }}:用內建權杖,你不用自己建任何 secret。
id: meta+docker/metadata-action@v5:自動算出要打的標籤。type=sha,format=long:用 commit 的完整 SHA 當標籤(例如sha-1a2b3c…)——每次 build 都有一個可追溯到某次提交的不可變標籤。type=raw,value=latest,enable={{is_default_branch}}:在預設分支(main)才額外打上latest,方便「永遠拉最新」。- 它也會自動把 image 名稱轉成小寫(GHCR 要求小寫;GitHub 帳號可能有大寫)。
docker/build-push-action@v6+with::一步完成 build 與 push。context: .:用 repo 根目錄當 build context(會吃Dockerfile與.dockerignore)。push: true:build 完直接推上去。tags/labels:用上一步meta算好的值(steps.meta.outputs.*)。cache-from/cache-to: type=gha:把 build 快取存到 GitHub Actions 的快取,下次 build 更快。
🔁 為何拆成 ci.yml 和 cd.yml? 目前兩者各自獨立觸發:
ci.yml(push/PR 都跑、負責測試)、cd.yml(只在 push main、負責產生 image)。第 10 章會把它們串成一條「測試通過才部署」的完整 gated pipeline。現在先讓「image 進得了 GHCR」這件事跑通。
步驟 4:push,並把 image 設為公開
- 提交並 push:
cd code
git add .github/workflows/cd.yml
git commit -m "cd: 自動 build 並推送 image 到 GHCR"
git push
-
到 repo 的 Actions 分頁,等 CD 跑完(綠勾)。
-
把 package 設為公開(很重要,第 6、8 章雲端才能免憑證拉取):
- 到你的 repo 首頁右側 → Packages → 點進
no-logic-trade這個 package。 - 右側 Package settings → Danger Zone → Change visibility → 選 Public → 確認。
- 因為第 2 章加了
org.opencontainers.image.source標籤,這個 package 會自動連結回你的 repo(package 頁面會顯示來源)。
- 到你的 repo 首頁右側 → Packages → 點進
💡 為什麼設公開? 公開 image 任何人(包含 Cloud Run、GKE)都能直接 pull,不必設定拉取憑證,對新手最省事。私有 image 則需要在部署端配置 imagePullSecret(較進階)。
✅ 如何驗證這一章成功
驗證 1:CD 綠勾、GHCR 有 image
- repo Actions 分頁:最新 CD 為綠色 ✅。
- repo 首頁 Packages:看到
no-logic-trade,點進去有兩個標籤:latest與sha-<長字串>。
驗證 2:在本機把 image 拉下來
設為公開後,不用登入就能拉(把 <你的帳號> 換成你的):
docker pull ghcr.io/<你的帳號>/no-logic-trade:latest
docker images | grep no-logic-trade
預期看到 image 被拉下來、出現在清單裡。
驗證 3:看標籤可追溯到 commit
sha-xxxx 那個標籤的 xxxx 應該對得上你 git log 最新一筆 commit 的完整 SHA:
git rev-parse HEAD # 對照 GHCR 上的 sha- 標籤
- [ ] CD workflow 綠勾。
- [ ] GHCR 出現
latest與sha-...兩個標籤。 - [ ] 設為 Public 後
docker pull成功。
🧯 常見錯誤 / 踩雷點
| 症狀 | 原因 | 解法 |
|---|---|---|
push 時 denied: permission_denied / installation not allowed to Create ... package |
workflow 少了 permissions: packages: write |
補上該權限區塊;並到 repo Settings → Actions → General → Workflow permissions 確認是 Read and write |
invalid reference format: repository name must be lowercase |
image 名含大寫(GitHub 帳號大寫) | 用 docker/metadata-action(會自動小寫);若手寫請全小寫 |
部署端拉不到 image(unauthorized / not found) |
package 還是 private | 照步驟 4 把 package 改 Public(或設定 imagePullSecret) |
| CD 想推到別人的 repo package 失敗 | GITHUB_TOKEN 只能寫「自己這個 repo」的 package |
正常限制;同 repo 沒問題 |
| image 只有 amd64,arm 機器跑不動 | 預設只 build 單一平台 | 我們的部署目標(Cloud Run/GKE)就是 amd64,無妨;要多架構可在 build-push-action 加 platforms: linux/amd64,linux/arm64(較慢) |
| 每次 build 都很久 | 沒善用快取 | 已加 cache-from/to: type=gha;確認 buildx 步驟有啟用 |
| package 頁面沒連到 repo | 缺 OCI source 標籤 | 確認 Dockerfile 有 LABEL org.opencontainers.image.source=...(第 2 章) |
下一步 → 第 5 章:IaC 入門 — 用 Terraform 佈建雲端基礎設施
我們開始碰雲端。先理解「基礎設施即程式碼(IaC)」與「宣告式設定」的概念,安裝 gcloud 與 terraform、完成 GCP 授權,並用 Terraform 寫出第一份設定:啟用必要的 API、建立部署用的服務帳號——而且學會 plan / apply / destroy 與「狀態(state)」。