GitHub Actions layered custom images入門——Next.js CIを階層化する
GitHub Actions custom imagesのレイヤー化を題材に、Next.js + FirestoreのCI環境を共通基盤とアプリ固有環境に分ける手順を解説します。

はじめに
GitHub ActionsのCIで、こんな待ち時間が積み上がっていませんか?
| 状況 | 問題 |
|---|---|
| PRごとにNode.jsやブラウザを準備する | yarn build前が長い |
| 複数リポジトリで同じツールを入れる | 手順が重複する |
| アプリごとに追加ツールが違う | 共通imageが肥大化する |
2026年6月18日、GitHubはGitHub-hosted larger runnersのcustom imagesについて、custom imageを別のcustom imageのベースにできるようになったと発表しました。あわせて、image snapshotを作るかどうかを条件で制御できるようになっています。
本記事では、架空の受注管理SaaS「OrderDock」を題材に、Next.js + Firestore + GitHub Actions + TypeScriptのCIを「共通基盤」と「アプリ固有環境」に分ける方法を解説します。
何が変わったのか
GitHub Docsでは、custom imagesはGitHub-hosted larger runnersで使う実行環境を事前に作成し、依存ツールや設定を先に入れておく仕組みとして説明されています。利用にはlarger runners、custom imagesの有効化、管理権限が必要です。
今回の更新でうれしいのは、画像を1枚に詰め込まず、次のように分けられる点です。
web-ci-baseにはNode.js、Yarn、共通の検証ツールを入れます。orderdock-nextjs-ciには、OrderDockだけで使うFirebase Emulatorやブラウザ検証を追加します。共通部分とアプリ固有部分を分けることで、CIを小さく保てます。
設計方針
OrderDockでは、CI環境を3層に分けます。GitHub-owned imageを土台に、週1回更新のweb-ci-baseを作り、その上にorderdock-nextjs-ciを重ねます。
GitHub Docsでは、custom imageから作った派生imageは、ベースimageの有効期限タイムラインを引き継ぐと説明されています。ベースを古いままにして派生imageだけ作り直しても、更新計画の問題は解決しません。
ハンズオン1: 共通ベースimageを作る
まず、Organization側でimage-generation用のlarger runnerを作成します。ここでは架空のrunner名を image-builder-linux-x64 とします。workflowでは、snapshotキーワードを使ってimage名を指定します。
name: Build web CI base image
on: workflow_dispatch
jobs:
build-base:
runs-on: image-builder-linux-x64
snapshot:
image-name: web-ci-base
version: 1.*
steps:
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "yarn"
- run: node --version
実運用ではこれを週次scheduleで動かします。ここで入れるものは、複数のWebアプリで共通して使うものだけにします。Firebaseの本番資格情報、アプリ固有の.env、顧客ごとの設定ファイルはimageに含めません。
ハンズオン2: OrderDock専用imageを重ねる
次に、GitHubのrunner設定で web-ci-base をベースにしたimage-generation runnerを用意します。OrderDock専用imageでは、Firestore Emulatorを使う結合テストの前提を追加します。ただし、Google CloudのサービスアカウントキーやFirebase本番プロジェクトIDはimageに入れません。
name: Build OrderDock CI image
on: workflow_dispatch
jobs:
build-orderdock:
runs-on: image-builder-orderdock
snapshot:
if: ${{ github.ref == 'refs/heads/main' }}
image-name: orderdock-nextjs-ci
version: 1.*
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "yarn"
- run: yarn install --frozen-lockfile
- run: yarn build
GitHub Docsでは、snapshotのmapping syntaxでifを使い、image snapshotを作る条件を制御できると説明されています。上の例では、main以外からimage versionを作らないようにしています。
ハンズオン3: PR用CIでcustom imageを使う
imageが生成され、通常実行用のlarger runnerにインストールされたら、PR用workflowのruns-onをそのrunner名に向けます。
name: PR Quality Gate
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: orderdock-nextjs-runner
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: yarn install --frozen-lockfile
- run: yarn build
custom imageを使ってもyarn installを完全に消すとは限りません。lockfileが変わったPRでは依存関係の検証が必要です。狙いは、OSパッケージや重い共通ツールの準備を毎回やらないことです。
Firestoreを使うテストは、本番DBへ接続せず、Emulatorかモックに寄せます。監査ログの保存処理はTypeScriptで薄く分けておくと、CIで検証しやすくなります。
import type { Firestore } from "firebase-admin/firestore";
type CiImageAudit = {
imageName: string;
imageVersion: string;
sourceSha: string;
};
export async function recordCiImageAudit(
db: Firestore,
audit: CiImageAudit,
) {
await db.collection("ciImageAudits").add({
...audit,
createdAt: new Date(),
});
}
image versionはGitHubの画面やjobログで確認し、リリース時に環境変数として渡す運用にします。確認できていないAPIを推測で呼び出すより、「どのimageを使う想定か」を監査ログに残すほうが安全です。
運用チェックリスト
custom imagesを導入すると、CIは速くなりますが、更新責任も増えます。OrderDockでは次のルールにします。
- ベースimageは週1回、派生imageはベース更新後に作る
snapshotを作るworkflowはmainまたは管理者の手動実行に限定する- image-generation runnerは専用runner groupに置く
- 本番secretsやサービスアカウントキーをimageに焼き込まない
- PR用CI、夜間結合テスト、デプロイworkflowでrunnerを分ける
特に3つ目は重要です。GitHub Docsも、production imageを生成するrunner groupを開発・テスト用リポジトリと共有しないことを推奨しています。
参考リンク
まとめ
GitHub Actions custom imagesのレイヤー化は、CI高速化の話でありながら、チームの責任分界を整理する話でもあります。
Next.js + Firestoreの業務アプリでは、共通のWeb CI基盤とアプリ固有の検証環境を分けることで、重いセットアップを減らしつつ、必要なツールだけを持つrunnerを作れます。まずは週次生成のベースimageを1つ作り、PR用CIで効果を測るところから始めるのが現実的です。