目錄

用 GitHub Actions 自動建置並推送 Docker 映像檔(CI/CD)

部署到雲端主機 中,建置、推送、到伺服器更新都是手動執行的。這一節用 GitHub Actions 把它自動化:

  • 每次推送到 main:自動建置並推送映像檔,tag 是 main。
  • 打上版本 tag(例如 v1.2.0):建置 amd64 + arm64 映像檔,推送 1.2.0、1.2 等 tag,然後自動 SSH 到伺服器更新。
  • Pull Request:只建置不推送,確認 Dockerfile 沒有壞掉。
從 git push 到 GitHub Actions 建置、推送到 ghcr.io、伺服器更新的流程
CI/CD 流程:推送程式碼 → GitHub Actions 建置 → ghcr.io → 伺服器拉取新版本

專案結構

沿用 部署到雲端主機 的 Node.js 專案,加上一個 workflow 檔:

myapp/
├── .github/
│   └── workflows/
│       └── docker.yml
├── Dockerfile
├── package.json
├── package-lock.json
└── server.js

GitHub 會自動執行 .github/workflows/ 中的所有 YAML 檔。

完整的 workflow

.github/workflows/docker.yml:

name: Build and push image

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Check Dockerfile
        run: docker build --check .

      - uses: docker/setup-qemu-action@v4

      - uses: docker/setup-buildx-action@v4

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Decide image tags
        id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          build-args: APP_VERSION=${{ github.ref_name }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - name: Update the server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            cd /srv/myapp
            VERSION='${{ github.ref_name }}'
            sed -i "s/^APP_VERSION=.*/APP_VERSION=${VERSION#v}/" .env
            docker compose pull app
            docker compose up -d app

逐段說明

觸發條件與權限

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:

permissions:
  contents: read
  packages: write
  • 推送到 main 分支、推送 v 開頭的 tag、開 Pull Request 時執行。
  • packages: write 讓自動提供的 GITHUB_TOKEN 可以推送到 ghcr.io。只給需要的權限是好習慣。

建置前先檢查

      - name: Check Dockerfile
        run: docker build --check .

GitHub 的 ubuntu-latest 執行環境已經安裝了 Docker。docker build --check 只檢查 Dockerfile 的寫法(介紹見 docker init),有警告時不會讓建置失敗,但會顯示在記錄中。這個專案的 Dockerfile 在本機檢查的結果:

$ docker build --check . 2>&1 | tail -n 1
Check complete, no warnings found.

QEMU 與 Buildx

      - uses: docker/setup-qemu-action@v4
      - uses: docker/setup-buildx-action@v4

GitHub 的執行環境是 amd64。要建置 arm64 映像檔、而 Dockerfile 中有 RUN npm ci 這類需要執行程式的步驟時,就需要 QEMU 模擬(原因見 多平台映像檔)。setup-buildx-action 建立一個支援多平台和快取匯出的 builder。

登入 Registry

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

Pull Request 不推送映像檔,所以不需要登入(來自外部的 PR 也拿不到推送權限)。secrets.GITHUB_TOKEN 不需要自己建立。

改推送到 Docker Hub 時,先在 Docker Hub 建立 Access Token,存到 GitHub 儲存庫的 Settings → Secrets and variables → Actions,然後:

        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

(不寫 registry 就是 Docker Hub),並把 metadata-action 的 images 改成 你的帳號/myapp。

自動決定映像檔 tag

docker/metadata-action 根據觸發 workflow 的事件產生 tag:

觸發事件產生的 tag
推送到 mainmain、sha-1a2b3c4
推送 tag v1.2.01.2.0、1.2、sha-1a2b3c4
Pull Request #7pr-7、sha-1a2b3c4(只建置不推送)
  • type=semver 只在 tag 符合語意化版本(v1.2.0)時產生,前面的 v 會被去掉。
  • type=sha 用 commit 的前 7 碼,方便追查映像檔是哪一版程式碼建置的。
  • 它也產生 OCI 標準的 labels(來源儲存庫、commit、建立時間),由 build-push-action 寫進映像檔。

建置與推送

      - name: Build and push
        uses: docker/build-push-action@v7
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          build-args: APP_VERSION=${{ github.ref_name }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
參數說明
platforms同時建置兩種架構,推送後成為一個多平台映像檔
pushPull Request 時為 false,只建置
build-args對應 Dockerfile 的 ARG APP_VERSION,程式能顯示自己的版本
tags / labels使用上一步 metadata-action 的輸出
cache-from / cache-to: type=gha把建置快取存在 GitHub Actions 的快取空間。mode=max 連中間階段的層也快取,下次只改程式碼時,npm ci 那一層可以直接重用

自動部署

  deploy:
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
  • needs: build:建置成功才執行。
  • if:只有推送版本 tag 時才部署,推送到 main 不會動到正式環境。

appleboy/ssh-action 用 SSH 登入伺服器執行 script,內容就是 部署到雲端主機 手動做的步驟:修改 .env 的 APP_VERSION,然後 pull 與 up -d。${{ github.ref_name }} 是 tag 名稱(v1.2.0),${VERSION#v} 去掉開頭的 v。這行 sed 的效果在本機實測:

$ VERSION='v1.2.0'; sed -i "s/^APP_VERSION=.*/APP_VERSION=${VERSION#v}/" .env; cat .env
APP_VERSION=1.2.0
DB_PASSWORD=secret

需要在 GitHub 儲存庫的 Secrets 中設定三個值:

Secret內容
DEPLOY_HOST伺服器的 IP 或網域
DEPLOY_USERSSH 使用者名稱
DEPLOY_SSH_KEY專門給部署用的 SSH 私鑰(對應的公鑰放在伺服器的 ~/.ssh/authorized_keys)

建議為部署建立專用的使用者和金鑰,不要用你自己登入伺服器的金鑰。

用 actionlint 驗證 workflow

Workflow 寫錯時,要推送到 GitHub 才會發現,來回修改很花時間。開源工具 actionlint 可以在本機檢查語法和運算式,用容器執行不需要安裝:

$ docker run --rm -v "$(pwd):/repo" -w /repo rhysd/actionlint:latest -color=false -verbose .github/workflows/docker.yml
verbose: Linting .github/workflows/docker.yml
verbose: Found 0 parse errors in 1 ms for .github/workflows/docker.yml
verbose: Found total 0 errors in 13 ms for .github/workflows/docker.yml

0 errors 表示沒有問題。故意把 github.ref_name 打錯成 github.ref_nam:

$ docker run --rm -v "$(pwd):/repo" -w /repo rhysd/actionlint:latest -color=false .github/workflows/docker.yml
.github/workflows/docker.yml:52:39: property "ref_nam" is not defined in object type {action: string; action_path: string; action_ref: string; action_repository: string; action_status: string; actor: string; actor_id: string; api_url: string; artifact_cache_size_limit: number; base_ref: string; env: string; event: object; event_name: string; event_path: string; graphql_url: string; head_ref: string; job: string; output: string; path: string; ref: string; ref_name: string; ref_protected: bool; ref_type: string; repository: string; repository_id: string; repository_owner: string; repository_owner_id: string; repository_visibility: string; repositoryurl: string; retention_days: number; run_attempt: string; run_id: string; run_number: string; secret_source: string; server_url: string; sha: string; state: string; step_summary: string; token: string; triggering_actor: string; workflow: string; workflow_ref: string; workflow_sha: string; workspace: string} [expression]
   |
52 |           build-args: APP_VERSION=${{ github.ref_nam }}
   |                                       ^~~~~~~~~~~~~~

actionlint 指出第 52 行的 ref_nam 不存在,並列出 github 物件所有可用的屬性。這種錯誤 GitHub 不一定會報錯,只會悄悄地變成空字串。

本章的 workflow 經過 actionlint 驗證,但沒有實際推送到 GitHub 執行(那會在 ghcr.io 建立套件、SSH 連到真實伺服器)。第一次在你的儲存庫執行時,請到 Actions 頁面檢查每個步驟的記錄。

伺服器端的設定

伺服器的 compose.yaml 把映像檔改成 ghcr.io 的位址:

  app:
    image: ghcr.io/yourname/myapp:${APP_VERSION}

ghcr.io 新建立的套件預設是私有的,伺服器要先登入一次(使用具有 read:packages 權限的 Personal Access Token):

echo "你的 token" | docker login ghcr.io -u yourname --password-stdin

--password-stdin 從標準輸入讀取密碼,不會留在指令歷史中。

發布新版本的流程

設定完成後,發布新版本只需要:

git tag v1.2.0
git push origin v1.2.0

GitHub Actions 會依序:檢查 Dockerfile → 建置 amd64 與 arm64 → 推送 ghcr.io/yourname/myapp:1.2.0、1.2 → SSH 到伺服器把 APP_VERSION 改成 1.2.0 → 拉取並重建 app 容器。

要回滾時,在伺服器上把 APP_VERSION 改回舊版本再 docker compose up -d app 即可,舊的映像檔仍然在 ghcr.io 上。

重點回顧

  • Workflow 放在 .github/workflows/,用 on 決定何時執行。
  • 官方的 Docker Actions:setup-qemu、setup-buildx、login、metadata、build-push,組合起來就能建置並推送多平台映像檔。
  • 推送到 ghcr.io 用自動提供的 GITHUB_TOKEN,記得 packages: write。
  • metadata-action 依分支、版本 tag、PR 自動產生映像檔 tag。
  • cache-from/to: type=gha 讓重複建置變快。
  • 部署工作只在版本 tag 時執行,透過 SSH 修改 .env 並 pull + up -d。
  • 用 actionlint 在本機先檢查 workflow。

動手練習

練習 1:修改 workflow,讓推送到 main 時只建置 amd64(比較快),只有版本 tag 才建置兩個平台。

答案

platforms 也可以使用運算式:

          platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}

修改後用 actionlint 檢查一次。

練習 2:如果打了一個 tag release-1(不是 v 開頭),會發生什麼事?

答案

不會發生任何事。on.push.tags 只比對 v*,release-1 不會觸發 workflow;deploy 工作的 if 也只接受 refs/tags/v 開頭的 tag。

練習 3:用 actionlint 檢查你自己專案中的 workflow 檔,看看有沒有被忽略的錯誤。

常見問題

GitHub Actions 要付費嗎?

公開(public)儲存庫使用 GitHub 提供的執行環境是免費的;私有儲存庫每個月有免費額度,超過才需要付費。額度與價格以 GitHub 官方說明為準。

為什麼推送到 ghcr.io 不需要另外建立密碼?

每次執行 workflow 時,GitHub 會自動提供一個暫時的 GITHUB_TOKEN。只要在 workflow 的 permissions 加上 packages: write,就能用它推送到同一個帳號或組織的 ghcr.io。

伺服器拉取 ghcr.io 的映像檔時出現 unauthorized?

新建立的 ghcr.io 套件預設是私有的。伺服器要先用具有 read:packages 權限的 Personal Access Token 執行 docker login ghcr.io,或在 GitHub 的套件設定中把它改成公開。

多平台建置很慢怎麼辦?

arm64 在 GitHub 的 x86 執行環境上是用 QEMU 模擬,需要編譯的步驟會慢很多。可以只在打版本 tag 時才建置 arm64、使用 cache-from: type=gha 快取,或改用 arm64 的執行環境分別建置。能交叉編譯的語言(Go、Rust)參考 [多平台映像檔](/zh-tw/docker/multi-platform/) 的寫法。

更新於 2026年10月13日