+ 作る
図を読み込んでいます…
1 / 9
Dockerを理解しよう / 全3章

第2章 Docker を理解しよう:イメージはどこから来るのか、Dockerfile・ビルド・レジストリ

発行
かえで
版
第2版
改訂
分量
9コマ・4,132字

イメージの2つの入口、作る(Dockerfile と docker build)と取ってくる(レジストリと docker pull)。1行1レイヤー、ソースのビルドは含まれるのか、レイヤーキャッシュで CI は毎回ゼロから作らないこと、環境とソースを分ける考え方、イメージ名の読み方、なぜ会社のイメージは非公開か、CI と手元がレジストリを経由する理由まで。

イメージはどこから来るのか:作るか、取ってくるか

前のアニメモで、イメージは「インストール済みの小さな Linux のディスク」だと分かりました。では、そのイメージはどこから来るのでしょうか。入口は2つです。1つは自分で作る道。Dockerfile というレシピを書いて docker build すると、手元にイメージができます。もう1つは取ってくる道。レジストリと呼ばれる倉庫から docker pull で手元にダウンロードします。

自分のアプリのイメージは前者で作り、PostgreSQL や Node のような既製品は後者で取ってきます。そして自分で作ったイメージを docker push で倉庫に上げれば、他の人や本番サーバーが後者の道で取ってこられます。どちらの道で来ても、手元に置かれたイメージは同じもので、あとは docker run するだけです。

このアニメモでは、まず作る側(Dockerfile・build・レイヤーキャッシュ)を、次に取ってくる側(レジストリ・イメージ名・なぜ非公開か・CI との関係)を追います。

  • イメージはどこから来るのか:作る(build)か、取ってくる(pull)か
  • Dockerfile (作り方のレシピ)
  • イメージ myapp:1.0
  • レジストリ (イメージの倉庫)
  • docker build
  • docker pull
  • docker push
  • 自分のアプリはこちら (レシピから作る)
  • 公式イメージや会社のイメージは こちら(倉庫から取ってくる)
  • docker run
  • コンテナ
  • どちらの入口から来ても、手元に置かれたイメージは同じもの。あとは run するだけ

Dockerfile:1行が1レイヤーになる

Dockerfile は「イメージをどう組み立てるか」を上から順に書いたテキストファイルです。FROM で土台になるイメージを指定し、COPY でファイルを入れ、RUN でコマンドを実行し、CMD で起動時に実行するコマンドを決めます。

docker build はこのファイルを上から順に実行し、各行が終わるたびに「その時点のファイルシステムの差分」を1枚の層として固めます。だから Dockerfile の行数とイメージのレイヤー数はほぼ一致します。図の例では、土台の上に作業ディレクトリ、package.json、依存ライブラリ、ソースコード、起動コマンドが順に積まれています。

なぜ package.json を先にコピーして npm ci し、ソース全体はその後にコピーするのか。これは2ページ先のレイヤーキャッシュの話で効いてきます。

  • Dockerfile:1行が1レイヤーになる
  • Dockerfile(上から順に実行)
  • レイヤー(下の行ほど上に積まれる)
  • FROM node:22-slim
  • 土台: Debian + Node 22
  • WORKDIR /app
  • 作業ディレクトリ /app
  • COPY package*.json ./
  • package.json のコピー
  • RUN npm ci
  • 依存ライブラリ(重い。数分かかる)
  • COPY . .
  • ソースコード(テストも含む)
  • CMD ["node", "server.js"]
  • 起動コマンド
  • docker build -t myapp:1.0 . → 上から順に実行し、各行の結果(ファイルの差分)を層として固める
  • Dockerfile の各行を上から実行し、結果を層として積む。これが docker build

「ソースのビルド」は含まれるのか

よくある疑問が「docker build には、ソースコードのビルド(コンパイル)も含まれるのか」です。答えは、Dockerfile に書いてあれば含まれるし、書いてなければ含まれない。docker build は「書かれた命令を順に実行して固める」だけの道具で、中で何をするかは書く側の自由です。

左の例はスクリプト言語の典型で、ランタイムと依存ライブラリを入れ、ソースをコピーするだけ。コンパイル工程はありません。右の例は Go のようにコンパイルが要る言語で、RUN go build の行で実際にソースをビルドしています。さらに「ビルド用の重い箱でコンパイルし、できた実行ファイルだけを軽い箱にコピーする」という2段構え(マルチステージビルド)にしていて、本番用イメージにコンパイラやソースを残さないようにしています。

「会社のイメージは外に出せない」のは、ビルド工程が秘密だからではありません。結果物であるイメージの中に、プロダクト本体が入っているからです。この話は後のページで戻ってきます。

  • 「ソースのビルド」は Dockerfile に書いてあれば含まれる
  • スクリプト言語(コンパイル工程なし)
  • コンパイルが要る言語(マルチステージビルド)
  • FROM node:22-slim COPY package*.json ./ RUN npm ci COPY . . CMD ["node", "server.js"]
  • FROM golang:1.23 AS builder COPY . . RUN go build -o /server . FROM debian:slim COPY --from=builder /server /server CMD ["/server"]
  • ランタイムと依存を入れてソースをコピーするだけ。 ビルド工程は無い
  • RUN go build の行でソースをビルド。 重いビルド用の箱でコンパイルし、 できた実行ファイルだけを軽い箱へコピー
  • docker build は「書かれた命令を順に実行するだけ」。 コンパイルを書けば含まれるし、書かなければ含まれない
  • 外に出せない理由はビルド工程ではなく、結果物(イメージ)にプロダクト本体が入っているから
  • docker build = 命令を順に実行して固めるだけ。何をするかは Dockerfile 次第

レイヤーキャッシュ:変わった行から下だけ作り直す

PR を出すたびに CI が docker build しているなら、毎回ゼロから数分かけているのか、という疑問が出ます。ここで効くのがレイヤーキャッシュです。Docker は各行について「命令の内容と、入力になるファイルが前回と同じなら、前回作った層をそのまま再利用する」という動きをします。

テストを1つ追加したとします。変わったのはソースファイルだけなので、FROM・WORKDIR・package.json のコピー・npm ci までは前回と同じ。キャッシュがそのまま使われます。作り直されるのは COPY . . とそれより下の行だけです。いちばん重い依存のインストールを飛ばせるので、「毎回ビルドする」けれど「毎回ゼロから」ではありません。package.json を先にコピーしておくのは、まさにこのためです。

CI のマシンは使い捨てで、手元にキャッシュが残らないことが多いので、「前回ビルドしたイメージをレジストリから pull してきてキャッシュとして使う」というやり方がよく取られます。CI がイメージを取ってきている理由の1つがこれです。

  • レイヤーキャッシュ:変わった行から下だけ作り直す
  • 2回目の build(テストを追加したあと)
  • FROM node:22-slim
  • キャッシュ
  • 各行は「命令 + 入力ファイル」が前回と同じなら、 前回の結果をそのまま再利用する テストを1つ足しても、変わるのは COPY . . から下 → 重い npm ci は飛ばせる 「毎回ビルド」だが「毎回ゼロから」ではない
  • WORKDIR /app
  • キャッシュ
  • COPY package*.json ./
  • キャッシュ
  • RUN npm ci
  • キャッシュ(数分を節約)
  • CI は使い捨てマシンで手元にキャッシュが無い → 前回のイメージをレジストリから pull して キャッシュに使うことが多い
  • COPY . .
  • 作り直し
  • CMD ["node", "server.js"]
  • 作り直し
  • ソース(テストを追加)が変わった → COPY . . とそれより下の層が無効に
  • テストを足しても作り直すのはソースの層から下だけ。依存のインストールはキャッシュで一瞬

環境とソースを分ける:焼き込むか、マウントするか

テストファイルもソースの一部なので、イメージに焼き込む方式なら、テストを足すたびに作り直しは必要です。ただ、そもそも焼き込まない方式もあります。

本番向けのイメージは、ソースを COPY で中に入れて、イメージ単体で完結させます。どこに持って行っても動くのが利点です。一方、開発や CI 向けには「ランタイムと依存だけが入ったイメージ」を用意し、ソースはホストのフォルダをそのままコンテナに見せます。これがボリュームマウントで、-v $(pwd):/app のように指定します。コンテナの中の /app を見ると、ホストで編集中のファイルがそのまま見えます。

この方式なら、ソースを書き換えてもビルドし直しはゼロで、テストを足したら即座に走らせられます。コツは「環境」と「ソース」を分けて考えること。環境は変わる頻度が低いのでイメージにして配り、ソースは変わる頻度が高いのでマウントするか、最後の薄い層としてコピーする。docker compose でも volumes: に同じことを書けます。

  • 環境とソースを分ける:焼き込む(COPY)か、マウント(-v)か
  • 本番向け:ソースを COPY で焼き込む
  • 開発・CI 向け:ソースはマウントする
  • コンテナ(イメージだけで完結) ランタイム + 依存ライブラリ ソースコード(COPY . . で中に入っている)
  • コンテナ ランタイム + 依存ライブラリ(イメージの中身) /app ← ホストのフォルダをそのまま見せる
  • どこに持って行っても動く。 ただしソースを変えたら build し直す
  • -v $(pwd):/app
  • ホストの ./ (ソース・テスト。編集中のもの)
  • ソースを書き換えても build し直し不要。 テストを足したらすぐ走らせられる
  • docker run --rm -v $(pwd):/app -w /app myapp-dev npm test
  • 環境は変わる頻度が低いのでイメージに。ソースは頻繁に変わるのでマウントか、最後の薄い層に

レジストリ:イメージを保管して配る倉庫

取ってくる側の話に移ります。docker pull postgres:16 と打つと、Docker はどこかからイメージをダウンロードしてきます。その「どこか」がレジストリです。npm のサーバーがパッケージを配り、GitHub がコードを置く場所であるのと同じ位置づけで、イメージを置く専用のサーバーです。

何も指定しないときの既定が Docker Hub で、PostgreSQL や Node、Nginx などの公式イメージが置いてある公開レジストリです。ただレジストリはそれだけではありません。GitHub Container Registry、Google の Artifact Registry、Amazon ECR、自社サーバーに立てる私設レジストリなど、クラウドごと・会社ごとにあります。

会社のプロダクトのイメージは外に公開できないので、クラウド上の非公開レジストリに置くのが普通です。開発者や CI が docker push で上げ、本番サーバーや他の開発者、CI が docker pull で取っていく。レジストリは「ビルド済みの環境を配る倉庫」です。

  • レジストリ:イメージを保管して配る倉庫
  • レジストリ(倉庫)
  • postgres:16
  • ノートPC
  • サーバー
  • docker push
  • node:22-slim
  • myapp:1.0
  • docker pull
  • 開発者 / CI
  • docker pull
  • myapp:1.1(新しい版)
  • 本番 / CI / 同僚の PC
  • 公開:Docker Hub(docker.io。ホスト省略時の既定) 非公開:GitHub Container Registry(ghcr.io)・Google Artifact Registry(*.pkg.dev)・Amazon ECR・自社サーバー
  • npm がパッケージを、GitHub がコードを置くように、レジストリはイメージを置く
  • docker pull の「どこから」がレジストリ。会社のイメージは非公開レジストリに置く

イメージ名の読み方:ホスト / パス / 名前 : タグ

どの倉庫から取ってくるかは、イメージ名そのものに書いてあります。フルで書くと「レジストリのホスト / パス / イメージ名 : タグ」という形です。

図の上の例なら、先頭の asia-northeast1-docker.pkg.dev が Google Artifact Registry の東京リージョン、続く my-project/my-repo がその中のプロジェクトとリポジトリ(倉庫の中の棚)、api がイメージ名、abc123 がタグです。タグは「版」を表し、バージョン番号やコミットハッシュを使います。

postgres:16 のようにホストが無い場合は Docker Hub、パスが無い場合は公式の library/ が補われます。タグを省略すると latest になりますが、何が来るか分からないので実務では必ず書きます。

非公開レジストリから pull するには、事前に認証が要ります。手元なら docker login や gcloud auth configure-docker のようなコマンド、CI ならサービスアカウントの鍵を設定に入れておく、という形で「この Docker クライアントは誰としてアクセスするか」を教えます。

  • イメージ名の読み方:ホスト / パス / 名前 : タグ
  • asia-northeast1-docker.pkg.dev/my-project/my-repo/api:abc123
  • レジストリのホスト (Google Artifact Registry 東京)
  • パス:プロジェクト / リポジトリ (倉庫の中の棚)
  • イメージ名 (api)
  • タグ(版) バージョン番号やコミット ID
  • postgres:16
  • ホスト省略 = docker.io(Docker Hub)、パス省略 = library/(公式) タグ省略 = latest。何が来るか分からないので実務では必ず書く
  • ghcr.io/owner/myapp:1.2.0
  • GitHub Container Registry の owner/myapp、タグ 1.2.0
  • 非公開レジストリは pull の前に認証が要る(docker login / gcloud auth configure-docker)。 CI ではサービスアカウントの鍵を設定に入れておく
  • 名前の先頭を見れば、どの倉庫のどの棚から取ってくるかが分かる

なぜ会社のイメージは外に出せないのか

イメージの中身をもう一度見ると、答えは明らかです。土台の OS、依存ライブラリ、ビルド済みのバイナリや JavaScript、そして COPY . . したあと消していなければソースコードそのもの、場合によっては設定ファイルや焼き込まれてしまった鍵まで入っています。

イメージは docker save で tar ファイルに吐き出せて、中のファイルは全部取り出せます。つまりイメージを公開することは、プロダクトを配布することと同じです。秘密なのはビルドの工程ではなく、その結果物。これは Docker 特有の話ではなく、ビルド済みの成果物を公開しないのと同じ判断です。

だから会社のイメージは非公開レジストリに置き、認証した人と CI だけが pull できるようにします。

  • なぜ会社のイメージは外に出せないのか
  • 会社のイメージ api:abc123 の中身
  • 起動コマンド
  • イメージ = プロダクトそのもの
  • 設定ファイル(.env が焼き込まれていることも)
  • docker save で tar にすれば、中身は全部取り出せる → 公開 = プロダクトの配布と同じ
  • ビルド済みのバイナリ / JavaScript
  • 錠
  • 秘密なのは「ビルド工程」ではなく「結果物」。 ビルド済みの成果物を公開しないのと同じ判断
  • ソースコード(COPY . . のまま残っていれば)
  • 依存ライブラリ
  • だから非公開レジストリ(Artifact Registry など)に置き、 認証した人と CI だけが pull できるようにする
  • OS の土台
  • イメージの中には本体・設定・場合によってはソースも入っている。だから非公開の倉庫へ

CI とローカルがレジストリを経由する理由

ここまでをつなげると、「ローカルで CI を回すのに、なぜクラウドのレジストリを経由するのか」が見えてきます。

流れはこうです。コードを push すると CI が docker build し、できたイメージを docker push で倉庫に上げる。その先で、CI のテストジョブがそれを pull して同じ環境でテストを走らせ、本番サーバーが pull してデプロイし、開発者も pull して手元で同じ環境を再現する。全員が同じイメージを使います。

手元がクラウドを経由する理由は3つ。1つ目、CI と同じイメージを使えば、ランタイムのバージョンも OS のライブラリも CI と完全に一致し、「手元では通るのに CI で落ちる」を潰せる。2つ目、依存のインストールだけで数分かかるプロダクトはざらで、誰かが一度ビルドして上げておけば、あとは pull するだけで済む。3つ目、社内ライブラリや他チームのサービスのイメージは、そこにしか無い。

「ローカルなのにクラウド」は、計算をクラウドでやっているのではなく、ビルド済みの環境ファイルを倉庫からダウンロードしているだけです。npm install がパッケージを落としてくるのと同じで、落としたあとの実行は全部手元で起きています。次のアニメモでは、こうしてそろえた複数のコンテナを docker compose でまとめます。

  • CI とローカルがレジストリを経由する理由
  • git push / PR
  • CI:docker build (前回のイメージをキャッシュに)
  • docker push → レジストリへ
  • docker pull
  • CI のテストジョブ 同じ環境でテストを走らせる
  • 本番サーバー そのままデプロイ
  • あなたの PC 手元で CI と同じ環境を再現
  • 手元がクラウドを経由するのは、計算をクラウドでやるためではなく、 ビルド済みの環境を倉庫からダウンロードしているだけ(npm install と同じ)
  • ① CI と同じ環境を手元で再現できる ② 自分でビルドすると遅い(依存のインストールで数分〜) ③ 社内のイメージはそこにしか無い
  • レジストリ = ビルド済みの環境を配る倉庫。CI も本番も手元も、同じイメージを pull する

0 件のコメント

読み込み中…

権利侵害・不適切な内容の連絡: [email protected]