目錄
用 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 沒有壞掉。
專案結構
沿用 部署到雲端主機 的 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 |
|---|---|
推送到 main | main、sha-1a2b3c4 |
推送 tag v1.2.0 | 1.2.0、1.2、sha-1a2b3c4 |
| Pull Request #7 | pr-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 | 同時建置兩種架構,推送後成為一個多平台映像檔 |
push | Pull 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_USER | SSH 使用者名稱 |
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日