Dockerイメージ作成入門【初心者向け】

はい、承知いたしました。Dockerイメージ作成の入門記事として、初心者向けに約5000語の詳細な解説を含む記事を作成します。


Dockerイメージ作成入門【初心者向け】

はじめに:なぜ今、Dockerなのか?

現代のソフトウェア開発において、Dockerはもはやデファクトスタンダードと言える存在になりました。開発者は「自分の環境では動くのに、本番環境では動かない」といった問題を劇的に減らすことができ、運用担当者はアプリケーションのデプロイや管理を大幅に効率化できます。

そのDockerの中心的な要素の一つが「Dockerイメージ」です。Dockerイメージは、アプリケーションとその実行に必要なすべてのもの(コード、ランタイム、ライブラリ、環境変数、設定ファイルなど)を一つにまとめた、静的なテンプレートのようなものです。このイメージから「コンテナ」という、アプリケーションを実行可能な独立した環境が作られます。

例えるなら、DockerイメージはOSのインストールメディアや、VMwareなどの仮想マシンで使う「ディスクイメージ」に近い概念です。ただし、Dockerイメージはもっと軽量で、特定のアプリケーションの実行に特化しており、仮想マシンほどOS全体をエミュレートするわけではありません。

この記事では、このDockerイメージを自分でゼロから作成する方法を、初心者の方にも分かりやすいように詳細に解説します。Dockerの基本から、Dockerfileという設計図の書き方、そして実践的なイメージ作成のテクニックまで、ステップバイステップで学んでいきましょう。

この記事を読むことで、あなたは以下のことができるようになります。

  • Dockerイメージとコンテナの違いを理解する。
  • Dockerfileの基本的な書き方を理解する。
  • 自分のアプリケーション用のDockerイメージを作成する。
  • 作成したイメージを実行し、コンテナを起動する。
  • イメージビルドの効率化や最適化の基本的な考え方を理解する。

さあ、Dockerイメージ作成の世界へ飛び込みましょう!

1. Dockerの基本を押さえよう

Dockerイメージの作成に入る前に、まずはDockerの基本的な概念をいくつか確認しておきましょう。

1.1 コンテナとイメージの違い

最も重要なのが、「コンテナ」と「イメージ」の違いです。

  • Dockerイメージ (Image)

    • アプリケーションを実行するために必要なすべてのものを一つにまとめた、読み取り専用のテンプレートです。
    • コード、ランタイム(Python、Node.jsなど)、ライブラリ、設定ファイル、環境変数などが含まれます。
    • 一度ビルドすれば、どのDocker環境でも同じように動作することが期待できます。
    • いわば「設計図」や「金型」のようなものです。
  • Dockerコンテナ (Container)

    • Dockerイメージを基にして作成される、アプリケーションの実行可能なインスタンスです。
    • イメージは読み取り専用ですが、コンテナは実行中にデータの書き込みなどが可能です(コンテナ内で作成/変更されたファイルは、コンテナを削除すると消えます。永続化には別途ボリュームなどの仕組みを使います)。
    • 他のコンテナやホストシステムから隔離された独立した環境でアプリケーションが実行されます。
    • いわば「設計図から作られた、実際に動く製品」や「金型から作られた具体的な物体」のようなものです。

一つのDockerイメージから、複数のDockerコンテナを同時に起動することができます。例えば、同じウェブアプリケーションのイメージから、負荷分散のために複数のコンテナを立ち上げるといった使い方をします。

1.2 Docker Hubとレジストリ

作成したDockerイメージを共有したり、他の人が作成したイメージを利用したりするために、「Dockerレジストリ」という仕組みがあります。Docker Hubはその代表例です。

  • Dockerレジストリ (Registry)

    • Dockerイメージを保管・配布するための倉庫です。
    • パブリックなレジストリ(Docker Hubなど)とプライベートなレジストリがあります。
  • Docker Hub

    • Docker社が提供する、世界最大のパブリックなDockerレジストリです。
    • 公式イメージ(Ubuntu, Alpine, Nginx, Node.js, Pythonなど)や、個人・企業が公開しているイメージが多数登録されています。
    • docker pull <イメージ名> コマンドでイメージをダウンロードしたり、docker push <イメージ名> コマンドで自分が作ったイメージをアップロードしたりできます(アップロードにはアカウントが必要です)。

この記事では、自分でイメージを作成し、それを自分のローカル環境で実行するところまでを主に扱いますが、イメージを共有する際にはレジストリの存在が重要になります。

1.3 Dockerfileの役割

Dockerイメージを手動で構築することも可能ですが、一般的には「Dockerfile」というテキストファイルを使ってイメージをビルドします。Dockerfileは、イメージを作成するための一連の手順(命令)を記述したスクリプトです。

例えば、「Ubuntuをベースにして、Pythonをインストールし、アプリケーションのコードをコピーして、特定のコマンドを実行する」といった手順をDockerfileに記述します。

“`dockerfile

これはコメント行です

どのイメージをベースにするか

FROM ubuntu:22.04

作成者情報を記載することもできます(必須ではない)

LABEL maintainer=”Your Name your.email@example.com

イメージ内で実行するコマンド

RUN apt-get update && \
apt-get install -y python3 python3-pip && \
rm -rf /var/lib/apt/lists/*

ホストOSからイメージにファイルをコピー

COPY ./app /app

イメージ内で作業ディレクトリを設定

WORKDIR /app

コンテナ起動時に実行されるコマンド

CMD [“python3”, “app.py”]
“`

このように、Dockerfileはイメージ作成の「レシピ」や「設計図」として機能します。このDockerfileを使って docker build コマンドを実行すると、DockerエンジンがDockerfileに記述された手順に従って自動的にイメージをビルドしてくれます。

Dockerfileを使う最大のメリットは、イメージ作成プロセスを自動化再現可能共有可能にできる点です。Dockerfileを見れば、そのイメージがどのように作られているかが一目でわかりますし、誰でも同じDockerfileを使えば同じイメージを作成できます。

1.4 Dockerエンジンの仕組み(簡単な説明)

Dockerエンジンは、Dockerイメージのビルドやコンテナの実行を管理するソフトウェアです。クライアント(dockerコマンドを使う部分)とデーモン(バックグラウンドで実行されるサービス)から構成されます。

  • Dockerクライアント: ユーザーが操作するコマンドラインツールです。docker builddocker runなどのコマンドを受け付けます。
  • Dockerデーモン (dockerd): クライアントからの要求を受けて、イメージのビルド、コンテナの実行、ストレージやネットワークの管理など、実際の処理を行います。

あなたが docker build コマンドを実行すると、クライアントはその要求をDockerデーモンに送信します。デーモンは受け取ったDockerfileを解析し、記述された命令を一つずつ実行してイメージを作成します。

イメージは「レイヤー」と呼ばれる読み取り専用の層の積み重ねで構成されます。Dockerfileの各命令(FROM, RUN, COPYなど)は、通常、新しいレイヤーを作成します。イメージはこれらのレイヤーを積み重ねたものであり、コンテナはイメージの全レイヤーの上に、読み書き可能な新しいレイヤーを重ねることで作成されます。このレイヤー構造のおかげで、イメージの効率的な共有や、ビルド時のキャッシュ活用が可能になっています(これは後述します)。

2. Docker環境の準備

Dockerイメージを作成・実行するには、まずDockerがインストールされている必要があります。ここでは、一般的な環境でのDocker Desktopのインストール方法を簡単に説明します。

2.1 Docker Desktopのインストール

Docker Desktopは、Windows、macOS、Linux (一部ディストリビューション) で利用できる、Dockerを簡単に開始するための統合環境です。GUIツールとコマンドラインツールが含まれています。

公式サイトからインストーラーをダウンロードして、指示に従ってインストールしてください。

  • Windows: Docker Desktop for Windows
    • WSL 2 (Windows Subsystem for Linux 2) が必要です。インストール時に自動的に設定されることが多いですが、必要に応じて事前にインストール・有効化してください。
  • macOS: Docker Desktop for Mac
    • Intelチップ搭載MacとAppleシリコン搭載Macの両方に対応しています。
  • Linux: Docker Desktop for Linux
    • Ubuntu, Debian, Fedoraに対応しています。他のLinuxディストリビューションの場合は、Docker Engineのインストールを参照してください。

インストール後、Docker Desktopアプリケーションを起動してください。初めて起動する際は、必要なコンポーネントのダウンロードや設定に時間がかかる場合があります。

2.2 インストール後の確認

ターミナル(コマンドプロンプト、PowerShell、macOSのターミナル、Linuxのシェルなど)を開き、以下のコマンドを実行してDockerが正しくインストールされ、実行可能か確認します。

bash
docker version

このコマンドを実行すると、DockerクライアントとDockerデーモンのバージョン情報が表示されます。バージョン情報が表示されれば、Dockerコマンドが実行可能な状態です。

次に、Dockerが正しくコンテナを実行できるか確認するために、公式のhello-worldイメージを使ったテストコンテナを実行してみましょう。

bash
docker run hello-world

このコマンドは以下の処理を行います。

  1. ローカルにhello-worldイメージが存在するか確認する。
  2. 存在しない場合、Docker Hubからhello-worldイメージをダウンロード(プル)する。
  3. ダウンロードしたイメージから新しいコンテナを作成し、実行する。
  4. コンテナは標準出力にメッセージ(”Hello from Docker!”のようなもの)を表示して終了する。

“Hello from Docker!”といったメッセージが表示されれば、Docker環境は正常に動作しています。これで、Dockerイメージ作成を始める準備が整いました。

3. はじめてのDockerfile

それでは、実際に簡単なアプリケーションのDockerイメージを作成してみましょう。ここでは、Pythonを使った非常にシンプルなウェブサーバーを例に取ります。このサーバーは、リクエストに対して「Hello, Docker!」というテキストを返すだけです。

3.1 サンプルアプリケーションの準備

まず、イメージに含めるサンプルアプリケーションを用意します。プロジェクト用のディレクトリを作成し、その中に以下の2つのファイルを作成します。

  1. app.py: Pythonのコード
  2. Dockerfile: イメージの設計図

ディレクトリ構造:

my-python-app/
├── app.py
└── Dockerfile

app.py の内容:

“`python

app.py

from http.server import BaseHTTPRequestHandler, HTTPServer
import time

HOST_NAME = “0.0.0.0” # どのIPアドレスからの接続も許可
SERVER_PORT = 8000 # 待ち受けるポート番号

class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200) # 成功応答
self.send_header(“Content-type”, “text/plain”) # ヘッダー設定
self.end_headers() # ヘッダー終了
# レスポンスボディを送信
self.wfile.write(bytes(“Hello, Docker!\n”, “utf-8”))

if name == “main“:
webServer = HTTPServer((HOST_NAME, SERVER_PORT), MyHandler)
print(f”Server started http://{HOST_NAME}:{SERVER_PORT}”)

try:
    webServer.serve_forever() # サーバー起動
except KeyboardInterrupt:
    pass # Ctrl+Cで停止

webServer.server_close() # サーバー停止
print("Server stopped.")

“`

このコードは、Pythonの標準ライブラリだけで動くシンプルなHTTPサーバーです。8000番ポートで待ち受け、どのGETリクエストに対しても「Hello, Docker!」と返します。

3.2 Dockerfileの作成手順と基本命令

次に、このPythonアプリケーションをコンテナ内で実行するためのDockerfileを作成します。

Dockerfile の内容:

“`dockerfile

Dockerfile

1. ベースイメージの指定

Python実行環境が必要なので、公式のPythonイメージを使う

タグ (例: 3.9-slim) を指定することで、特定のバージョンや軽量版を選択できる

FROM python:3.9-slim

2. イメージ内の作業ディレクトリを設定

以降の命令 (COPY, RUN, CMDなど) は、このディレクトリが基準となる

WORKDIR /app

3. アプリケーションコードをイメージにコピー

ホストOSの ./app.py ファイルを、イメージ内の /app ディレクトリにコピー

COPY app.py /app/

4. コンテナ起動時に実行されるコマンドを指定

コンテナが起動したときに、/appディレクトリで app.py を実行する

CMD [“python”, “app.py”]
“`

このDockerfileには、Dockerイメージを作成するための最低限必要な命令が記述されています。それぞれの命令について解説します。

  • FROM <image>[:<tag>]:
    • 新しいイメージを構築するためのベースイメージを指定します。全てのDockerfileはFROM命令から始まります(例外はARG命令をFROM命令より先に記述する場合など、ごく一部です)。
    • Dockerイメージはレイヤー構造になっており、ベースイメージはその一番下のレイヤーを提供します。
    • ここでは、Pythonアプリケーションを実行するために、公式のpythonイメージを使っています。:3.9-slimはタグで、バージョン3.9の軽量版を指定しています。タグを指定しない場合はデフォルトで:latestが使われますが、特定のバージョンを指定する方が再現性が高まるため推奨されます。
  • WORKDIR <directory>:
    • Dockerfile中でその後に続くRUN, CMD, ENTRYPOINT, COPY, ADDなどの命令が実行される際の作業ディレクトリを設定します。
    • 絶対パスで指定します。もし指定したディレクトリが存在しない場合は、自動的に作成されます。
    • これにより、COPY app.py /app/ の代わりに COPY app.py . と書いたり、CMD ["python", "/app/app.py"] の代わりに CMD ["python", "app.py"] と書いたりできるようになり、Dockerfileが見やすくなります。
  • COPY <src>... <dest>:
    • ビルドコンテキスト(Dockerfileが存在するディレクトリとそのサブディレクトリ)内のファイルやディレクトリを、構築中のイメージの指定されたパスにコピーします。
    • <src>はコピー元(Dockerfileからの相対パス)、<dest>はコピー先(イメージ内のパス)を指定します。
    • ここでは、ホストOSのapp.pyファイルを、イメージ内の/app/ディレクトリにコピーしています。WORKDIR /appを指定しているので、実際にはイメージ内の/app/app.pyとして配置されます。
  • CMD ["executable","param1","param2"]:

    • イメージを基にコンテナが起動されたときに、デフォルトで実行されるコマンドを指定します。
    • Dockerfile中に複数記述された場合、最後のCMD命令だけが有効になります。
    • CMDには、以下の3つの形式があります。

      1. CMD ["executable","param1","param2"] (exec形式 – 推奨): JSON配列の形式で、実行するプログラムとその引数を指定します。これにより、シェルを介さずに直接プログラムが実行されます。シグナル(Ctrl+Cなど)が正しく処理されるため、この形式が推奨されます。
      2. CMD command param1 param2 (shell形式): シェル(Linuxでは/bin/sh -c)を介してコマンドが実行されます。環境変数などが展開されますが、プロセスID 1で実行されないためシグナル処理に注意が必要です。
      3. CMD ["param1","param2"] (ENTRYPOINTのデフォルト引数): ENTRYPOINT命令と組み合わせて使われる形式です。
    • ここではexec形式で["python", "app.py"]を指定しており、コンテナ起動時にpython app.pyというコマンドが実行されます。

これらが、非常にシンプルなイメージを作成するための基本的なDockerfileの要素です。

3.3 docker buildコマンドの使い方

Dockerfileを作成したら、次はそれを使ってDockerイメージをビルドします。ビルドにはdocker buildコマンドを使用します。

my-python-appディレクトリがある場所でターミナルを開き、以下のコマンドを実行します。

bash
docker build -t my-python-app:latest .

このコマンドについて解説します。

  • docker build: Dockerイメージをビルドするためのコマンドです。
  • -t <image_name>:<tag>: ビルドするイメージに名前タグを付けるオプションです。
    • <image_name>: イメージの名前を指定します(例: my-python-app)。
    • <tag>: イメージのバージョンなどを識別するためのタグを指定します(例: latest, v1.0.0, devなど)。タグを指定しない場合、デフォルトで:latestが使用されます。:ごと省略すると、名前のみが設定され、タグは自動的に:latestになります。
    • 名前には、[ユーザ名/]<イメージ名> の形式を使うのが一般的です。Docker Hubにプッシュする際には、ユーザ名/イメージ名 の形式でないとプッシュできません。今回はローカルで使うだけなのでmy-python-appのようなシンプルな名前でも構いません。
  • .: Dockerfileが置かれているディレクトリのパスを指定します。このパスは「ビルドコンテキスト」と呼ばれます。Dockerデーモンはビルドコンテキスト内のファイルにアクセスして、COPYADD命令でイメージに取り込みます。. は「現在のディレクトリ」を意味します。

コマンドを実行すると、Dockerデーモンが起動し、Dockerfileを解析して記述された命令を順番に実行していきます。各ステップ(命令)が実行されるたびに、新しい「レイヤー」が作成されます。

[+] Building 12.3s (6/6) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 137B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9-slim 2.9s
=> [1/2] FROM docker.io/library/python:3.9-slim@sha256:... 7.6s
=> => extracting sha256:... 4.4s
=> [2/2] WORKDIR /app 0.0s
=> [3/2] COPY app.py /app/ 0.0s
=> [4/2] CMD ["python", "app.py"] 0.0s
=> exporting to image 1.6s
=> => exporting layers 1.5s
=> => writing image sha256:... 0.0s
=> => naming to docker.io/library/my-python-app:latest 0.0s

上記のような出力が表示され、最後にnaming to docker.io/library/my-python-app:latestのようなメッセージが出れば、イメージのビルドは成功です。

docker imagesコマンドを実行すると、ローカルに存在するイメージの一覧を確認できます。

bash
docker images

出力の中に、作成したmy-python-appイメージが表示されているはずです。

REPOSITORY TAG IMAGE ID CREATED SIZE
my-python-app latest abcdef123456 2 minutes ago 123MB
python 3.9-slim ghijkl789012 2 days ago 120MB
hello-world latest mnopq345678 2 months ago 13.3kB

IMAGE IDはイメージを一意に識別するためのIDです。CREATEDはそのイメージが作成された日時、SIZEはイメージのサイズです。ベースイメージであるpython:3.9-slimも表示されています。

3.4 ビルドしたイメージの実行(docker run

イメージが正常にビルドできたので、次にこのイメージを使ってコンテナを実行してみましょう。実行にはdocker runコマンドを使います。

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

このコマンドについて解説します。

  • docker run <image>[:<tag>]: 指定したイメージから新しいコンテナを作成し、実行します。
  • -p <host_port>:<container_port>: ポートマッピングを設定します。コンテナ内の特定のポート(ここでは8000番)への通信を、ホストOSの特定のポート(ここでは8000番)に転送します。これにより、ホストOSからコンテナ内のサーバーにアクセスできるようになります。
  • my-python-app:latest: 実行したいイメージの名前とタグを指定します。

コマンドを実行すると、コンテナが起動し、中でPythonスクリプトが実行されます。

Server started http://0.0.0.0:8000

というメッセージがターミナルに表示されるはずです。これは、コンテナ内のPythonサーバーが起動し、8000番ポートで待ち受けを開始したことを示しています。

これでサーバーが起動しました。別のターミナルを開くか、ウェブブラウザで以下のURLにアクセスしてみましょう。

http://localhost:8000/

ウェブブラウザには「Hello, Docker!」というテキストが表示されるはずです。これで、作成したDockerイメージから起動したコンテナ内で、アプリケーションが正しく動作していることを確認できました!

サーバーを停止するには、サーバーが起動しているターミナルで Ctrl+C を押してください。

これで、Dockerfileを作成し、イメージをビルドし、コンテナを実行するという、Dockerイメージ作成の基本的な流れを体験できました。

4. Dockerfileの詳細な命令解説

ここからは、Dockerfileで使える様々な命令について、より詳しく解説していきます。これらの命令を使いこなすことで、より複雑なアプリケーションに対応したり、イメージを効率的に構築したりできるようになります。

Dockerfileの命令は、大文字で書くのが慣習です。

4.1 基本的な命令

FROM <image>[:<tag>] [AS <name>]

  • 説明: 新しいビルドステージのベースイメージを指定します。Dockerfileの最初の命令である必要があります(ARGが先頭に来る場合を除く)。AS <name>を使うことで、マルチステージビルドの際にステージに名前を付けることができます。
  • 使い方: FROM ubuntu:22.04, FROM python:3.9-slim AS builder, FROM alpine
  • 注意点: タグは省略可能ですが、再現性のために特定のタグを指定することが強く推奨されます。:latestタグはイメージの内容が頻繁に更新される可能性があるため、本番環境などで使用する場合は注意が必要です。

RUN <command> (shell form) または RUN ["executable", "param1", "param2"] (exec form)

  • 説明: イメージのビルド中にコマンドを実行します。実行結果は新しいレイヤーとしてイメージにコミットされます。アプリケーションのインストール、ディレクトリの作成、ファイルのダウンロードなど、イメージの準備作業に使われます。
  • 使い方:
    • RUN apt-get update && apt-get install -y some-package
    • RUN ["pip", "install", "-r", "requirements.txt"]
    • RUN mkdir /app
  • shell形式 (RUN command param1 param2): コマンドはデフォルトシェル (/bin/sh -c on Linux, cmd /S /C on Windows) を介して実行されます。環境変数の展開などが容易です。
  • exec形式 (RUN ["executable", "param1", "param2"]): シェルを介さずに、指定された実行ファイルとその引数が直接実行されます。推奨される形式です。
  • 注意点:
    • RUN命令は新しいレイヤーを作成します。関連する複数のコマンドは、&&でつないで一つのRUN命令にまとめることで、レイヤー数を減らし、イメージサイズを小さくし、ビルドキャッシュを効率的に利用できます。(例: RUN apt-get update && apt-get install -y ... && rm -rf ...
    • 特にパッケージインストール後は、不要なキャッシュファイルなどを削除することで、イメージサイズを削減できます(例: rm -rf /var/lib/apt/lists/* after apt-get install).
    • 長いコマンドは、バックスラッシュ \ を使って複数行に分割できます。

CMD ["executable","param1","param2"] (exec form), CMD command param1 param2 (shell form), or CMD ["param1","param2"] (params to ENTRYPOINT)

  • 説明: イメージを基にコンテナが起動されたときに、デフォルトで実行されるコマンドを指定します。
  • 使い方:
    • CMD ["python", "app.py"] (exec形式)
    • CMD python app.py (shell形式)
    • CMD ["--help"] (ENTRYPOINTと組み合わせて使う場合)
  • 注意点:
    • Dockerfile中に複数CMD命令がある場合、最後の1つだけが有効になります。
    • ENTRYPOINT命令も指定されている場合、CMDENTRYPOINTに対するデフォルトの引数として扱われます。この使い方が一般的で推奨されます。
    • docker runコマンドで別のコマンド(例: docker run my-python-app bash)を指定した場合、DockerfileのCMD上書きされます。
    • exec形式が推奨されます。shell形式はプロセスID 1がシェルになるため、コンテナがシグナル(SIGTERMなど)を正しく受け取れず、 graceful shutdown ができない場合があります。

ENTRYPOINT ["executable", "param1", "param2"] (exec form) or ENTRYPOINT command param1 param2 (shell form)

  • 説明: コンテナが実行可能ファイルとして振る舞うように設定します。ENTRYPOINT命令は、コンテナ起動時に常に実行されるコマンドを指定します。
  • 使い方:
    • ENTRYPOINT ["gunicorn", "app:app", "-b", "0.0.0.0:8000"]
    • ENTRYPOINT ["docker-entrypoint.sh"]
  • ENTRYPOINT vs CMD:
    • CMDはデフォルトの実行コマンドであり、docker runの引数で簡単に上書きできます。
    • ENTRYPOINTはコンテナの主たるコマンドであり、docker runの引数はENTRYPOINTコマンドへの引数として渡されます。ENTRYPOINT自体を上書きするには、docker run --entrypoint ... という特別なオプションが必要です。
    • 一般的には、ENTRYPOINTでアプリケーションの起動スクリプト(例: ["docker-entrypoint.sh"])を指定し、CMDでそのスクリプトへのデフォルト引数(例: ["run"]["serve"])またはアプリケーションの主たるコマンドとその引数を指定します。例えば、ENTRYPOINT ["curl", "-s"]CMD ["http://example.com"] とすると、コンテナ実行時に curl -s http://example.com が実行されます。
    • こちらもexec形式が推奨されます。
  • 例(ENTRYPOINTCMDの組み合わせ):
    dockerfile
    ENTRYPOINT ["echo", "Hello"]
    CMD ["World!"]

    このイメージを実行すると、echo Hello World! が実行されます。
    docker run my-image John と実行すると、echo Hello John が実行されます。

COPY <src>... <dest>

  • 説明: ビルドコンテキスト内の指定されたファイルやディレクトリを、イメージ内の指定されたパスにコピーします。
  • 使い方: COPY ./app /app, COPY requirements.txt ., COPY --from=builder /app/dist /app
  • 注意点:
    • コピー元はビルドコンテキスト内にある必要があります。ビルドコンテキストの外にあるファイルはコピーできません。
    • --from=<name|index> オプションを使用すると、マルチステージビルドで前のビルドステージからファイルをコピーできます。これは非常に重要で、ビルドに必要なツールなどが含まれる中間イメージから、最終的な実行環境イメージに必要なファイルだけをコピーすることで、最終イメージを軽量化できます。
    • ディレクトリをコピーする場合、コピー元ディレクトリの中身がコピー先ディレクトリにコピーされます。例えば、COPY ./app /app/ とすると、ホストの ./app ディレクトリ内のファイルが、イメージの /app/ ディレクトリ直下にコピーされます。コピー先ディレクトリが存在しない場合は作成されます。
    • 特別なファイルとして認識される...はコピー元に使えません。

ADD <src>... <dest>

  • 説明: COPYと似ていますが、以下の追加機能があります。
    • コピー元がURLの場合、ファイルをダウンロードしてイメージに配置します。
    • コピー元が圧縮ファイル(tar, gzip, bzip2など)の場合、イメージ内で自動的に解凍します。
  • 使い方: ADD http://example.com/some.tar.gz /tmp/, ADD app.tar.gz /app/
  • 注意点:
    • 自動解凍やURLダウンロード機能は便利ですが、意図しない展開や外部リソースへの依存を生む可能性があります。シンプルにファイルやディレクトリをコピーするだけであれば、COPY命令の使用が推奨されますCOPYの方が透明性が高く、キャッシュも効率的に機能しやすいです。
    • ADDは自動解凍するため、キャッシュが無効になりやすいです。例えば、外部URLのコンテンツが変わっても、URL自体が変わらない限りDockerはキャッシュを利用しようとする可能性があります(ただし、最近のDockerバージョンではこの辺りも改善されています)。ローカルのtarファイルをADDすると、tarファイルの内容が変わってもファイル名が同じであればキャッシュが使われてしまい、更新が反映されない可能性があります。
    • セキュリティ面でも、URLダウンロードは中間者攻撃などのリスクを伴う可能性があります。

WORKDIR <directory>

  • 説明: その後に続くRUN, CMD, ENTRYPOINT, COPY, ADDなどの命令が実行される際の作業ディレクトリを設定します。
  • 使い方: WORKDIR /app, WORKDIR /usr/src/app
  • 注意点:
    • 相対パスを指定した場合、それは直前のWORKDIR命令に対する相対パスとして解釈されます。例えば、WORKDIR /app の後に WORKDIR src とすると、作業ディレクトリは /app/src になります。
    • 存在しないディレクトリを指定した場合、自動的に作成されます。

ENV <key>=<value> ... または ENV <key> <value>

  • 説明: イメージ内に環境変数を設定します。設定された環境変数は、イメージから起動されるコンテナ内で利用可能になります。
  • 使い方: ENV PORT=8000, ENV DEBUG=true, ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  • 注意点:
    • 設定された環境変数は、その後のDockerfileの命令(RUNなど)でも利用できます。例: RUN echo $PORT
    • 複数の環境変数をまとめて設定することもできます (ENV KEY1=VALUE1 KEY2=VALUE2)。
    • 環境変数はコンテナの動作に影響を与える重要な設定として利用されます。

ARG <name>[=<default value>]

  • 説明: イメージをビルドする際に、docker buildコマンドの引数として渡せる変数を定義します。
  • 使い方: ARG VERSION=latest, ARG BUILD_DATE
  • 注意点:
    • ARGで定義した変数は、ビルド中のみ有効であり、ビルド後のイメージには永続化されませんENVとの大きな違い)。
    • docker build --build-arg <name>=<value> の形式で値を渡します。
    • Dockerfileの最初のFROM命令より前に記述することも可能ですが、その場合はそのARGはそのFROM命令でのみ有効になります。
    • パスワードなどの秘密情報をARGで渡してビルド中に利用することは、ビルド履歴に残る可能性があるため非推奨です。Docker BuildKitのシークレット機能など、より安全な方法を検討してください。

VOLUME ["/path/to/volume"] or VOLUME /path/to/volume

  • 説明: イメージを基にコンテナを起動する際に、指定されたパスがボリュームとして扱われることを示します。ボリュームはコンテナのライフサイクルとは独立してデータを永続化したり、ホストOSとコンテナ間でデータを共有したりするための仕組みです。
  • 使い方: VOLUME /app/data, VOLUME ["/var/lib/mysql"]
  • 注意点:
    • これは、指定されたパスがボリュームとしてマウントされる「意図」を示すものであり、実際にコンテナ起動時にボリュームが作成・マウントされるかどうかはdocker runコマンドのオプション(-v--mount)によります。
    • 指定されたパスにイメージビルド中にファイルが存在する場合、その内容は新しいボリュームにコピーされます。ただし、これは初回マウント時のみで、その後のコンテナ起動ではコピーされません。
    • 通常はデータベースのデータディレクトリやログディレクトリなど、コンテナが停止・削除されても永続化したいデータを置くパスに指定します。

EXPOSE <port> [<port>/<protocol>...]

  • 説明: コンテナがリッスンするポートを指定します。これはドキュメンテーション目的であり、実際にポートを公開するかどうかはdocker runコマンドのオプション(-p-P)に依存します。
  • 使い方: EXPOSE 80, EXPOSE 8000/tcp, EXPOSE 80 443
  • 注意点:
    • EXPOSEだけではポートは外部に公開されません。ポートを公開してホストOSからアクセス可能にするには、docker run -p (特定のポートをマッピング) または docker run -P (DockerfileのEXPOSEで指定されたすべてのポートをランダムなホストポートにマッピング) オプションが必要です。
    • プロトコル(tcpまたはudp)を指定しない場合、デフォルトはtcpです。

USER <user>[:<group>] or USER <uid>[:<gid>]

  • 説明: その後に続くRUN, CMD, ENTRYPOINT命令を、指定されたユーザーまたはUIDとして実行するように設定します。デフォルトではrootユーザーで実行されます。
  • 使い方: USER www-data, USER 1000, USER nobody:nogroup
  • 注意点:
    • セキュリティの観点から、root以外のユーザーでアプリケーションを実行することが推奨されます。root権限が必要な設定作業(パッケージインストールなど)が終わった後にUSER命令を使ってユーザーを変更します。
    • 指定するユーザー/グループはイメージ内に存在している必要があります。存在しないユーザー/グループを指定するとエラーになります。RUN groupadd ... && useradd ...のように、事前にユーザー/グループを作成しておく必要があります。

LABEL <key>="<value>" ...

  • 説明: イメージにメタデータ(ラベル)を追加します。イメージの作成者、バージョン、ライセンス情報、ドキュメントへのURLなど、様々な情報をKey-Value形式で付与できます。
  • 使い方: LABEL maintainer="Your Name", LABEL version="1.0" description="My web app", LABEL org.opencontainers.image.authors="Your Name"
  • 注意点:
    • ラベルはDockerや外部ツールがイメージを管理・検索する際に利用できます。
    • 複数のラベルは一つのLABEL命令にまとめることができます。これにより、新しいレイヤーの作成を抑えられます。

4.2 その他の命令(補足)

  • STOPSIGNAL <signal>:
    • コンテナを停止する際に、デフォルトでSIGTERMシグナルが送信されますが、この命令で別のシグナルを指定できます。アプリケーションが特定のシグナルを受け取って graceful shutdown するように設定する場合などに使用します。
    • 例: STOPSIGNAL SIGKILL
  • HEALTHCHECK [OPTIONS] CMD command:
    • コンテナが正常に動作しているか(アプリケーションが応答しているか)を定期的にチェックするための命令です。
    • 例: HEALTHCHECK --interval=5m --timeout=3s CMD curl -f http://localhost/ || exit 1
    • これにより、docker psなどでコンテナの状態を確認する際に、healthyunhealthyといったヘルスステータスが表示されるようになります。オーケストレーションツール(Kubernetesなど)でもヘルスチェック結果が利用されます。
  • SHELL ["executable", "parameters"]:
    • RUN, CMD, ENTRYPOINT命令のshell形式で使用されるデフォルトシェルを変更します。
    • 例: SHELL ["/bin/bash", "-c"] (bashを使う), SHELL ["powershell", "-command"] (WindowsでPowerShellを使う)
    • 特に指定しない限り、Linuxベースイメージでは/bin/sh -c、Windowsベースイメージではcmd /S /Cがデフォルトです。

これらの命令を組み合わせることで、様々な要件に応じたDockerイメージを作成できます。

5. イメージビルドの最適化

Dockerイメージはレイヤー構造になっており、これがビルドのキャッシュやイメージサイズの効率化に大きく貢献します。しかし、Dockerfileの書き方によっては、キャッシュが無効になりやすかったり、イメージサイズが肥大化したりすることがあります。ここでは、イメージビルドを最適化するためのいくつかのテクニックを紹介します。

5.1 キャッシュの活用(レイヤーの概念)

前述の通り、Dockerイメージは読み取り専用のレイヤーの積み重ねです。Dockerfileの各命令(FROMを除く最初の命令以降)は、通常、前のレイヤーの上に新しいレイヤーを作成します。

docker buildコマンドは、Dockerfileの各命令を順番に実行する際に、ビルドキャッシュを利用しようとします。ある命令を実行する際に、Dockerはそれまでのレイヤーの組み合わせと現在の命令が、過去にビルドした際に実行された命令と完全に一致するかどうかを確認します。一致した場合、その命令によって作成されたレイヤーがキャッシュとして再利用され、実際の命令実行はスキップされます。

このキャッシュ機構を効率的に利用するためには、変更される可能性の低い命令をDockerfileの上の方に配置するのがベストプラクティスです。

例えば、以下のDockerfileを考えてみましょう。

“`dockerfile

Dockerfile (非効率な例)

FROM ubuntu:22.04

RUN apt-get update && apt-get install -y … # 外部パッケージのインストール

COPY . /app # アプリケーションコードを全てコピー

… 後続の命令

“`

このDockerfileで、アプリケーションコード(/appディレクトリの中身)を少し変更した場合、COPY . /app命令がキャッシュミスとなり、それ以降の全ての命令(この例では特にありませんが)が再度実行されることになります。外部パッケージのインストールは時間がかかることが多いため、これは非効率です。

これを改善するためには、変更頻度の高いものを下の方に配置します。

“`dockerfile

Dockerfile (効率的な例)

FROM ubuntu:22.04

依存関係ファイルだけを先にコピーし、依存関係をインストール

これにより、依存関係ファイル (例: requirements.txt, package.json) が変わらない限り

apt-get install や pip install などの時間がかかるRUN命令のキャッシュが利用できる

COPY requirements.txt /app/
WORKDIR /app
RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
RUN pip install –no-cache-dir -r requirements.txt # –no-cache-dirでpipのキャッシュを削減

アプリケーションコードをコピー (変更頻度が高い)

COPY . /app

… 後続の命令 (CMDなど)

CMD [“python3”, “app.py”]
“`

この例では、まず依存関係ファイル(requirements.txt)だけをコピーし、その後依存関係をインストールしています。アプリケーションコード全体(.)は、その後にコピーしています。

もしapp.pyだけを変更した場合、COPY requirements.txtRUN apt-get ...RUN pip install ...のレイヤーはキャッシュが有効になります。キャッシュミスとなるのはCOPY . /appの命令からとなり、依存関係のインストールはスキップされるため、ビルド時間が短縮されます。

キャッシュ無効化のルール:

  • FROM命令: ベースイメージが変更された場合、または別のタグが指定された場合、キャッシュは無効になります。
  • RUN命令: 命令文字列が変更された場合、キャッシュは無効になります。
  • COPY, ADD命令: コピー元のファイルやディレクトリの内容またはメタデータ(最終更新時刻など)が変更された場合、キャッシュは無効になります。
  • その他の命令 (ENV, WORKDIR, CMDなど): 命令文字列が変更された場合、キャッシュは無効になります。

キャッシュを利用したくない場合は、docker build --no-cache . オプションを使用します。

5.2 .dockerignoreファイルの利用

.dockerignoreファイルは、イメージビルド時にビルドコンテキストから除外したいファイルやディレクトリを指定するためのファイルです。Gitにおける.gitignoreファイルと似ています。

ビルドコンテキスト(docker buildコマンドの最後の引数であるパス)には、Dockerfileが存在するディレクトリ以下の全てのファイルが含まれます。しかし、アプリケーションのソースコードには、バージョン管理システム関連のファイル(.git)、一時ファイル、ログファイル、ローカル環境固有の設定ファイル、依存関係のキャッシュディレクトリ(node_modulesなど)など、イメージに含める必要のないファイルがたくさん含まれていることが多いです。

これらの不要なファイルがビルドコンテキストに含まれると、以下のような問題が発生します。

  • ビルド速度の低下: Dockerデーモンに送信されるデータ量が増加します。
  • キャッシュミス: 不要なファイルが変更されただけでCOPY . /appのような命令のキャッシュが無効になる可能性があります。
  • イメージサイズの肥大化: 不要なファイルがイメージにコピーされてしまいます。
  • セキュリティリスク: 誤って機密情報が含まれたファイルをイメージに含めてしまう可能性があります。

.dockerignoreファイルを作成し、イメージに不要なファイルやディレクトリを指定することで、これらの問題を回避できます。

my-python-appディレクトリに .dockerignore ファイルを作成する場合の例:

“`

.dockerignore

.git
.gitignore
pycache
.pyc
.vscode/
venv/
dist/
build/
nohup.out
.log
temp/
“`

これにより、上記のファイルやディレクトリがビルドコンテキストから除外され、COPY . /appのような命令を実行してもイメージには含まれなくなります。

5.3 RUNコマンドの結合

前述しましたが、RUN命令は一つ実行されるごとに新しいレイヤーを作成します。不必要なレイヤーの積み重ねは、イメージサイズの肥大化につながります。

関連する複数のコマンドは、&&でつなぎ、一つのRUN命令にまとめることが強く推奨されます。

“`dockerfile

非効率な例

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y some-package
RUN cleanup-script.sh

効率的な例

FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package && \
cleanup-script.sh && \
rm -rf /var/lib/apt/lists/* # 不要なキャッシュ削除
“`

このようにまとめることで、実行されるRUN命令の数を減らし、結果としてイメージのレイヤー数を減らすことができます。また、apt-get updateapt-get installを同じRUN命令で行うことで、apt-get updateのキャッシュが原因で古いパッケージがインストールされてしまう問題を回避できます。

5.4 不要なファイルの削除

ビルド中に一時的に必要だったが、最終的なイメージには不要なファイルやキャッシュは、同じRUN命令内で削除することが重要です。そうしないと、そのファイルが一度レイヤーとしてコミットされてしまい、後から削除してもそのレイヤーは残るため、イメージサイズは減りません。

“`dockerfile

BAD: 一時ファイルを削除しても、前のレイヤーに残る

RUN wget http://example.com/somefile.tar.gz -O /tmp/somefile.tar.gz
RUN tar -xf /tmp/somefile.tar.gz -C /app
RUN rm /tmp/somefile.tar.gz

GOOD: 同じRUN命令でダウンロードと削除を行う

RUN wget http://example.com/somefile.tar.gz -O /tmp/somefile.tar.gz && \
tar -xf /tmp/somefile.tar.gz -C /app && \
rm /tmp/somefile.tar.gz
“`

特にパッケージマネージャー(apt-get, apk, yum, npm, pipなど)を使ったインストールでは、キャッシュファイルや一時ファイルが大量に生成されることがあります。これらをクリーンアップするコマンドをインストールコマンドと同じRUN命令に含めることで、イメージサイズを大幅に削減できます。

例 (apt):
dockerfile
RUN apt-get update && apt-get install -y some-package && rm -rf /var/lib/apt/lists/*

例 (apk – Alpine Linux):
dockerfile
RUN apk add --no-cache some-package

例 (pip):
dockerfile
RUN pip install --no-cache-dir -r requirements.txt

例 (npm):
dockerfile
RUN npm install --production && npm cache clean --force

5.5 マルチステージビルドの導入

マルチステージビルドは、Dockerイメージのビルドプロセスを複数の「ステージ」に分割する強力な機能です。これにより、ビルドに必要なツールチェーンやSDKを含むビルド用イメージと、アプリケーションの実行に必要なランタイムだけを含む最終的な実行用イメージを分けることができます。

例えば、Go言語やJava、フロントエンドのJavaScriptアプリケーション(React, Vueなど)は、ソースコードをコンパイル/ビルドして実行可能なバイナリや静的ファイル、圧縮されたJavaScriptファイルを生成します。このビルドプロセスには、コンパイラやビルドツール(Go SDK, Maven, Gradle, Webpack, Babelなど)が必要ですが、生成された成果物を実行する際にはこれらのツールは不要です。

マルチステージビルドを使わない場合、これらのツールを全て最終イメージに含める必要があり、イメージサイズが非常に大きくなってしまいます。

マルチステージビルドでは、以下のように複数のFROM命令を使います。

“`dockerfile

Stage 1: ビルドステージ

ビルドに必要なイメージを使用 (例: Go SDK)

FROM golang:1.20 AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

Goバイナリをビルド

RUN go build -o /app/my-app

Stage 2: 実行ステージ

アプリケーションの実行に必要な軽量イメージを使用 (例: Alpine Linux)

FROM alpine:latest

ビルドステージから実行可能バイナリをコピー

COPY –from=builder /app/my-app /app/my-app

最終イメージの作業ディレクトリと実行コマンド設定

WORKDIR /app
CMD [“./my-app”]
“`

この例では、

  1. 最初のFROM golang:1.20 AS builderで、builderという名前のビルドステージを定義し、Goコンパイラを含むイメージを使用します。
  2. このステージで依存関係のダウンロード (go mod download) やアプリケーションのビルド (go build) を行い、実行可能バイナリ /app/my-app を生成します。
  3. 二つ目のFROM alpine:latestで、最終的な実行ステージを開始し、軽量なAlpine Linuxイメージを使用します。
  4. COPY --from=builder /app/my-app /app/my-app命令で、前のステージ (builder) から、生成された実行可能バイナリだけを、新しいステージにコピーします。
  5. 最終的なイメージは、この二つ目のステージだけから作成されます。ビルドステージに含まれていたGoコンパイラや中間ファイルは最終イメージには含まれません。

これにより、最終イメージのサイズを大幅に削減できます。これは、ビルドプロセスが複雑で、多くのツールが必要な場合に非常に有効なテクニックです。

5.6 小さいベースイメージの選択

イメージサイズの最適化の最初のステップは、適切なベースイメージを選択することです。

  • フルサイズのLinuxディストリビューション: ubuntu, debian, centos, fedora など。多くのツールやライブラリが含まれており、使い慣れていることが多いですが、イメージサイズは大きくなります(数百MB〜1GB以上)。
  • 軽量ディストリビューション: alpine, distroless など。
    • Alpine Linux: 非常に軽量なLinuxディストリビューションで、busyboxという最小限のツールセットを使用しています。イメージサイズは数MB程度と非常に小さいのが特徴です。ただし、使用するパッケージマネージャーはapkであり、GlibcではなくMusl libcを使用しているため、一部のアプリケーションやライブラリで互換性の問題が発生する可能性があります。
    • Distroless: Googleが提供する、OSディストリビューションの機能(シェル、パッケージマネージャーなど)を完全に排除し、アプリケーションの実行に必要なもの(アプリケーション本体と依存ライブラリ)だけを含むイメージです。非常に軽量で、攻撃対象領域を減らすことができるためセキュリティ面で優れていますが、デバッグなどでコンテナ内に入るのが難しくなります。

アプリケーションの要件に応じて、適切なベースイメージを選択することが重要です。開発中は使い慣れたフルサイズのイメージで開始し、本番環境向けにイメージを最適化する際に軽量イメージへの移行を検討する、といったアプローチも可能です。

Pythonの場合、python:3.9のようなタグは通常フルサイズですが、python:3.9-slimは不要なパッケージを削減した軽量版、python:3.9-alpineはAlpine Linuxベースのさらに軽量なイメージです。

これらの最適化テクニックを組み合わせることで、より効率的で軽量なDockerイメージを作成できるようになります。

6. 実践的なイメージ作成例

これまで学んだことを活かして、もう少し実践的なアプリケーションのDockerイメージを作成してみましょう。ここでは、Node.jsのExpressフレームワークを使った簡単なウェブアプリケーションを例に取ります。

6.1 Node.js Expressアプリケーションのイメージ作成

以下のファイル構成でアプリケーションを作成します。

my-node-app/
├── app.js
├── package.json
└── Dockerfile

package.json の内容:

json
{
"name": "my-node-app",
"version": "1.0.0",
"description": "Simple Node.js Express App",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2"
},
"author": "",
"license": "ISC"
}

app.js の内容:

“`javascript
// app.js
const express = require(‘express’);
const app = express();
const port = 3000;

app.get(‘/’, (req, res) => {
res.send(‘Hello from Node.js Express!’);
});

app.listen(port, () => {
console.log(App listening at http://localhost:${port});
});
“`

このアプリケーションは、Expressを使って3000番ポートで待ち受け、「/」へのGETリクエストに対して「Hello from Node.js Express!」と返すだけです。

Dockerfile の内容:

“`dockerfile

Dockerfile

1. ベースイメージの指定

Node.js実行環境が必要なので、公式のNode.jsイメージを使用

FROM node:18-slim # 軽量版を指定

2. イメージ内の作業ディレクトリを設定

WORKDIR /app

3. package.jsonとpackage-lock.jsonを先にコピーし、依存関係をインストール

これにより、依存関係ファイルに変更がない限りnpm installのキャッシュが有効になる

COPY package*.json ./

依存関係のインストール (–productionで本番環境に必要なものだけインストール)

npm ciはpackage-lock.jsonに基づいて確実に依存関係をインストールし、キャッシュを活用する

RUN npm ci –production

4. アプリケーションコードをコピー

package.json以外のファイル(app.jsなど)をコピー

COPY . .

5. アプリケーションがリッスンするポートを宣言

EXPOSE 3000

6. コンテナ起動時に実行されるコマンドを指定

package.jsonで定義したstartスクリプトを実行

CMD [“npm”, “start”]

もしくは node app.js を直接実行する場合

CMD [“node”, “app.js”]

“`

解説:

  • FROM node:18-slim: Node.jsバージョン18の軽量版をベースイメージとして使用します。
  • WORKDIR /app: イメージ内の作業ディレクトリを/appに設定します。
  • COPY package*.json ./: package.jsonpackage-lock.json(もし存在すれば)を作業ディレクトリ(/app)にコピーします。依存関係のインストールより前にこれを置くことで、依存関係ファイルが変更されない限り、次に来るRUN npm ciのキャッシュを有効にできます。
  • RUN npm ci --production: コピーした依存関係ファイルに基づいて、本番環境に必要な依存関係だけをインストールします。npm cinpm installよりもCI/CD環境などでの利用に適しており、package-lock.jsonを使い、キャッシュを効率的に利用します。
  • COPY . .: 残りのアプリケーションコードを作業ディレクトリ(/app)にコピーします。.dockerignoreファイルがある場合は、その設定に従って不要なファイルは除外されます。
  • EXPOSE 3000: アプリケーションが3000番ポートでリッスンすることを宣言します。
  • CMD ["npm", "start"]: コンテナ起動時にnpm startコマンドを実行します。これにより、package.jsonで定義されたstartスクリプト(ここではnode app.js)が実行されます。

.dockerignoreファイルを作成する場合の例:

“`

.dockerignore

node_modules
npm-debug.log
yarn-error.log
.git
.gitignore
.vscode/
dist/
build/
“`

node_modulesディレクトリは、ホスト側でnpm installを実行して作成された場合でも、イメージ内ではDockerが管理するため、ホスト側のnode_modulesはイメージにコピーする必要がありません。.dockerignoreで除外することで、ビルドパフォーマンスとイメージサイズを改善できます。

6.2 イメージのビルドと実行

my-node-appディレクトリでターミナルを開き、以下のコマンドでイメージをビルドします。

bash
docker build -t my-node-app:latest .

ビルドが完了したら、以下のコマンドでコンテナを実行します。Node.jsアプリは3000番ポートで待ち受けるので、ホストOSのポートとマッピングします。

bash
docker run -p 3000:3000 my-node-app:latest

コンテナが起動し、Node.jsサーバーが開始したことを示すログが表示されるはずです。

“`

[email protected] start /app
node app.js

App listening at http://localhost:3000
“`

ウェブブラウザまたはcurlコマンドで http://localhost:3000/ にアクセスすると、「Hello from Node.js Express!」というテキストが表示されるはずです。

これで、Node.jsアプリケーションのDockerイメージを正常に作成・実行できました。

6.3 開発環境用イメージと本番環境用イメージ

アプリケーションによっては、開発時と本番時で必要なものが異なる場合があります。例えば、開発時にはデバッグツールやテストコード、開発用の依存関係が必要ですが、本番環境ではこれらは不要で、軽量でセキュアなイメージが望ましいです。

このような場合、マルチステージビルドや、ARGENVを使ってビルドプロセスを切り替える方法があります。

マルチステージビルドを使った例(Node.js):

“`dockerfile

Stage 1: 開発ステージ (依存関係のインストールなど)

FROM node:18 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install # 開発/テスト/本番全ての依存関係をインストール

COPY . . # 全てのソースコードをコピー

Stage 2: 本番ステージ (軽量イメージに必要なものだけコピー)

FROM node:18-slim # 軽量なNode.jsランタイムイメージ

WORKDIR /app

開発ステージから本番に必要なファイルだけをコピー

node_modules (production dependenciesのみ) とソースコード

COPY –from=builder /app/node_modules /app/node_modules
COPY –from=builder /app/app.js /app/app.js
COPY –from=builder /app/package.json /app/package.json # package.jsonはCMD/ENTRYPOINTで使う可能性があるのでコピー

EXPOSE 3000
CMD [“node”, “app.js”]
“`

この例では、ビルドステージで全ての依存関係をインストールし、その後の本番ステージでは--productionでインストールされた依存関係が含まれるnode_modulesと、必要なソースファイルだけをコピーしています。これにより、最終イメージには本番稼働に必要なものだけが含まれ、開発ツールなどは除外されます。

ARGを使った例:

“`dockerfile
ARG NODE_ENV=production # デフォルト値はproduction

FROM node:18-slim

WORKDIR /app

COPY package*.json ./

ビルド引数NODE_ENVの値に応じて依存関係のインストール方法を切り替える

RUN if [ “$NODE_ENV” = “development” ]; then npm install; else npm ci –production; fi

COPY . .

EXPOSE 3000

環境変数NODE_ENVをコンテナ内で設定 (これもARGの値を使うことができる)

ARG NODE_ENV
ENV NODE_ENV=$NODE_ENV

CMD [“node”, “app.js”]
“`

このDockerfileは、NODE_ENVというビルド引数を受け取ります。

  • デフォルトではNODE_ENVproductionとなり、npm ci --productionが実行されます。
  • 開発用にビルドしたい場合は、docker build --build-arg NODE_ENV=development -t my-node-app:dev . のようにビルド引数を渡します。これにより、npm installが実行され、開発用の依存関係も含まれるイメージが作成されます。

また、ビルド引数NODE_ENVの値を、コンテナ実行時に利用できる環境変数NODE_ENVとして設定しています。これは、多くのNode.jsアプリケーションがprocess.env.NODE_ENVを見て挙動を変えるためです。

このように、Dockerfileの書き方を工夫することで、様々な環境や目的に合わせたDockerイメージを作成できます。

7. イメージの管理

作成したDockerイメージを管理するための基本的なコマンドを紹介します。

  • docker images:
    • ローカルに存在するDockerイメージの一覧を表示します。
    • オプション: -a (中間イメージを含む全てのイメージを表示), --filter (特定の条件でフィルタリング), --format (表示形式を指定)
  • docker rmi <image_id>|<image_name>[:<tag>]:
    • 指定したDockerイメージを削除します。
    • イメージIDまたはイメージ名:タグで指定できます。
    • そのイメージから実行中のコンテナがある場合、またはそのイメージをベースにした他のイメージが存在する場合は、削除できません。強制的に削除するには -f オプションを使いますが、注意が必要です。
    • 未使用のイメージを全て削除するには、docker image prune コマンドが便利です。
  • docker image prune:
    • どのコンテナからも参照されていない未使用のイメージを削除します。
    • docker image prune -a とすると、コンテナから参照されていない全てのイメージ(中間イメージを含む)を削除します。
  • docker tag <source_image> <target_image>[:<tag>]:
    • 既存のイメージに新しいタグを付けます。例えば、特定のイメージIDに複数の名前/タグを付けたい場合に使います。
    • 例: docker tag my-app:latest my-app:v1.0, docker tag my-app:latest your-dockerhub-user/my-app:latest
  • docker push <image_name>[:<tag>]:
    • ローカルのDockerイメージをDockerレジストリ(デフォルトはDocker Hub)にアップロードします。
    • Docker Hubにアップロードする場合、イメージ名は[Docker Hubユーザー名]/[イメージ名]または[Docker Hubユーザー名]/[イメージ名]:[タグ]の形式である必要があります。docker loginコマンドで事前にログインが必要です。
  • docker pull <image_name>[:<tag>]:
    • DockerレジストリからDockerイメージをダウンロードします。
    • 例: docker pull ubuntu:22.04, docker pull nginx

これらのコマンドを使って、ローカル環境のイメージを整理したり、作成したイメージを共有したりできます。

8. トラブルシューティング

Dockerイメージのビルドやコンテナの実行中に発生しうる一般的な問題とその対処法について説明します。

8.1 ビルドエラーの原因と対処法

ビルド中にエラーが発生した場合、docker buildコマンドの出力メッセージを確認します。エラーメッセージは、どの命令で問題が発生したか、そしてその原因(ファイルが見つからない、コマンドの実行に失敗した、権限がないなど)を示しています。

  • 特定のRUN命令でエラーになる:
    • エラーメッセージを見て、実行されたコマンドとその出力、終了コードを確認します。
    • Dockerfileのその行をローカルのシェルで直接実行してみて、同じエラーが発生するか確認します。環境変数が違う可能性もあるので注意が必要です。
    • ビルドキャッシュが原因で問題が発生している可能性もあります。docker build --no-cache ... でキャッシュを使わずにビルドし直してみます。
    • ベースイメージに問題がある可能性もゼロではありません。
  • COPYまたはADD命令でエラーになる (ファイル/ディレクトリが見つからない):
    • コピー元パスが、Dockerfileからの相対パスで正しく指定されているか確認します。
    • .dockerignoreファイルで、コピーしたいファイルやディレクトリが誤って除外されていないか確認します。
    • docker buildコマンドを実行したディレクトリ(ビルドコンテキスト)に、Dockerfileとコピー元のファイル/ディレクトリが存在するか確認します。
  • レイヤーサイズが大きすぎる:
    • 中間イメージをビルド中に確認したい場合があります。Dockerfileの特定の命令の後に一時停止してコンテナを実行するには、docker build --progress=plain を使ってレイヤーIDを確認し、docker run -it <layer_id> /bin/bash などでその時点のコンテナに入って調査できます。
    • 不要なファイルを削除するステップが漏れていないか確認します (rm -rf ...)。特にパッケージマネージャーのキャッシュ。
    • RUN命令をまとめることで、レイヤー数を減らせるか検討します。
    • マルチステージビルドを使って、ビルドツールなどを最終イメージから除外できないか検討します。

8.2 イメージ実行時のエラー

ビルドは成功したが、docker runでコンテナを起動しようとするとエラーになる、またはコンテナがすぐに終了してしまう場合。

  • コンテナのログを確認する:
    • コンテナが起動しない、またはすぐに終了する場合、その原因はアプリケーションのエラーである可能性が高いです。
    • docker ps -a コマンドで、終了したコンテナを含めた全てのコンテナを表示します。ステータスが Exited (...) となっているはずです。
    • docker logs <container_id> コマンドで、そのコンテナの標準出力と標準エラー出力を確認します。ここにアプリケーションが出力したエラーメッセージが表示されているはずです。
  • ポートマッピングの問題:
    • アプリケーションがコンテナ内のどのポートでリッスンしているか確認します (EXPOSE命令やアプリケーションコードを確認)。
    • docker run -p <host_port>:<container_port> のポート番号が、アプリケーションがリッスンしているポートと一致しているか確認します。
    • ホストOSの指定したポートが、既に他のプロセスで使用されていないか確認します。
  • CMD/ENTRYPOINTの問題:
    • CMDまたはENTRYPOINTで指定したコマンドが、イメージ内に存在するか、実行可能か確認します。
    • コマンドの形式(shell vs exec)が適切か確認します。shell形式の場合、パスが通っているかなども関係します。
    • docker run -it <image_name> bash (または/bin/sh) のようにしてコンテナ内にシェルで入り、手動でアプリケーション起動コマンドを実行してみて、何が起きるか確認します。
  • ファイルパス/パーミッションの問題:
    • COPYなどでコピーしたファイルが、WORKDIRに対して正しいパスに配置されているか確認します。
    • USER命令で指定したユーザーが、アプリケーションファイルにアクセスできる権限を持っているか確認します。
    • ボリュームマウント(docker run -v ...)が関係する場合、ホスト側のファイルのパーミッションや所有者がコンテナ側のユーザーと合っているか確認します。

8.3 イメージサイズの肥大化への対処

既に最適化の章で触れましたが、イメージサイズが大きすぎる場合は以下の点を見直します。

  • .dockerignoreファイルが適切に設定されているか確認する。
  • RUN命令をまとめて、中間レイヤーで不要なファイルが残っていないか確認する(特にインストール後のキャッシュ削除)。
  • マルチステージビルドを導入し、ビルド環境と実行環境を分離する。
  • より軽量なベースイメージ(Alpine, Slim版など)の使用を検討する。

これらのトラブルシューティングの手順を踏むことで、イメージ作成や実行時の問題を解決に導くことができます。

9. より進んだトピック(触れる程度)

今回の入門記事では詳細に触れませんでしたが、Dockerイメージ作成に関連して、より進んだいくつか重要なトピックがあります。

  • Docker Compose: 複数のコンテナで構成されるアプリケーション(例: ウェブアプリ + データベース)を、YAMLファイル一つで定義・管理できるツールです。Docker Composeファイルの中で、各サービスのイメージ指定やビルド設定 (build: . でDockerfileを指定) を行うことができます。
  • セキュリティ: Dockerイメージのセキュリティは非常に重要です。最小限の権限でアプリケーションを実行する(rootユーザーを使用しない)、最小限のベースイメージを選択する、不要なパッケージを含めない、Credentialをイメージにハードコードしない(環境変数やシークレット管理システムを使う)、イメージスキャンツールを利用するなどのベストプラクティスがあります。
  • イメージスキャンツール: trivy, Clair, Docker Scoutなどのツールを使うと、イメージに含まれるソフトウェアの脆弱性を検出できます。CI/CDパイプラインに組み込むことで、脆弱性のあるイメージが本番環境にデプロイされるのを防ぐことができます。
  • Docker BuildKit: Dockerの新しいビルドエンジンです。並列ビルド、キャッシュの改善、シークレット管理、マルチステージビルドの効率化など、多くの改善が加えられています。Dockerfileの先頭に # syntax=docker/dockerfile:1 を追加することで利用できます。
  • コンテナレジストリ: Docker Hub以外にも、Google Container Registry (GCR), Amazon Elastic Container Registry (ECR), Azure Container Registry (ACR), Quay.ioなど、様々なパブリック/プライベートなコンテナレジストリがあります。目的に応じて適切なレジストリを選択します。

これらのトピックは、Dockerをより深く活用したり、本番環境で安全かつ効率的に運用するために非常に重要です。入門後の次のステップとして、ぜひ学んでみてください。

10. まとめ

この記事では、Dockerイメージ作成の入門として、Dockerの基本からDockerfileの書き方、イメージのビルド、実行、そして最適化やトラブルシューティングまで、幅広く詳細に解説しました。

  • Dockerイメージはアプリケーションとその実行に必要なものをまとめたテンプレートであり、コンテナはそのイメージから作られる実行インスタンスです。
  • Dockerイメージは通常、Dockerfileという設計図を使ってビルドします。
  • Dockerfileの主要な命令には、FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT, ENV, ARGなどがあります。
  • docker build -t <image_name> . でイメージをビルドし、docker run <image_name> でコンテナを実行します。docker run -p でポートをマッピングします。
  • イメージビルドの最適化には、キャッシュの活用、.dockerignoreの使用、RUN命令の結合、不要ファイルの削除、マルチステージビルド、軽量ベースイメージの選択といったテクニックがあります。
  • ビルドや実行時のトラブルは、エラーメッセージの確認、コンテナログの確認、コンテナ内でのデバッグなどで解決します。

Dockerイメージを自分で作成できるようになることは、現代のアプリケーション開発や運用において非常に重要なスキルです。これにより、開発環境と本番環境の差異による問題を減らし、アプリケーションの配布やデプロイを標準化できます。

この記事が、あなたがDockerイメージ作成の世界に一歩を踏み出すための助けとなれば幸いです。まずはこの記事で作成したシンプルな例を繰り返し試して、各命令の挙動やビルドプロセスに慣れてください。そして、ぜひご自身のアプリケーションのDockerイメージ作成に挑戦してみてください。

Happy Dockering!


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール