目次
GitHub Actions による Docker イメージの自動ビルドとプッシュ(CI/CD)
クラウドサーバーへのデプロイでは、ビルド、プッシュ、サーバーの更新をすべて手動で行いました。この章では GitHub Actions を使って自動化します。
mainにプッシュするたびに、イメージを自動でビルドしてプッシュします。タグはmainです。- バージョンタグ(例:
v1.2.0)を作成すると、amd64 + arm64 イメージをビルドし、1.2.0、1.2などのタグでプッシュしてから、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で始まるタグのプッシュ、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 に変更します。
イメージのタグを自動で決める
docker/metadata-action は、workflow を起動したイベントに応じてタグを生成します。
| トリガーイベント | 生成されるタグ |
|---|---|
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 | 2 種類のアーキテクチャを同時にビルドします。プッシュすると、1 つのマルチプラットフォームイメージになります |
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 に次の 3 つの値を設定します。
| 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 "あなたのトークン" | 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 コンテナの pull と再作成を行います。
ロールバックする場合は、サーバーで 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はブランチ、バージョンタグ、PR に応じてイメージタグを自動生成します。cache-from/to: type=ghaを使うと、繰り返しのビルドが速くなります。- デプロイ用ジョブはバージョンタグのときだけ実行し、SSH 経由で
.envを変更してからpullとup -dを実行します。 - actionlint を使って、事前にローカルで workflow をチェックします。
練習問題
練習 1:workflow を変更して、main へのプッシュ時は amd64 のみをビルド(より高速)し、バージョンタグの場合だけ 2 つのプラットフォームをビルドするようにしてください。
解答
platforms には式も使えます:
platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
変更後、actionlint で一度チェックしてください。
練習 2:release-1(v で始まらない)というタグを付けると、どうなりますか?
解答
何も起こりません。on.push.tags は v* にのみ一致するため、release-1 では workflow は起動しません。deploy ジョブの if も refs/tags/v で始まるタグのみを受け付けます。
練習 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 のパッケージ設定で公開に変更してください。
マルチプラットフォームビルドが遅い場合はどうすればよいですか?
GitHub の x86 実行環境では arm64 を QEMU でエミュレートするため、コンパイルが必要なステップは大幅に遅くなります。バージョンタグを作成したときだけ arm64 をビルドする、cache-from: type=gha でキャッシュする、または arm64 の実行環境で個別にビルドする方法があります。クロスコンパイルできる言語(Go、Rust)については、[マルチプラットフォームイメージ](/ja/docker/multi-platform/) の方法を参照してください。
更新日 2026年10月13日