第 4 章:CD — 自動 build Docker image 並推送到 GHCR

第 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 章那個 OCI image.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-xxxxlatest
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.ioIMAGE_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 設為公開

  1. 提交並 push:
cd code
git add .github/workflows/cd.yml
git commit -m "cd: 自動 build 並推送 image 到 GHCR"
git push
  1. 到 repo 的 Actions 分頁,等 CD 跑完(綠勾)。

  2. 把 package 設為公開(很重要,第 6、8 章雲端才能免憑證拉取):

    • 到你的 repo 首頁右側 → Packages → 點進 no-logic-trade 這個 package。
    • 右側 Package settingsDanger ZoneChange visibility → 選 Public → 確認。
    • 因為第 2 章加了 org.opencontainers.image.source 標籤,這個 package 會自動連結回你的 repo(package 頁面會顯示來源)。

💡 為什麼設公開? 公開 image 任何人(包含 Cloud Run、GKE)都能直接 pull,不必設定拉取憑證,對新手最省事。私有 image 則需要在部署端配置 imagePullSecret(較進階)。


✅ 如何驗證這一章成功

驗證 1:CD 綠勾、GHCR 有 image

  • repo Actions 分頁:最新 CD 為綠色 ✅。
  • repo 首頁 Packages:看到 no-logic-trade,點進去有兩個標籤:latestsha-<長字串>

驗證 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 出現 latestsha-... 兩個標籤。
  • [ ] 設為 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)」與「宣告式設定」的概念,安裝 gcloudterraform、完成 GCP 授權,並用 Terraform 寫出第一份設定:啟用必要的 API、建立部署用的服務帳號——而且學會 plan / apply / destroy 與「狀態(state)」。