Docker 入門

カスタムDockerイメージ

専用のDockerイメージをハンドメイドで構築するスキルを習得することは、Dockerを扱うすべての技術者にとっての必須科目です。これにより、アプリケーションに必要な環境と依存関係を正確に定義できるだけでなく、開発PC、テストサーバー、そして最終的なプロダクション環境のいずれにおいても、アプリケーションの絶対的な一貫性とポータビリティを確保できます。

効率的なDockerfileの記述方法、イメージの「軽量化」手法、およびビルドレイヤーの管理方法を理解することは、効率的なコンテナ化を実現するための鍵となります。

1. Dockerfileの基礎を深掘り

Dockerfileは純粋なテキストの「レシピ」であり、コマンドラインでイメージを手動で組み立てる際に実行するすべてのコマンドが記録されています。イメージのビルドコマンドを発行すると、Dockerはこの「レシピ」に従い、順序通りに1行ずつ実行していきます。ファイルシステムを変更する命令が実行されるたびに、イメージには新しい「レイヤー (Layer)」が追加されます。Dockerfileの構文と構造を深く理解することは、効率的で再現可能なイメージビルドの基盤を築くことになります。

1.1 Dockerfileのコア構文の復習

Dockerfileは1行ずつの命令で構成されています。命令は大文字・小文字を区別しませんが、後続のパラメーターと一目で区別できるように、命令をすべて大文字にするのが業界の慣習です。# 記号を使用してコメントを追加できます。

最も頻繁に使用される「常連」の命令を復習しましょう:

  • FROM: この「ビル」をどの「基礎」(ベースイメージ)の上に建てるかを指定します。これは通常、Docker Hub上の別のイメージです。これは、コメントを除いてDockerfileの最初の有効な命令でなければなりません。
  • RUN: イメージのビルドプロセス中に、コンテナ内部でコマンドを実行します。通常、ソフトウェアのインストールや設定の変更などに使用されます。
  • CMD: コンテナ起動後にデフォルトで実行する内容を規定します。1つのDockerfileには1つの CMD しか記述できません。複数記述された場合、最後のものだけが有効になります。docker run 時にこのデフォルトコマンドは簡単にオーバーライドできます。
  • ENTRYPOINT: コンテナを通常の実行可能プログラムのように実行させます。CMD と似ていますがオーバーライドが難しく、通常はコンテナのメインプロセスを定義するために使用されます。
  • COPY: ローカルPC上のファイルやフォルダをそのままイメージ内にコピーします。
  • ADD: COPY の拡張版とも言え、圧縮ファイルの自動解凍や、ネットワークリンクからのファイル直接ダウンロードが可能です。
  • WORKDIR: ワークディレクトリを設定し、これ以降の命令はこのディレクトリ下で実行されます。
  • EXPOSE: コンテナ実行時にリッスンする予定のポートを宣言します(主にドキュメント目的であり、実際に外部ネットワークへポートを公開するわけではありません)。
  • ENV: コンテナ内に環境変数を設定します。
  • ARG: 変数を定義し、docker build コマンド実行時に --build-arg パラメーターを通じて値を渡せるようにします。
  • VOLUME: 永続化が必要なデータを保存するためのボリュームマウントポイントを作成します。
  • USER: これ以降のコマンドを実行する際に使用するユーザー(UIDまたはユーザー名)を指定します。
  • LABEL: キー・バリュー形式でイメージに様々なタグ(バージョン番号や作者情報など)を貼り付けます。
  • STOPSIGNAL: コンテナをグレースフルに終了させるためのシステムシグナルを設定します。
  • HEALTHCHECK: コンテナが「病気」になっていないか(正常に動作しているか)をDockerに確認させる方法を指示します。
  • SHELL: RUN 命令でデフォルトで使用されるシェル環境を変更できるようにします。

1.2 Dockerfileの黄金の構造法則

レイアウトが良く、構造が明確なDockerfileは、見た目が美しいだけでなく、将来のメンテナンスも容易になります。以下の構造を採用することを推奨します:

  1. ベースイメージ (FROM): 可能な限りコンパクトで適切な開始点を選択します。例えば、Alpine Linuxを使用するとボリュームを大幅に削減できます。
  2. メタデータ (LABEL): メンテナーは誰か、バージョンはいくつか、何の用途かを追記します。
  3. 環境変数 (ENV): アプリケーションに必要な環境変数を事前に設定します。
  4. 依存関係のインストール (RUN): アプリケーションの実行に必要なソフトウェアをインストールします。関連する RUN コマンドは && で繋ぐことで、イメージのレイヤー数を減らすよう努めます。
  5. コードのコピー (COPY/ADD): アプリケーションのコードをイメージ内に配置します。
  6. ワークディレクトリ (WORKDIR): コードを実行するディレクトリを設定します。
  7. ポートの公開 (EXPOSE): アプリケーションがリッスンするポートを宣言します。
  8. 起動コマンド (CMD/ENTRYPOINT): コンテナ起動時に具体的にどのプログラムを実行するかを指示します。

1.3 命令の適用例のデモンストレーション

これらの命令が実際にどのように記述されるか見てみましょう:

FROM

# 公式の Python 3.9 (スリム版) をベースにする
FROM python:3.9-slim-buster

RUN

# 依存パッケージのインストール。--no-cache-dir を追加して不要なキャッシュの保存を防ぎ、イメージをスリムにする
RUN pip install --no-cache-dir -r requirements.txt

COPY と WORKDIR

# /app ディレクトリに切り替える
WORKDIR /app

# カレントディレクトリのすべてのコンテンツをイメージ内の /app にコピーする
COPY . /app

CMD と ENV

# 環境変数を設定する
ENV APP_HOME /app
ENV DEBUG=True

# コンテナ起動時に app.py を実行する
CMD ["python", "app.py"]

EXPOSE と LABEL

LABEL maintainer="[email protected]"
LABEL version="1.0"
LABEL description="シンプルな Python Web アプリケーション"

# アプリケーションが8000番ポートをリッスンすることを宣言する
EXPOSE 8000

1.4 完全な例:Pythonアプリケーションイメージの組み立て

上記の断片を繋ぎ合わせると、Pythonアプリケーションを直接実行できる完全なDockerfileになります:

# 1. 基礎を選ぶ
FROM python:3.9-slim-buster

# 2. ワークディレクトリを決定する
WORKDIR /app

# 3. まず依存関係の設定ファイルをコピーする(キャッシュメカニズムの利用)
COPY requirements.txt .

# 4. 依存関係をインストールする
RUN pip install --no-cache-dir -r requirements.txt

# 5. 残りのすべてのコードをコピーする
COPY . .

# 6. ポートを宣言する
EXPOSE 8000

# 7. 環境変数を設定してみる
ENV NAME Docker

# 8. 起動コマンドを設定する
CMD ["python", "app.py"]

2. コードをイメージへ:ビルドの実行

Dockerfileを書き終えたら、それをどのように本物のイメージにするのでしょうか?ここで docker build コマンドの出番です。

Dockerfileが含まれるディレクトリでターミナルを開き、以下を入力します:

docker build -t my-python-app .
  • -t my-python-app: この出来立てのイメージに名前(タグ Tag)を付け、後で見つけやすくします。
  • .: このドットは非常に重要です!これはビルドコンテキスト (Build Context) を表し、Dockerに対して「おい、Dockerfileとパッケージ化したいファイルはこのカレントディレクトリにあるから、ここを探してくれ」と指示します。

ビルドが完了すると、それを実行できるようになります:

docker run -p 8000:8000 my-python-app

このコマンドは、ホストの8000番ポートとコンテナの8000番ポートを接続し、先ほど作成したイメージを起動します。

3. 高度なテクニック:エキスパートレベルのDockerfileの作成

基礎をマスターしたところで、イメージをさらに小さく、ビルドをさらに速くするための高度なテクニックを見ていきましょう。

3.1 究極のスリム化秘伝:マルチステージビルド (Multi-Stage Builds)

マルチステージビルドを使用すると、1つのDockerfile内で複数の FROM ステートメントを使用できます。各 FROM は完全に新しい「ステージ」を表します。最も素晴らしいのは、前のステージで生成された良いもの(コンパイルされたプログラムなど)を後のステージで直接「盗んで」使用し、コンパイルやインストールのための重いツールはすべて破棄できることです!

これは、大きなキッチン(コンパイル環境)で豪華な食事を作り、完成した後、美しく盛り付けられた料理だけを清潔でコンパクトなダイニング(実行環境)に運び、キッチンの鍋やフライパンは一切持っていかないようなものです。

Go言語プログラムの例を見てみましょう:

# --- ステージ1:このステージを "builder" と呼ぶ (コンパイル担当) ---
FROM golang:1.17-alpine AS builder
WORKDIR /app
# コードをコピーし、依存関係をダウンロードする
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# myapp という名前の実行可能ファイルをコンパイルする
RUN go build -o myapp

# --- ステージ2:これが最終的に実行されるイメージ (極小環境のみ必要) ---
FROM alpine:latest
WORKDIR /app
# 【コア操作】前のステージ (builder) でコンパイルされた myapp をコピーしてくる!
COPY --from=builder /app/myapp .
EXPOSE 8080
# コンパイルされたプログラムを実行する
CMD ["./myapp"]

最終的に生成されるイメージには、ベースとなるAlpineシステムと、その単独の myapp プログラムのみが含まれており、サイズは驚くほど小さくなります!

3.2 究極の高速化:Dockerキャッシュの価値を搾り取る

前の章で、Dockerはビルド時にレイヤーごとにキャッシュすることについて言及しました。あるレイヤーに変更がなければ、それをそのまま再利用するため、大幅な時間短縮になります。

キャッシュヒット率を高めたい場合は、以下の3つの原則を覚えておいてください:

  1. 頻繁に変更されるものを後ろに置く: 例えばアプリケーションコード(COPY . .)は頻繁に変更されるため、必ずDockerfileの最後に配置します。システムソフトウェアのインストール(RUN apt-get update)のように数ヶ月に一度も変更しないようなものは、一番前に置きます。
  2. 命令は専一かつ安定させる: RUN コマンド内のスペース1つを変更しただけでも、そのレイヤーとそれ以降のすべてのキャッシュが無効になります。
  3. 不要なものをコピーしない: イメージにコピーされたファイルに変更があった場合も、キャッシュは無効になります。

3.3 クリーンに保つ:.dockerignoreの活用

イメージをビルドする際、Dockerは指定したディレクトリ(つまりあの . )の下にあるすべてのものをDockerエンジンに送信します。もしディレクトリに巨大なログファイルや数十メガバイトの .git フォルダがある場合、ビルド速度が極端に低下し、イメージが肥大化してしまいます。

ここで .dockerignore ファイルの出番です。その使用法は .gitignore と全く同じで、Dockerに対して「これらのファイルは気にしないで、パッケージ化しないでくれ」と伝えるためのものです。

Dockerfileと同じディレクトリレベルに .dockerignore ファイルを作成し、以下のように記述します:

node_modules
.git
.DS_Store
*.log

これにより、これら4種類のファイルまたはフォルダはDockerによって完全に無視されます。