Docker Composeのbuildとimageの違いをVPSで確認
imageは公開済みタグを取得し、buildはVPS上でDockerfileから作成します。Dockerfile変更後もcompose upが再ビルドしない理由と、docker compose buildを使う修正方法を説明します。
Docker Compose の build と image の違い:簡単な答え
Docker Compose ファイルでは、image: にレジストリから取得するイメージを指定し、build: でこのマシン上に Dockerfile からイメージをビルドするよう Compose に指示します。image: だけを設定すると、Compose はそのタグを取得して実行します。build: だけを設定すると、Compose はこのマシン上でイメージをビルドし、プロジェクト名とサービス名から生成した名前を付けます。両方を設定すると、Compose はローカルでビルドした後、image: に指定した名前で結果にタグを付けます。これは、イメージをビルドし、任意の名前で push する方法です。
違いはこれだけです。以下では、サーバー上での動作上の意味を説明します。ここでは、Docker Engine と Compose plugin がすでにインストールされているものとします。VPS で Docker を実行するで、その手順を説明しています。
3 つの形式の全体像
公開済みのタグを取得して実行します。Dockerfile は一切使用しません。
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"現在のディレクトリにある Dockerfile からビルドします。取得されるのは FROM で指定されたベースイメージだけです。
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"ローカルでビルドし、結果にタグを付けます。その後、docker compose push でそのタグを正確にレジストリへ送信できます。
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context は builder に送信されるディレクトリです。dockerfile はそのコンテキストを基準に解決されるため、dockerfile: docker/prod.Dockerfile を指定した context: . は通常どおり正しい指定です。docker compose images を実行すると、各サービスコンテナが使用するイメージ名とイメージ ID を確認できます。実際に記述した形式がこの 3 つのどれかを確認する最も速い方法です。
docker compose up を実行しても、Dockerfile を変更した後に再ビルドされないのはなぜですか?
up はイメージが存在するかどうかを確認します。イメージが最新かどうかは確認しません。
Compose は、build: セクションを持つサービスを起動するとき、ローカルのイメージストアを検索します。その名前のイメージがすでに存在する場合、Compose はそのイメージを使用します。Dockerfile を読み取ったり、ソースファイルを比較したり、タイムスタンプを確認したりすることはありません。Compose 仕様では、このルールを pull_policy 属性として定義しています。デフォルトの動作では、イメージが存在しない場合にのみイメージをビルドします。存在していれば十分と判断されます。
そのため、app.py を編集して docker compose up -d を実行すると、Compose はコンテナが実行中であると報告しますが、古いコードが提供されます。処理は失敗していないため、警告も表示されません。これは、Compose で最もよくある「変更が反映されない」という報告です。判断材料になるのは、Compose がコンテナ名の横に表示するステータスです。Compose がコンテナを置き換えた場合は recreated または started と表示され、Compose がそのままにした場合は running と表示されます。
2 つの確認で原因を特定できます。docker compose images は各コンテナが使用しているイメージ ID を表示するため、デプロイ前に記録して比較します。docker image ls には CREATED 列があります。最終コミットより前に作成されたイメージは、デプロイスクリプトが何を表示したかに関係なく、古いイメージです。
再ビルドを強制するフラグ
docker compose up -d --buildは最初にビルドし、イメージが変更されたコンテナを再作成します。多くの場合、必要なのはこのフラグです。docker compose build webは 1 つのサービスをビルドするだけで、何も起動しません。続けてdocker compose up --no-deps -d webを実行すると、そのコンテナだけを置き換え、スタックの残りは稼働したままにできます。docker compose build --no-cache webはキャッシュされたすべてのレイヤーを破棄し、最初の命令から再ビルドします。docker compose build --pullはFROMにあるベースイメージの新しいバージョンを取得しようとします。そのため、node:22のような更新されるタグは、3 月にダウンロードしたコピーではなく、現在の内容を取得します。docker compose up -d --force-recreateは、既存のコンテナを現在使用しているイメージから再作成します。ビルドは行いません。--buildのつもりでこれを実行してしまうのは、よくある行き詰まりです。
この判断をファイルに記述することもできます。Compose specification の説明では、pull_policy: build は Compose がイメージをビルドし、すでに存在する場合は再ビルドすることを意味します。これにより、up のたびにビルドが発生します。これはノートパソコンでは適していますが、サーバーではほとんどの場合、望ましくありません。
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: buildもう 1 つ、組み合わせによる動作を知っておく必要があります。docker compose pull は、build セクションを持つサービスのイメージも取得しようとします。その取得に失敗すると、イメージをビルドする必要があることを通知します。--ignore-buildable を渡すと、これらのサービスを通知なしでスキップできます。
ビルドキャッシュがデプロイ時間を決める仕組み
Dockerfile の各命令はレイヤーを生成します。命令とその入力に変更がなければ、ビルダーはキャッシュされたレイヤーを再利用します。COPY の場合、入力はコピー対象ファイルの内容です。いずれかのレイヤーでキャッシュが失われると、その後のすべてのレイヤーが再ビルドされます。各レイヤーは、直前のレイヤーが生成したファイルシステムを基に構築されるためです。
このルールだけで、デプロイに数秒かかるか数分かかるかが決まります。Dockerfile は、変更頻度の低いものから、コミットごとに変更されるものの順に並べます。
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci は COPY . . の上に配置されます。そのため、ソースファイルを編集しても install レイヤーはキャッシュされたままで、ビルドは copy のステップから再開されます。この2行を入れ替えると、1文字の変更だけで依存関係がすべて再インストールされます。COPY . . によって、npm ci が基にするレイヤーが無効になるためです。同じ構成は pip install -r requirements.txt と go mod download にも当てはまります。
古いレイヤーが修正を隠していると疑われる場合は、--no-cache が適切なツールです。ただし、通常の既定値として使うのは適切ではありません。Dockerfile の順序付けによって得られる再利用をすべて破棄してしまうためです。
イメージが設定する内容のうち、Compose が上書きできるものが1つあります。Dockerfile の CMD は、イメージがデフォルトで実行する内容です。サービスの command: キーを指定すると、それに置き換わります。ここでは command と entrypoint の相互作用 が重要です。Compose による上書きによって、新しくビルドしたイメージが以前のイメージとまったく同じ動作をする場合があるためです。
ビルドコンテキストと.dockerignore
context: .は、最初の命令が実行される前に、そのディレクトリをComposeでパッケージ化してビルダーへ送信します。.gitを含め、ソースの隣に置いているデータディレクトリなど、その配下にあるすべてのファイルが対象になります。変更していないプロジェクトで、ビルドがコンテキストの転送処理のまま停止する場合、コンテキストが大きすぎることを示しています。
コンテキストのルートに.dockerignoreファイルを配置すると、転送対象からパスを除外できます。構文は.gitignoreに近い形式です。
.git
node_modules
*.log
data/
.env利点は2つあります。転送量が減るため、すべてのビルドが速く始まります。また、COPY . .が.envをイメージへコピーできなくなるため、そのイメージを取得した人が内容を読み出すこともできません。
時間の経過とともに遅くなるビルドの原因は、bind mountであることがあります。名前付きボリュームはプロジェクトディレクトリの外部にあります。一方、./data:/var/lib/postgresql/dataのようなbind mountはビルドコンテキスト内にあるため、データベースが大きくなるにつれて、毎週ビルドが遅くなります。.dockerignoreに1行追加すれば解決できます。より広いトレードオフについては、名前付きボリュームに対するbind mountを参照してください。
ビルド引数にも、同じリスクを小さくした形で存在します。args:で渡した値は、イメージを持つ誰でもイメージの履歴から確認できます。そのため、ここにはバージョン番号を指定し、トークンは絶対に指定しないでください。認証情報を配置する場所については、Composeの環境変数ファイルとSecretを参照してください。
VPS 上でビルドするべきか、それとも別の場所でビルドして pull するべきか
トラフィックを処理するサーバー上でビルドする方法がデフォルトです。経路が最短だからです。git pull、続いて docker compose up -d --build を実行します。まだ誰も依存していない小規模なサーバーであれば問題ありません。ただし、測定可能な2つの理由と、障害発生時に初めて表面化する1つの理由により、適さなくなります。
メモリ。 ビルドでは、稼働中のアプリケーションと同じ場所でコンパイラーやバンドラーを実行します。多くの構成では、これらが最も多くのメモリを消費します。1 GB の VPS では、JavaScript バンドラーや Rust のコンパイル処理が、サーバー上で通常もっとも大きなプロセスになります。カーネルのメモリが不足すると、最大のプロセスを強制終了します。ビルドが Killed と終了ステータス 137 で停止する場合もあれば、代わりにデータベースが強制終了され、デプロイの途中でサイトが停止する場合もあります。dmesg -T | grep -i oom はプロセス名とともに強制終了の行を表示するため、推測せずにどちらが発生したかを確認できます。
ディスク。 ビルドのたびにレイヤーが残り、ビルダーはイメージとは別に独自のキャッシュを保持します。docker system df で両方を確認できます。ビルドキャッシュの行は増え続けます。未使用のイメージは docker image prune、キャッシュされたレイヤーは docker builder prune で削除して容量を回収します。ディスクが満杯になると、ビルドだけでなく、データベースも書き込みを停止します。この障害による損失は、デプロイが遅くなる場合よりはるかに大きくなります。
再現性。 サーバー上でビルドしたイメージは、そのサーバーにしか存在しません。ロールバックするには、以前のコミットを checkout して再度ビルドする必要があります。しかし、ベースタグやパッケージミラーが更新されているため、以前と同じ結果になる保証はありません。別の場所でビルドしてタグを push すれば、ロールバックは編集作業になります。image: を以前のタグに向けて、docker compose up -d を実行するだけです。
安定して運用できる構成は単純です。継続的インテグレーションでビルドを実行して registry.example.com/acme/web:<git-sha> を push し、VPS 上の Compose ファイルでは image: を指定します。build: キーは一切指定しません。これでデプロイは、メモリをほとんど必要としない2つのコマンドだけになります。
docker compose pull
docker compose up -dサーバー上で docker login registry.example.com を1回実行すると、それ以降 Compose でプライベートタグを pull できます。
削除するのではなく、開発用として build セクションを残し、自分で名前を付けたファイルに分離してください。
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --buildそのファイルには compose.dev.yaml という名前を付け、compose.override.yaml にはしないでください。Compose は、override ファイルが存在すると自動的に読み込みます。そのため、意図せずコピーされた override がサーバー上にあると、そこで再びビルドが開始されます。複数の Compose ファイルを重ねるでは、各キーのマージ結果を説明しています。
別の環境でビルドするときのアーキテクチャの落とし穴
イメージには、ビルド対象の CPU アーキテクチャが含まれます。Apple Silicon のラップトップでビルドして push し、そのタグを x86_64 の VPS に pull すると、Docker は要求されたイメージの platform が検出したホストの platform と一致しないと警告します。その後、プロセスは exec format error で終了します。このメッセージはバイナリが破損しているように見えますが、実際には破損を示していません。対象環境向けに明示的にビルドします。
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .ラップトップが x86 で、x86 ではなく ARM の VPS を使用する場合も、同じ不一致が発生します。デプロイ先と同じアーキテクチャで CI にビルドさせれば、この問題を避けられます。
デプロイ後に確認すること
docker compose imagesは、実行中のすべてのコンテナで使用しているイメージとタグを表示します。イメージ ID が変わっていれば、新しいビルドが稼働している証拠です。docker compose configは、変数を置換した後の結合済みファイルを表示します。何も実行する前に、Compose が使用する最終的なイメージ名を確認できます。docker compose logs -f webは、切り替え後の最初の 30 秒間実行します。起動して終了するコンテナは稼働し続けず、再起動ループに入ります。確認しない限り、このループは気付きにくい状態で続きます。docker image lsは CREATED 列を表示します。最後のコミットより古いイメージは、再ビルドされていません。
これらの確認対象となるファイルをまだ組み立てている段階であれば、VPS 上の Compose ファイルの基本で関連するキーを確認できます。Compose コマンドのチートシートには、その他のサブコマンドがまとめられています。
FAQ
build と image を同じ service で使用できますか?
はい。自分でビルドするプロジェクトでは通常の構成です。Compose は build: セクションの内容からビルドし、結果に image: の値でタグを付けます。このタグを docker compose push が registry に送信し、別のマシンが pull します。image: key がなくても Compose はビルドしますが、project と service の名前を組み合わせて image 名を付けます。また、属性がないため image を push できないという警告を表示します。
Dockerfile を変更したのに docker compose up が反映しないのはなぜですか?
up は、その名前の image が存在するかどうかだけを確認するためです。image が存在すると、Compose はそれを起動し、Dockerfile や source files との比較は行いません。docker compose up -d --build を実行するか、docker compose build web に続けて docker compose up --no-deps -d web を実行して、単一の service を置き換えてください。service に pull_policy: build を設定すると、すべての up で rebuild されるため、開発用マシンに適しています。
--build と --force-recreate の違いは何ですか?
--build は image を再度 build し、image が変更された container を再作成します。--force-recreate は、現在使用している image から container を再作成するだけなので、code の変更を反映できません。source または Dockerfile を変更した場合は、--build が使用する flag です。--force-recreate は、同じ image を維持したまま、書き込み可能な layer を消去するなど、container 自体をリセットする場合に使用します。
Docker image は VPS 上で build すべきですか、それとも別の場所で build すべきですか?
別の場所で build し、サーバーが network traffic も処理するようになったら tag を pull してください。build はアプリケーションと memory を競合します。小規模な VPS では、kernel が最大の process を kill することでこの競合を解消する場合があり、その process が build のことも database のこともあります。build によって disk 上に cache も残りますが、これは自動的には reclaim されません。ユーザーがいない小規模なプロジェクトであれば、server 上で build しても問題ありません。build: セクションを開発専用の Compose file に残しておけば、後で移行する場合も手間はかかりません。
Docker build cache によって disk がいっぱいになるのを防ぐにはどうすればよいですか?
docker system df を実行すると、image と build cache がそれぞれどれだけの容量を使用しているか確認できます。docker builder prune は cache された layer を削除し、docker image prune は以前の build で残った dangling image を削除します。どちらかに -a を追加すると、より積極的に削除し、次回の build は cache のない状態から開始します。server では docker system prune -af --volumes をスケジュール実行しないでください。--volumes は、現在どの container も使用していない volume を削除します。メンテナンスのために停止した stack では、database がまさにそのような volume に保存されている可能性があります。