高度な Dockerfile テクニック

DockerBeginner
オンラインで実践に進む

はじめに

この実験では、Dockerfile のテクニックをさらに深く掘り下げ、より効率的で柔軟な Docker イメージを作成するための高度な概念を探索します。詳細な Dockerfile 命令、マルチステージビルド(multi-stage builds)、および .dockerignore ファイルの使用方法について説明します。また、Docker イメージにおけるレイヤー(layers)という極めて重要な概念についても学びます。この実験を終える頃には、これらの高度な Dockerfile テクニックを包括的に理解し、自身のプロジェクトに応用できるようになります。

この実験は初心者向けに設計されており、詳細な説明を提供し、混乱しやすいポイントを解消します。すべてのファイル編集作業には WebIDE(VS Code)を使用するため、ブラウザ上で直接ファイルを簡単に作成・修正できます。

Dockerfile の命令とレイヤーの理解

まずは、さまざまな命令を活用した Dockerfile を作成することから始めましょう。Flask を使用した Python Web アプリケーションのイメージを構築し、その過程で各命令が Docker イメージのレイヤーにどのように寄与するかを詳しく見ていきます。

  1. 最初に、プロジェクト用の新しいディレクトリを作成します。WebIDE のターミナルで以下を実行してください:
mkdir -p ~/project/advanced-dockerfile && cd ~/project/advanced-dockerfile

このコマンドは、project フォルダ内に advanced-dockerfile という新しいディレクトリを作成し、そのディレクトリに移動します。

  1. 次に、アプリケーションファイルを作成します。WebIDE のファイルエクスプローラー(通常は画面の左側)で、advanced-dockerfile フォルダを右クリックし、「New File」を選択します。ファイル名を app.py とします。

  2. app.py を開き、以下の Python コードを追加します:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from {os.environ.get('ENVIRONMENT', 'unknown')} environment!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

これは、実行されている環境名を含む挨拶メッセージを返すシンプルな Flask アプリケーションです。

  1. 次に、Python の依存関係を指定するための requirements.txt ファイルを作成する必要があります。同じディレクトリに requirements.txt という名前の新しいファイルを作成し、以下の内容を追加します:
Flask==2.0.1
Werkzeug==2.0.1

ここでは、互換性を確保するために Flask と Werkzeug の両方に正確なバージョンを指定しています。

  1. それでは、Dockerfile を作成しましょう。同じディレクトリに Dockerfile(先頭の 'D' は大文字)という名前の新しいファイルを作成し、以下の内容を追加します:
## ベースイメージとして公式の Python ランタイムを使用
FROM python:3.9-slim

## コンテナ内の作業ディレクトリを設定
WORKDIR /app

## 環境変数を設定
ENV ENVIRONMENT=production

## requirements ファイルをコンテナにコピー
COPY requirements.txt .

## 必要なパッケージをインストール
RUN pip install --no-cache-dir -r requirements.txt

## アプリケーションのコードをコンテナにコピー
COPY app.py .

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

## アプリが動作するポートを公開
EXPOSE 5000

## メタデータ用のラベルを追加
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo for advanced Dockerfile techniques"

ここで、これらの命令を分解して、Docker イメージのレイヤーにどのように影響するかを理解しましょう:

  • FROM python:3.9-slim: これは常に最初の命令です。構築のベースとなるイメージを指定します。これにより、Python ランタイムを含むイメージの最初のレイヤーが作成されます。
  • WORKDIR /app: 以降の命令のための作業ディレクトリを設定します。新しいレイヤーは作成しませんが、後続の命令の動作に影響を与えます。
  • ENV ENVIRONMENT=production: 環境変数を設定します。環境変数は新しいレイヤーを作成しませんが、イメージのメタデータに保存されます。
  • COPY requirements.txt .: ホストからイメージに requirements ファイルをコピーします。これにより、このファイルのみを含む新しいレイヤーが作成されます。
  • RUN pip install --no-cache-dir -r requirements.txt: ビルドプロセス中にコンテナ内でコマンドを実行します。ここでは Python の依存関係をインストールしています。これにより、インストールされたすべてのパッケージを含む新しいレイヤーが作成されます。
  • COPY app.py .: アプリケーションのコードをイメージにコピーし、別のレイヤーを作成します。
  • CMD ["python", "app.py"]: コンテナ起動時に実行するコマンドを指定します。レイヤーは作成しませんが、コンテナのデフォルトコマンドを設定します。
  • EXPOSE 5000: これは実際にはドキュメントの一種です。コンテナが実行時にこのポートでリッスンすることを Docker に伝えますが、実際にポートを公開(パブリッシュ)するわけではありません。レイヤーは作成しません。
  • LABEL ...: イメージにメタデータを追加します。ENV 命令と同様に、新しいレイヤーは作成せず、イメージのメタデータに保存されます。

Dockerfile 内の各 RUNCOPYADD 命令は、新しいレイヤーを作成します。レイヤーは Docker の基本的な概念であり、イメージの効率的な保存と転送を可能にします。Dockerfile を変更してイメージを再構築すると、Docker は変更されていないキャッシュされたレイヤーを再利用し、ビルドプロセスを高速化します。

  1. Dockerfile の内容を理解したところで、Docker イメージをビルドしましょう。ターミナルで以下を実行します:
docker build -t advanced-flask-app .

このコマンドは、advanced-flask-app というタグを付けて新しい Docker イメージをビルドします。末尾の . は、現在のディレクトリにある Dockerfile を探すよう Docker に指示しています。

ビルドプロセスの各ステップを示す出力が表示されます。各ステップが Dockerfile の命令に対応していること、およびビルドコマンドを複数回実行した場合に変更のないステップで Docker が「Using cache」と表示することを確認してください。

  1. ビルドが完了したら、新しいイメージに基づいてコンテナを実行できます:
docker run -d -p 5000:5000 --name flask-container advanced-flask-app

このコマンドは以下のことを行います:

  • -d: コンテナをデタッチドモード(バックグラウンド)で実行します。
  • -p 5000:5000: ホストのポート 5000 をコンテナのポート 5000 にマッピングします。
  • --name flask-container: 新しいコンテナに名前を付けます。
  • advanced-flask-app: コンテナ作成に使用するイメージ名です。

実行中のコンテナリストを確認することで、コンテナが動作しているか検証できます:

docker ps
  1. アプリケーションが正しく動作しているかテストするために、curl コマンドを使用します:
curl http://localhost:5000

「Hello from production environment!」というメッセージが表示されるはずです。

curl で問題が発生した場合は、新しいブラウザタブを開いて http://localhost:5000 にアクセスすることもできます。同じメッセージが表示されるはずです。

問題が発生した場合は、以下のコマンドでコンテナのログを確認できます:

docker logs flask-container

これにより、Flask アプリケーションからのエラーメッセージや出力が表示されます。

マルチステージビルド

基本的な Dockerfile の命令とレイヤーについて理解したところで、より高度なテクニックであるマルチステージビルド(multi-stage builds)を探索しましょう。マルチステージビルドを使用すると、Dockerfile 内で複数の FROM ステートメントを使用できます。これは、あるステージから別のステージへ必要な成果物のみをコピーすることで、最終的なイメージサイズを小さくするのに非常に役立ちます。

実際にイメージサイズを小さくするマルチステージビルドを使用するように Dockerfile を修正してみましょう:

  1. WebIDE で、先ほど作成した Dockerfile を開きます。
  2. 内容をすべて以下のものに置き換えます:
## ビルドステージ
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN pip install --user --no-cache-dir -r requirements.txt

## 最終ステージ
FROM python:3.9-slim

WORKDIR /app

## ビルダー(builder)ステージからインストール済みパッケージのみをコピー
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH
ENV ENVIRONMENT=production

CMD ["python", "app.py"]

EXPOSE 5000

LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo with multi-stage build"

このマルチステージ Dockerfile で何が起きているのか分解してみましょう:

  1. builder ステージから開始します:

    • 最初からサイズを抑えるために、ベースとして python:3.9-slim イメージを使用します。
    • pip install --user を使用して、このステージで Python の依存関係をインストールします。これにより、パッケージはユーザーのホームディレクトリにインストールされます。
  2. 次に最終ステージがあります:

    • 別の python:3.9-slim イメージで新しく開始します。
    • builder ステージから、pip install --user によって配置された /root/.local 内のインストール済みパッケージのみをコピーします。
    • アプリケーションのコードをコピーします。
    • Python がインストール済みパッケージを見つけられるように、ローカルの bin ディレクトリを PATH に追加します。
    • 以前と同様に、コンテナの残りの設定(ENV, CMD, EXPOSE, LABEL)を行います。

ここでの主な利点は、最終的なイメージに pip のインストールプロセスで使用されたビルドツールやキャッシュが含まれないことです。最終的に必要な成果物のみが含まれます。これにより、イメージサイズが小さくなるはずです。

  1. この新しいマルチステージイメージをビルドしましょう。ターミナルで以下を実行します:
docker build -t multi-stage-flask-app .
  1. ビルドが完了したら、2 つのイメージのサイズを比較してみましょう。以下を実行します:
docker images | grep flask-app
multi-stage-flask-app         latest     7bdd1be2d1fb   10 seconds ago   129MB
advanced-flask-app            latest     c59d6fa303cc   10 minutes ago   136MB

multi-stage-flask-app が、以前にビルドした advanced-flask-app よりも小さくなっていることが確認できるはずです。

  1. それでは、新しいスリムなイメージでコンテナを実行しましょう:
docker run -d -p 5001:5000 --name multi-stage-container multi-stage-flask-app

前のコンテナとの衝突を避けるために、別のホストポート(5001)を使用していることに注意してください。

  1. アプリケーションをテストします:
curl http://localhost:5001

引き続き「Hello from production environment!」というメッセージが表示されるはずです。

  1. シングルステージイメージとマルチステージイメージの違いをさらに理解するために、docker history コマンドを使用できます。以下のコマンドを実行してください:
docker history advanced-flask-app
docker history multi-stage-flask-app

出力を比較してください。マルチステージビルドの方がレイヤー数が少なく、一部のレイヤーのサイズが小さくなっていることに気づくでしょう。

マルチステージビルドは、効率的な Docker イメージを作成するための強力なテクニックです。最終的なイメージを肥大化させることなく、ビルドプロセスでツールやファイルを使用できます。これは、コンパイルが必要な言語や、複雑なビルドプロセスを持つアプリケーションで特に有用です。

今回のケースでは、ビルド時の成果物やキャッシュを残さず、必要なインストール済みパッケージとアプリケーションコードのみをコピーすることで、より小さな Python アプリケーションイメージを作成しました。

.dockerignore ファイルの活用

Docker イメージをビルドする際、Docker はディレクトリ内のすべてのファイルを Docker デーモンに送信します。イメージ構築に不要な大きなファイルがある場合、ビルドプロセスが遅くなる可能性があります。.dockerignore ファイルを使用すると、Docker イメージをビルドする際に除外すべきファイルやディレクトリを指定できます。

.dockerignore ファイルを作成して、その仕組みを確認しましょう:

  1. WebIDE で、advanced-dockerfile ディレクトリに .dockerignore という名前の新しいファイルを作成します。
  2. .dockerignore ファイルに以下の内容を追加します:
**/.git
**/.gitignore
**/__pycache__
**/*.pyc
**/*.pyo
**/*.pyd
**/.Python
**/env
**/venv
**/ENV
**/env.bak
**/venv.bak

これらのパターンの意味を分解してみましょう:

  • **/.git: ディレクトリ構造のどこにあっても、.git ディレクトリとそのすべての内容を無視します。
  • **/.gitignore: .gitignore ファイルを無視します。
  • **/__pycache__: Python のキャッシュディレクトリを無視します。
  • **/*.pyc, **/*.pyo, **/*.pyd: コンパイルされた Python ファイルを無視します。
  • **/.Python: .Python ファイル(仮想環境でよく作成される)を無視します。
  • **/env, **/venv, **/ENV: 仮想環境のディレクトリを無視します。
  • **/env.bak, **/venv.bak: 仮想環境ディレクトリのバックアップコピーを無視します。

各行の先頭にある ** は「任意のディレクトリ内」を意味します。

  1. .dockerignore ファイルの効果を実証するために、無視したいファイルをいくつか作成してみましょう。ターミナルで以下を実行します:
mkdir venv
touch venv/ignore_me.txt
touch .gitignore

これらのコマンドは、ファイルを含む venv ディレクトリと .gitignore ファイルを作成します。これらは Python プロジェクトで一般的ですが、通常 Docker イメージには含めたくない要素です。

  1. それでは、イメージを再度ビルドしましょう:
docker build -t ignored-flask-app .
  1. 無視されたファイルがビルドコンテキストに含まれていないことを確認するために、docker history コマンドを使用できます:
docker history ignored-flask-app

venv ディレクトリや .gitignore ファイルをコピーするステップが表示されないはずです。

.dockerignore ファイルは、Docker イメージをクリーンに保ち、ビルドプロセスを効率化するための強力なツールです。最終的なイメージに不要なファイルが多数存在する可能性がある大規模なプロジェクトでは特に役立ちます。

高度な Dockerfile 命令

この最後のステップでは、Docker イメージをより安全でメンテナンスしやすく、使いやすくするための追加の Dockerfile 命令とベストプラクティスを探索します。また、プロセスの各ステップのトラブルシューティングと検証にも焦点を当てます。

  1. WebIDE で、再度 Dockerfile を開きます。

  2. 内容を以下のものに置き換えます:

## ビルドステージ
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

## 最終ステージ
FROM python:3.9-slim

## 非ルートユーザーを作成
RUN useradd -m appuser

## ヘルスチェック用に curl をインストール
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app

## Python バージョンと site-packages パスを動的に決定
RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && \
    SITE_PACKAGES_PATH="/home/appuser/.local/lib/python${PYTHON_VERSION}/site-packages" && \
    mkdir -p "${SITE_PACKAGES_PATH}" && \
    chown -R appuser:appuser /home/appuser/.local

## 変数を使用して site-packages とバイナリをコピー
COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}"
COPY --from=builder /root/.local/bin /home/appuser/.local/bin
COPY app.py .

ENV PATH=/home/appuser/.local/bin:$PATH
ENV ENVIRONMENT=production

## アプリケーションを実行するユーザーを設定
USER appuser

## ENTRYPOINT を CMD と組み合わせて使用
ENTRYPOINT ["python"]
CMD ["app.py"]

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:5000/ || exit 1

ARG BUILD_VERSION
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="${BUILD_VERSION:-1.0}"
LABEL description="Flask app demo with advanced Dockerfile techniques"

この Dockerfile で導入された新しい概念を分解してみましょう:

  • RUN useradd -m appuser: コンテナ内に appuser という名前の新しいユーザーを作成します。アプリケーションが侵害された場合の潜在的な被害を制限するため、非ルートユーザーとしてアプリケーションを実行することはセキュリティ上のベストプラクティスです。-m フラグはユーザーのホームディレクトリを作成します。
  • RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*: HEALTHCHECK 命令を動作させるために必要な curl パッケージをインストールします。また、イメージサイズを削減するために apt キャッシュをクリーンアップします。
  • RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && ...: この一連のコマンドは、コンテナ内の Python バージョンを動的に特定し、appuser 用の正しい site-packages ディレクトリを作成します。また、ユーザーのローカルディレクトリに対して正しい権限を設定します。
  • COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}": この命令は、インストールされた Python パッケージを builder ステージから最終イメージ内の動的に決定された site-packages パスにコピーし、appuser が使用できる正しい場所にパッケージが配置されるようにします。
  • COPY --from=builder /root/.local/bin /home/appuser/.local/bin: pip によってインストールされた実行可能スクリプト(Flask のコマンドラインインターフェースなど)を、builder ステージから appuser のローカル bin ディレクトリにコピーします。
  • ENTRYPOINT ["python"]CMD ["app.py"]: これらを組み合わせて使用すると、ENTRYPOINT はコンテナのメインの実行ファイル(この場合は python)を定義し、CMD はその実行ファイルへのデフォルト引数(app.py)を提供します。このパターンは柔軟性を高めます。ユーザーはデフォルトで app.py を実行することも、CMD を上書きして他の Python スクリプトやコマンドを実行することもできます。
  • HEALTHCHECK: この命令はコンテナのヘルスチェックを設定します。Docker は指定されたコマンド(curl -f http://localhost:5000/)を定期的に実行して、コンテナが正常(healthy)かどうかを判断します。--interval=30s--timeout=3s フラグは、それぞれチェックの間隔とタイムアウトを設定します。curl コマンドが失敗(ゼロ以外の終了コードを返す)した場合、コンテナは異常(unhealthy)とみなされます。
  • ARG BUILD_VERSION: BUILD_VERSION という名前のビルド引数を定義します。ビルド引数を使用すると、ビルド時に Docker イメージに値を渡すことができます。
  • LABEL version="${BUILD_VERSION:-1.0}": Docker イメージに version という名前のラベルを設定します。これには BUILD_VERSION ビルド引数を使用します。ビルド中に BUILD_VERSION が提供されればその値が使用され、提供されなければデフォルトの 1.0:- デフォルト値構文を使用)が使用されます。
  1. それでは、ビルドバージョンを指定して、この新しいイメージをビルドしましょう:
docker build -t advanced-flask-app-v2 --build-arg BUILD_VERSION=2.0 .

--build-arg BUILD_VERSION=2.0 フラグにより、イメージのビルドプロセス中に BUILD_VERSION 引数に 2.0 という値を渡すことができます。この値は Docker イメージの version ラベルを設定するために使用されます。

  1. ビルドが完了したら、イメージが正常に作成されたことを確認しましょう:
docker images | grep advanced-flask-app-v2

docker images コマンドの出力に、新しいイメージ advanced-flask-app-v2 がタグ、イメージ ID、作成日、サイズとともに表示されるはずです。

  1. 次に、新しいイメージでコンテナを実行します:
docker run -d -p 5002:5000 --name advanced-container-v2 advanced-flask-app-v2

このコマンドは、コンテナをデタッチドモード(-d)で実行し、ホストのポート 5002 をコンテナのポート 5000 にマッピング(-p 5002:5000)し、コンテナに advanced-container-v2 という名前を付け(--name advanced-container-v2)、advanced-flask-app-v2 イメージを使用してコンテナを作成します。

  1. コンテナが動作していることを確認しましょう:
docker ps | grep advanced-container-v2

コンテナが正常に動作していれば、docker ps コマンドの出力に表示されます。コンテナが表示されない場合は、終了してしまった可能性があります。停止したコンテナも含めて確認してみましょう:

docker ps -a | grep advanced-container-v2

docker ps -a の出力にコンテナはあるが実行されていない(ステータスが "Up" ではない)場合は、ログを確認してエラーを調べます:

docker logs advanced-container-v2

このコマンドは advanced-container-v2 コンテナのログを表示し、Flask アプリケーションの起動時の問題や実行時エラーの診断に役立ちます。

  1. コンテナが動作していると仮定して、起動まで少し待ってからヘルスステータスを確認します:
docker inspect --format='{{.State.Health.Status}}' advanced-container-v2

少し待つと(ヘルスチェックが少なくとも 1 回実行されるまで)、出力として "healthy" が表示されるはずです。最初に "unhealthy" と表示された場合は、さらに 30 秒(ヘルスチェックの間隔)待ってから再度コマンドを実行してください。もし "unhealthy" のままの場合は、docker logs advanced-container-v2 を使用して Flask アプリケーションに問題がないか確認してください。明らかな問題がなければ、"unhealthy" ステータスは無視しても構いません。

  1. ビルドバージョンのラベルが正しく適用されたことも確認できます:
docker inspect -f '{{.Config.Labels.version}}' advanced-container-v2

このコマンドは advanced-container-v2 コンテナから version ラベルの値を取得して表示します。出力に "2.0" と表示されれば、BUILD_VERSION ビルド引数がラベルの設定に正しく使用されたことが確認できます。

  1. 最後に、アプリケーションにリクエストを送信してテストしましょう:
curl http://localhost:5002

出力に "Hello from production environment!" というメッセージが表示されるはずです。これは、Flask アプリケーションが Docker コンテナ内で正常に動作しており、ホストのポート 5002 からアクセス可能であることを示しています。

これらの高度なテクニックにより、より安全で設定可能、かつ本番環境に適した Docker イメージを作成できます。非ルートユーザーはセキュリティを向上させ、HEALTHCHECK はコンテナのオーケストレーションと監視を助け、ビルド引数はより柔軟でバージョン管理されたイメージ構築を可能にします。

まとめ

この実験では、より効率的で安全、かつメンテナンスしやすい Docker イメージを作成するための高度な Dockerfile テクニックを探索しました。学んだ内容は以下の通りです:

  1. 詳細な Dockerfile 命令とイメージレイヤーへの影響:各命令が Docker イメージの構造にどのように寄与するか、またレイヤーを理解することがイメージの最適化にどのように役立つかを学びました。
  2. マルチステージビルド:ビルド環境と実行環境を分離することで、最終的なイメージサイズを小さくするためにこのテクニックを使用しました。
  3. .dockerignore ファイルの活用:ビルドコンテキストから不要なファイルを除外する方法を学びました。これによりビルドが高速化され、イメージサイズが削減されます。
  4. 高度な Dockerfile 命令:USER、ENTRYPOINT、HEALTHCHECK、ARG といった追加の命令を探索しました。これらにより、より安全で柔軟なイメージを作成できます。

これらのテクニックにより、以下のことが可能になります:

  • より最適化され、サイズの小さい Docker イメージの作成
  • 非ルートユーザーによるアプリケーション実行によるセキュリティの向上
  • より良いコンテナオーケストレーションのためのヘルスチェックの実装
  • 柔軟なイメージ構築のためのビルド時変数の使用

この実験を通じて、ファイルの編集には WebIDE(VS Code)を使用しました。これにより、ブラウザ上で Dockerfile やアプリケーションコードを直接作成・修正することが容易になりました。このアプローチは、Docker を使用した開発においてシームレスな体験を提供します。