目次

GitHub Actions による Docker イメージの自動ビルドとプッシュ(CI/CD)

クラウドサーバーへのデプロイでは、ビルド、プッシュ、サーバーの更新をすべて手動で行いました。この章では GitHub Actions を使って自動化します。

  • main にプッシュするたびに、イメージを自動でビルドしてプッシュします。タグは main です。
  • バージョンタグ(例:v1.2.0)を作成すると、amd64 + arm64 イメージをビルドし、1.2.0、1.2 などのタグでプッシュしてから、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 で始まるタグのプッシュ、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 #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
パラメーター説明
platforms2 種類のアーキテクチャを同時にビルドします。プッシュすると、1 つのマルチプラットフォームイメージになります
pushPull Request の場合は false となり、ビルドのみ実行します
build-argsDockerfile の ARG APP_VERSION に対応します。アプリケーションが自身のバージョンを表示できます
tags / labels前のステップにある metadata-action の出力を使います
cache-from / cache-to: type=ghaGitHub 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_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 "あなたのトークン" | 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日