Docker イメージとレイヤー: 0.1秒でビルドされる魔法の仕組み

「え? 今のでビルド終わったんですか?」

Docker の便利さを一度味わってしまったあと、私はチーム長に内緒で、自分の開発環境を少しずつ Docker に移し始めた。Dockerfile という設計図を書き、docker build コマンドを打った。

最初はそれなりに時間がかかった。Ubuntu を落として、Java を入れて、自分のプロジェクトのライブラリ、Jar、をダウンロードするのに 5 分ほどかかった。「まあ最初だから時間がかかるんだろう」

ところがコードのタイプミスを一つ直して、もう一度ビルドを実行したとき、私は自分の目を疑った。Enter を押した瞬間にターミナルへ Successfully built と表示されたのだ。かかった時間はたったの 0.1 秒。

「まさか壊れてないよな? さっきは 5 分かかったのに、どうしてこんな一瞬で終わるんだ?」

不安になってサーバーを立ち上げてみたが、コードはきちんと修正されていた。では Docker の中では一体何が起きていたのだろう? この異常な速さの秘密は「レイヤー(Layer)」という独特な保存方式にあった。

Docker は最初から全部作り直さない。変わったブロックだけを差し替える。

イメージ(Image)とコンテナ(Container)

本格的な仕組みに入る前に、混同しやすい二つの用語だけ整理しておこう。

  • イメージ(Image): 実行可能な「環境」をカチコチに凍らせた設計図であり、原本のハンコだ。(読み取り専用)
  • コンテナ(Container): そのハンコをポンと押して作り出した実体だ。実際に動いているプログラムそのものだ。(書き込み可能)

私たちがデプロイ時にサーバーへ送るのは「コンテナ」ではなく「イメージ」だ。サーバー側はそのイメージを受け取って実行するだけでいい。

レイヤー(Layer): 透明なセロファンの魔法

Docker イメージは一つの巨大なファイルではない。まるで「透明なセロファン」を何枚も重ねたようなものだ。Dockerfile に書かれた命令は、一行ごとにそれぞれ一つのレイヤーになる。

例えばこうだ。

# 1層目: Ubuntu (OS) をインストール
FROM ubuntu:20.04

# 2層目: Javaをインストール
RUN apt-get install openjdk-17-jdk

# 3層目: 自分のコードをコピー
COPY my-app.jar /app/my-app.jar

# 4層目: 実行
CMD ["java", "-jar", "/app/my-app.jar"]

Dockerfile に書かれた内容をじっくり見ると、構造はそれほど難しくないことが分かる。「Ubuntu を入れて、FROM、Java をインストールして、RUN、自分のコードを持ってきて、COPY、実行しろ、CMD。」という流れだ。

結局のところ、私たちが VM やサーバーのターミナルに入って手で一つずつ打っていた「サーバーのセットアップ手順」を、そのまま文章として書き写しただけなのだ。この一枚の文書さえあれば、誰かが横で指示しなくても、Docker が自動的に必要なインストールを進めてくれる。この考え方は、あとで扱う「CI/CD 自動化」の核心にもつながっている。

では、なぜ Docker はこの手順を一気にまとめず、わざわざ一行ずつ「レイヤー」に分けて積み上げているのだろう?

答えは 再利用、Caching のためだ。もし私がコードを修正してもう一度ビルドすると、Docker は賢く考える。「1 層目、OS と 2 層目、Java は前回と同じだな。なら新しく作り直す必要はない。前に作ったキャッシュをそのまま使おう」

そして変更が入った 3 層目、コードのコピー、から先だけを新しく作り直す。だから二回目のビルドは 0.1 秒で終わったのだ。

変わらない部分は凍らせてキャッシュし、変わった部分だけを新しく焼き直す。

[Code Verification] レイヤーキャッシュを自分の目で確認する

本当にそうなのか、コードで確認してみよう。簡単な Dockerfile を作って二回ビルドすればいい。

  1. Dockerfile を作成:
FROM alpine:latest
RUN echo "1. 基本ユーティリティをインストール中..." && sleep 2 # 2秒かかる処理
COPY test.txt /app/test.txt
CMD ["cat", "/app/test.txt"]

  1. 一回目のビルド:
$ docker build -t my-test:v1 .
# 結果:
# [2/3] RUN echo "1. 基本ユーティリティをインストール中..." ... 2.1s (2秒かかる)

  1. test.txt の内容を修正してから二回目のビルド:
$ docker build -t my-test:v2 .
# 結果:
# [2/3] RUN echo "1. 基本ユーティリティをインストール中..." ... CACHED (0秒!)

ログにくっきり表示された CACHED という単語が見えるだろうか。Docker が 2 秒かかる処理をスキップした証拠だ。

実務アドバイス: 順番が命

このレイヤーキャッシュの原理を理解すれば、Dockerfile をどう書くべきか答えが見えてくる。核心は単純で、「あまり変わらないものを下、つまり先に置き、よく変わるものを上、つまり後に置く」ことだ。

悪い例:

# 1. ソースコードを先にコピー(頻繁に変わる!)
COPY . . 
# 2. ライブラリをインストール(ほとんど変わらない)
RUN npm install

ソースコードは一日に何十回も変わる。もし 1 番が変われば、Docker はその後ろにある 2 番、ライブラリインストール、のキャッシュまで無効にして再実行してしまう。コードを一行直しただけなのに、また npm install を待つことになる。

良い例:

# 1. package.json だけを先にコピー
COPY package.json .
# 2. ライブラリをインストール(ソースが変わってもキャッシュされる)
RUN npm install
# 3. ソースコードをコピー
COPY . .

順番を変えるだけで、ビルド速度は 10 倍になることもある。これが Docker 初心者と、Docker を使いこなす人との差だ。

実務アドバイス 2: タグ(Tag)は命綱

上のコードでは docker build -t my-test:v1 . と書いたが、ここでコロン : の後ろについている v1 がタグ、Tag、だ。

多くの初心者は面倒だからとタグを付けないが、そうすると Docker は自動的に latest というタグを付ける。

  • my-test:latest: いちばん新しいバージョンという意味だ。
  • 問題点: 毎回 latest で上書きしてばかりいると、サーバーでエラーが出たときに「あれ? 昨日ちゃんと動いてたあのバージョンに戻して!」ができない。もう上書きされて消えてしまっているからだ。
  • 解決策: 必ず v1、v2、あるいは 日付-時刻 のような固有のタグを付けてビルドしなければならない。そうしておけば、事故が起きたときに v2 を捨てて v1 へ1 秒でロールバックできる。

このタグを付ける習慣一つが、将来あなたの退勤時間を守ってくれる。

おわりに: 0.1 秒の魔法を手に入れる

Docker イメージは単なるファイルの塊ではなく、環境をもっとも効率よく届けるために幾重にも積み上げられた 技術の結晶 だ。レイヤーキャッシュを理解し、Dockerfile の順番さえきちんと整えれば、0.1 秒でビルドが終わる軽くて速いデプロイ環境を作ることができる。

これで、自分のコンピュータ環境を完璧に凍らせて、イメージとしてサーバーへ送る準備は整った。

ところが実際にこのイメージをサーバーで立ち上げようとすると、新しい悩みが出てくる。Web サービスはサーバー用コンテナ一つだけでは動かない。データベースも必要だし、Redis も必要だし、フロントエンドサーバーも必要だ。

これだけ多くのコンテナを、一つひとつ別々のコマンドで起動・停止して管理できるのだろうか。互いの通信はどうするのか。単体の「イメージ」を超えて、巨大な「アプリケーション」をどう組み立てるのか。次回は、Docker の世界観を広げる コンテナ、サービス、そしてスタック という概念について見ていこう。

コメントする