
BuildKitは、Dockerの新しいビルドエンジンとして設計された仕組みで、従来のdocker buildより高速・柔軟・キャッシュ活用が得意です。docker buildxはそのBuildKitをフル活用するためのフロントエンドで、マルチプラットフォームビルドやCIでのキャッシュ共有を簡単にしてくれます。この記事では、BuildKitと従来ビルドの違い、buildxとの関係、キャッシュの種類、Dockerfileの書き方のコツ、そしてありがちな落とし穴まで順番に整理します。
1. BuildKitとは(結論から)
1-1. BuildKitの位置づけ:Dockerの“ビルドエンジン”
まず最初に押さえたいのは、BuildKitは「Dockerとは別物のツール」というより、Dockerの中で動くビルド専用エンジンだという位置づけです。コンテナイメージを作るとき、docker build の裏側では「Dockerfileを読む → レイヤを組み立てる → イメージを生成する」という処理が走っていますが、その“中身”を担うのがビルダー(builder)です。従来は「レガシービルダー」と呼ばれる古い仕組みが使われていましたが、今はBuildKitがその役割を置き換えつつあります。
BuildKitは、もともとDocker本体とは独立したプロジェクトとして開発されていて、CLIやDockerデーモンとは別プロセスで動くこともできます。そのため、Dockerの外から利用したり、Kubernetesクラスター上でビルドしたりといった拡張も可能になっています。日常的には「Dockerの設定でBuildKitをONにすると、新しいビルドエンジンが裏で使われる」という理解で問題ありません。
1-2. 従来ビルド(legacy builder)との違い:速さ・再現性・拡張性
従来のビルダーとBuildKitの違いを一言で言うと、「速さ・再現性・拡張性を優先して作り直したビルド基盤」です。従来のdocker buildは実装がシンプルな分、ビルドの依存関係をあまり賢く見ておらず、「上から順に命令を実行していく」動きに近いものでした。そのため、並列実行や柔軟なキャッシュ利用はあまり得意ではありませんでした。
BuildKitでは、Dockerfileの構造から依存関係を解析し、「ここは独立しているから同時に実行できる」といった判断ができるようになっています。また、キャッシュを別のストレージ(レジストリやCIサービス)に保存・復元できる設計が初めから組み込まれているため、ビルドの再現性やスピードを保ちやすくなっています。さらに、フロントエンド/バックエンドという構造に分けることで、Dockerfile以外のビルドフロントエンドを追加したり、出力形式を柔軟に変えたりできる拡張性も持っています。
1-3. 何が嬉しいかを一言で:高速化+キャッシュ共有+機能拡張
現場目線でBuildKitのメリットをまとめると、主に次の3つです。「ビルドが速くなる」「キャッシュを賢く使い回せる」「ビルドの機能が増える」です。単純にDOCKER_BUILDKIT=1を有効にするだけでも、依存関係の並列実行やキャッシュの最適化によって体感が変わるケースは多いです。特に大きな依存ライブラリをインストールするようなDockerfileでは違いが出やすくなります。
もう一つの大きなポイントが「キャッシュ共有」と「拡張性」です。CI環境などでは、毎回ゼロからビルドしていると時間とコストがかかりますが、BuildKitならレジストリやCIサービスにキャッシュを保管しておき、次回以降のビルドで再利用することが容易になります。また、BuildKit専用構文(--mount=type=cacheなど)も使えるようになり、npmやaptなどツール側のキャッシュも賢く永続化できるようになります。
2. buildxとは(BuildKitを使うための入口)
2-1. docker build と docker buildx build の関係
BuildKitの名前は聞いたことがあっても、「どうやって使うの?」となるとdocker buildxというコマンドが絡んできます。ざっくり言うと、docker buildxはBuildKit用のフロントエンドで、「BuildKitの機能を全部引き出したdocker buildの強化版」だと思っておくとイメージしやすいです。最近のDockerでは、docker buildも内部的にBuildKitを使えるようになっていますが、buildxの方が設定・機能面でリッチです。
普段のビルドであれば、docker buildだけでも問題ないケースは多いです。ただ、マルチプラットフォームのイメージを一度に作りたいときや、remote cache、カスタムbuilderを使いたいときは、docker buildx buildを使う必要があります。つまり、buildxは「BuildKitの機能をフルで使うためのコマンドセット」という立ち位置です。
2-2. builderインスタンスの概念(どこでビルドしているか)
buildxならではの考え方として、「builderインスタンス」という概念があります。これは「ビルドをどこで実行するか」を表す論理的なビルド環境です。例えば、ローカルDockerデーモンに紐づいたbuilder、Dockerコンテナとして動くbuilder、リモートのDockerホストやKubernetesクラスター上のbuilderなど、複数のビルド場所を切り替えて使うことができます。
典型的な操作としては、docker buildx createで新しいbuilderを作り、docker buildx useでそれをアクティブにする流れがあります。ビルドを分離したコンテナの中で動かしたい場合や、CI用に専用のリモートbuilderを用意したい場合などに便利です。「どこのCPU・どこの環境でビルドが走っているか」を意識してコントロールできるようになるイメージです。
2-3. 使い所:マルチプラットフォームビルド、キャッシュ共有、CI最適化
buildxを使う一番分かりやすいメリットは、マルチプラットフォームビルドです。例えば、linux/amd64とlinux/arm64向けのイメージを一度のビルドでまとめて作り、レジストリにプッシュしたい場合、buildxの--platformオプションが非常に便利です。開発マシンがMac(arm64)でも、x86_64向けのイメージを一緒に作れるため、環境差を気にせず配布しやすくなります。
また、buildxは--cache-from/--cache-toオプションを通じてリモートキャッシュを扱いやすくしてくれます。CI環境でレジストリやGitHub ActionsのキャッシュにBuildKitのキャッシュを保存しておくことで、「前回のビルド結果を次回も流用する」というパターンが簡単になります。結果として、ビルド時間の短縮だけでなく、CIの安定性向上にもつながります。
3. “速い”理由:従来ビルドと比べた改善点
3-1. 依存関係を見て並列に進める
BuildKitの「速さ」の大きな理由のひとつは、Dockerfileの依存関係を理解して、実行できるところから並列に進める設計になっている点です。従来のビルダーは、ほぼ上から順番に命令を処理していましたが、BuildKitは「このステージとあのステージは独立しているから一緒に進められる」といった判断ができます。特にマルチステージビルドでは、並列化の効果が出やすくなります。
また、各命令ごとのキャッシュの有無も見ながら、「キャッシュヒットしたところはスキップし、キャッシュがないところだけ実行する」といった最適化も行われます。これにより、ちょっとした変更しかしていない場合でも、無駄な再ビルドを最小限に抑えられます。「できるだけ同時に、必要なところだけ動かす」という方針で高速化しているイメージです。
3-2. キャッシュの扱いが強い(inline/remoteなど)
BuildKitは最初から「キャッシュを色々な場所に持ち出す」ことを想定して設計されています。従来のビルドでは、キャッシュは基本的にローカルのDockerデーモン内に閉じていましたが、BuildKitではイメージと一緒にキャッシュメタデータを埋め込んだり(inline cache)、レジストリや別ストレージにキャッシュだけを保存したり(remote cache)できます。
これによって、ローカルマシン間やCIジョブ間でキャッシュを共有しやすくなり、「1回目に時間をかけてビルド → 2回目以降はどこから実行しても速くなる」という状態を作りやすくなります。キャッシュに関するオプションが多いのは最初少しとっつきにくく感じますが、「どこにキャッシュを書いて」「どこから読むか」を分けて考えると整理しやすいです。
3-3. 出力の柔軟性(イメージ以外にも出せる)
もうひとつの特徴が、出力形式の柔軟さです。従来は「ビルドしたらDockerイメージとしてローカルに保存する」くらいしか選択肢がありませんでしたが、BuildKitでは--outputオプションを利用して、さまざまな形式に出力できます。例えば、OCI形式のイメージエクスポート、tarファイル、特定ディレクトリへのファイル展開などがサポートされています。
これにより、「イメージはレジストリへ送るけど、ビルド成果物だけローカルに残したい」といったユースケースに対応しやすくなります。CIでビルドしたバイナリをアーティファクトとして保存したい場合や、Dockerをイメージビルダー兼アーティファクトビルダーとして使いたい場合などに、BuildKitの柔軟な出力機能が役に立ちます。
4. キャッシュ機能が本体:BuildKitのキャッシュ種類
4-1. レイヤキャッシュ(Dockerfile命令単位の基本キャッシュ)
まず基本となるのが、Dockerfile命令ごとのレイヤキャッシュです。これは従来のビルダーにもあった仕組みで、「ある命令の入力(ベースイメージ+命令内容+直前までの状態)が同じなら、結果のレイヤを使い回す」という考え方です。BuildKitでもこの考え方は同じですが、キャッシュの読み書きがより柔軟になっています。
レイヤキャッシュを最大限活かすためには、Dockerfileの命令順序が重要になります。頻繁に変わる部分(アプリケーションのソースコードなど)を後ろに、あまり変わらない部分(依存ライブラリのインストールなど)を前に置くことで、「変わらない命令のキャッシュを何度も使う」ことができます。このあたりは5章で具体的に触れていきます。
4-2. --mount=type=cache(ツールのキャッシュを永続化)
BuildKitに特有の便利機能として、RUN命令内で使える--mount=type=cacheがあります。これはビルドコンテナの中の特定ディレクトリを、「ビルドごとに使い回せるキャッシュ領域」としてマウントする仕組みです。aptのキャッシュ、npmのnode_modulesや~/.npm、pipのキャッシュなどを指定しておくと、次回ビルド時にもダウンロード済みのパッケージを流用できて速くなります。
# syntax=docker/dockerfile:1.7-labs
FROM node:20
RUN --mount=type=cache,target=/root/.npm \
npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.pnpm-store \
pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
このDockerfileでは、BuildKit専用構文を有効にするために先頭で# syntax=...を指定しつつ、npmやpnpmのキャッシュディレクトリを--mount=type=cacheで指定しています。この書き方にしておくと、ビルドごとに依存ライブラリをゼロからダウンロードするのではなく、「前回のキャッシュ」を使い回してインストールを高速化できます。
4-3. remote cache(CIで効くやつ):cache-to / cache-from
ローカルだけでなく、CI環境で威力を発揮するのがリモートキャッシュ(remote cache)です。BuildKitでは、--cache-toと--cache-fromオプションを使って、「キャッシュをどこに保存するか」「どこから読み込むか」を指定できます。よくあるパターンとしては、「キャッシュをコンテナレジストリに保存し、次回以降のビルドでそこから読み込む」という使い方があります。
例えば、buildxで次のように指定します。
docker buildx build \
--cache-to type=registry,ref=example.com/myapp:buildcache,mode=max \
--cache-from type=registry,ref=example.com/myapp:buildcache \
-t example.com/myapp:latest .
このコマンドでは、レジストリexample.com上のmyapp:buildcacheというタグにキャッシュを書き出し、次回ビルド時にはそこからキャッシュを読み込むよう指定しています。GitHub ActionsのBuildxアクション(docker/build-push-action)などもこの仕組みを内部で使っており、CIごとに最適なキャッシュ保存先(レジストリ、GitHub Cacheなど)を選べるようになっています。
5. Dockerfileで効くポイント:BuildKit前提の書き方
5-1. # syntax=とBuildKit専用構文を使う前提づくり
BuildKitの機能をフルで使うには、Dockerfileの先頭でフロントエンド(syntax)を宣言するのが定番です。例えば、Docker公式のDockerfileフロントエンドを使う場合は、次のように書きます。
# syntax=docker/dockerfile:1.7-labs
FROM ubuntu:24.04
...
この宣言を入れておくことで、--mount=type=cacheや--mount=type=secretなどのBuildKit拡張構文を利用できるようになります。古いバージョンのDockerや、BuildKit無効な環境では動かない可能性もあるため、「この環境はBuildKitが前提です」というメッセージとしても機能します。
チーム内でDockerfileを共有する場合は、「新しく書くDockerfileは基本このsyntaxを入れる」「BuildKitを無効にした状態はサポートしない」といったルールを決めておくと運用がスムーズです。逆に、レガシー環境と兼用する必要がある場合は、BuildKit専用構文をどこまで使うかを慎重に決める必要があります。
5-2. “変わりにくい物を先”の原則(依存→ソースの順)
BuildKitでも従来と同様、「変わりにくい命令を上に、頻繁に変わる命令を下に」という原則はとても重要です。例えば、Node.jsアプリであれば、最初にpackage.jsonやpnpm-lock.yamlをコピーして依存インストールを行い、その後にアプリケーションのソースコード全体をコピーする、というパターンがよく使われます。依存ファイルが変わらなければインストール命令のキャッシュが効き、ソースコードの変更だけなら最後のRUN buildだけが実行されます。
また、.dockerignoreでビルドコンテキストをきちんと絞ることも大事です。Git履歴やビルド成果物、IDEの一時ファイルなどがコンテキストに含まれていると、ちょっとした変更で前のステップのキャッシュが無効になってしまいます。.dockerignoreを整えることで、「本当に必要なファイルだけがビルドコンテキストに入る」状態を作り、キャッシュヒット率を高めることができます。
5-3. マルチステージとキャッシュ境界(builder/runtimeの切り方)
マルチステージビルドは、BuildKitとも非常に相性が良いです。典型的には、builderステージでコンパイルやビルドを行い、その成果物だけをruntimeステージにコピーする構成です。これによって、最終的なイメージを小さく保ちつつ、ビルド環境は自由度高く保てます。BuildKitのキャッシュもステージごとに効くため、builder側の命令順序を工夫することでビルド時間をさらに短縮できます。
一方で、「どこでステージを分けるか」がキャッシュ境界にも影響します。例えば、依存インストールとビルドを別ステージに分けるか同じステージにまとめるかで、キャッシュの効き方が変わります。依存インストール部分を専用のステージに切り出し、そのステージを他のビルドでも共通利用する、といった設計も可能です。マルチステージは「イメージサイズ」だけでなく「キャッシュの単位」を考えるツールとしても活用できます。
6. よくある落とし穴(BuildKitなのに遅い/効かない)
6-1. CIでキャッシュを保存していない(ローカルだけ速い問題)
よくあるパターンが、「ローカルでは速いのに、CIでは毎回遅い」というものです。これは多くの場合、「ローカルではBuildKitのキャッシュが効いているが、CIではビルドごとにキャッシュが捨てられている」状態になっています。CIのジョブは毎回クリーンな環境からスタートすることが多いため、明示的にリモートキャッシュやサービス側のキャッシュ機能を使わないと、BuildKitのメリットを活かしきれません。
例えばGitHub Actionsの場合、公式のdocker/build-push-actionにはBuildKitキャッシュ統合のオプションが用意されています。そこを使ってレジストリやGitHub Cacheにキャッシュを保存しておくことで、次回以降のビルドで同じキャッシュを再利用できるようになります。ローカルだけで速くても、チーム開発やCI/CDで速くならないと恩恵は限定的なので、「CI側でキャッシュが生きているか?」を最初にチェックするのがおすすめです。
6-2. ビルドコンテキストが毎回変わる(.gitや成果物が混ざる)
もう一つの落とし穴は、ビルドコンテキストが毎回大きく変わってしまうパターンです。例えば、リポジトリルートでdocker build .しているのに、.dockerignoreで.gitやビルド成果物、node_modulesなどを除外していないと、コミットするたびにコンテキストの中身が変わり、Dockerfileの早い段階の命令までキャッシュが効かなくなります。
特に、COPY . .のような命令を早い段階に置いていると、「少しの変更でレイヤキャッシュが壊れる」という状況になりがちです。これを避けるためには、依存ファイルだけを先にコピーしてインストールするステップと、ソースコード全体をコピーするステップを分けることが重要です。あわせて、.dockerignoreで「ビルドに不要なもの」を徹底的に弾いておきましょう。
6-3. --pull/ベース更新ポリシーでキャッシュが毎回壊れる
最後の落とし穴として、ベースイメージの更新方針があります。docker buildやdocker buildx buildに--pullを付けて常に最新のベースイメージを取得する設定にしている場合、ベースイメージが更新されるたびに、そこから先のキャッシュが大きく無効になります。これはセキュリティ的には良い面もありますが、ビルド時間が不安定になりやすいというデメリットもあります。
現実的には、「日常の開発ビルド」と「定期的なセキュリティ更新ビルド」を分けて運用するケースが多いです。開発中は--pullなしでキャッシュ優先、本番リリース前や週次のセキュリティビルドで--pullありにしてベースイメージを更新する、といったルールをチームで決めておくと、ビルド時間と安全性のバランスを取りやすくなります。
7. まとめ
7-1. BuildKitは「速いdocker build」ではなく、ビルド基盤
ここまで見てきたように、BuildKitは単に「従来より速いdocker build」というより、キャッシュ共有と拡張性を重視したビルド基盤と捉えた方がしっくりきます。依存関係の並列実行、柔軟なキャッシュ保存先、BuildKit専用構文、複数の出力形式など、イメージビルドを一段上のレイヤから設計し直すための仕組みが揃っています。
一方で、「BuildKitをONにしただけ」で全てが勝手に速くなるわけではありません。Dockerfileの命令順序や.dockerignoreの整理、CIでのキャッシュ保存、ベースイメージ更新ポリシーなど、ビルド全体の設計を見直すことで初めて、BuildKitのポテンシャルを引き出せます。ビルド周りを技術的負債のままにせず、「ちゃんと設計する対象」として扱うきっかけにしてみてください。
7-2. 最短で効く導入順:buildx有効化 → Dockerfile見直し → remote cache
最後に、現場での導入ステップをざっくりまとめます。まずは、DOCKER_BUILDKIT=1やdocker buildxを有効にして、新しいビルドエンジンを使うところから始めます。その上で、Dockerfileの命令順序と.dockerignoreを見直し、「変わりにくいものを先に」「ビルドコンテキストを小さく安定させる」方針を徹底します。
次のステップとして、CI環境にremote cache(--cache-to/--cache-from)を導入し、ビルド結果をレジストリやCIのキャッシュに保存・再利用できるようにします。ここまでできれば、「ローカルでもCIでもBuildKitのキャッシュが効いて、ビルドが安定して速い」という状態にかなり近づきます。そこから先は、マルチプラットフォームビルドや--mount=type=cacheなどの細かいチューニングを少しずつ取り入れていくとよいと思います。
8. 参考リンク
- Docker Build(BuildKit概要)
https://docs.docker.com/build/buildkit/ - docker buildx(公式ドキュメント)
https://docs.docker.com/build/buildx/ - Dockerfile frontend / BuildKit syntax
https://docs.docker.com/build/dockerfile/frontend/ - GitHub Actions: docker/build-push-action(Buildx+キャッシュ例)
https://github.com/docker/build-push-action