本ページはプロモーションが含まれています

Docker

3分でわかる!Dockerfileの書き方 基本から最適化まで

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

「Dockerを使い始めたけど、Dockerfileの書き方がいまいち分からない…」

「とりあえず動くものは作れたけど、もっと効率的な書き方はないの?」

これは、私が開発現場でDockerを使い始めたころに抱えていた悩みです。当時は手探りで情報を集め、試行錯誤を繰り返す毎日でした。特に、ビルドに時間がかかったり、イメージサイズが大きくなりすぎたりして、何度も書き直した経験があります。

しかし、いくつかの重要なポイントを押さえることで、ビルド時間を最大で30%削減し、イメージサイズを半分以下にできたのです。この記事は、過去の私と同じようにDockerfileの書き方で悩んでいる方に向けて執筆しました。

この記事を読めば、Dockerfileの基本的な書き方から、現場で役立つ最適化のテクニックまでを体系的に学べます。あなたのアプリケーションを効率よく、そして安全にコンテナ化するための第一歩を、この記事と共にはじめましょう。

Dockerfileとは?基本を理解しよう

Dockerfileの書き方を学ぶ前に、まずDockerfileが何であるかを正確に理解する必要があります。ここでは、Dockerfileの基本的な役割や、関連するDockerの仕組みについて分かりやすく解説します。

Dockerfileの役割とメリット

Dockerfileは、Dockerイメージを自動で作成するための設計図のようなものです。テキストファイルに、どのようなOSを使い、どんなソフトウェアをインストールし、どうやってアプリケーションを動かすか、といった一連の指示を記述します。

この設計図を使う大きなメリットは、環境構築をコードで管理できる点にあります。誰が実行しても全く同じ環境を何度でも再現できます。これにより、「私のパソコンでは動いたのに、他の人の環境では動かない」といった問題をなくせます。また、Gitなどでバージョン管理できるため、変更履歴を追うのも簡単になります。

イメージとコンテナの関係

Dockerを理解する上で、「イメージ」と「コンテナ」の関係を知ることは非常に重要です。この2つはよく混同されがちですが、役割が全くちがいます。

イメージとコンテナ

  • イメージ: アプリケーションを動かすために必要なもの(OS、ライブラリ、アプリケーションコードなど)をすべて含んだ、読み取り専用のテンプレートです。家のモデルハウスのようなものと考えると分かりやすいでしょう。
  • コンテナ: イメージから作成される、実際にアプリケーションが動く環境です。モデルハウス(イメージ)を元に建てられた、実際に人が住む家(コンテナ)にたとえられます。

Dockerfileはこの「イメージ」を作るための設計図であり、docker buildというコマンドでDockerfileからイメージが作られ、docker runというコマンドでイメージからコンテナが起動されます。

Dockerの基本的な仕組み

Dockerは、コンテナという独立した環境でアプリケーションを動かすためのプラットフォームです。その心臓部であるDocker Engineが、Dockerfileの指示を読み取ってイメージを構築します。

Dockerfileに書かれた命令は、上から順番に実行されます。そして、各命令が実行されるたびに「レイヤー」と呼ばれる層が作られ、積み重なっていくことで1つのイメージが完成します。このレイヤー構造が、後で説明するビルドの高速化において非常に重要な役割を果たします。

Dockerfileの書き方:基本構文と命令

ここからは、具体的なDockerfileの書き方について解説します。基本構造と、よく使われる命令を覚えれば、簡単なアプリケーションならすぐにコンテナ化できます。

Dockerfileの基本構造

Dockerfileの書き方はとてもシンプルです。基本的には、以下のような形式で1行ずつ指示を記述します。

# コメント
命令 引数

#で始まる行はコメントとして扱われ、実行時には無視されます。命令はFROMRUNなど、Dockerにあらかじめ用意されているものを大文字で書くのが一般的です。その後に、命令に対する具体的な指示を引数として記述します。

よく使う命令:FROM, RUN, COPY, ADD, WORKDIR, CMD, ENTRYPOINT, EXPOSE

Dockerfileの書き方で中心となるのが、これらの命令です。ここでは、特に使用頻度の高い8つの命令の役割と使い方を説明します。

  • FROMベースとなるイメージを指定します。すべてのDockerfileは、このFROM命令から始まります。例えば、UbuntuやNode.js、Pythonなど、Docker Hubで公開されている公式イメージを土台にできます。FROM ubuntu:22.04
  • RUNシェルコマンドを実行します。主に、ライブラリのインストールや必要なディレクトリの作成などに使われます。RUN apt-get update && apt-get install -y nginx
  • COPYホストマシン(自分のPC)にあるファイルやディレクトリを、イメージの中にコピーします。アプリケーションのソースコードをイメージに含める際によく使われます。COPY ./app /usr/src/app
  • ADDCOPYと似ていますが、URLを指定してファイルをダウンロードしたり、圧縮ファイル(tar.gzなど)を自動で展開したりする機能も持っています。特別な理由がない限りは、よりシンプルなCOPYを使うのが推奨されます。
  • WORKDIR以降のRUNやCMDなどの命令を実行する作業ディレクトリを指定します。cdコマンドのように、これ以降の命令はこのディレクトリを基準に実行されます。WORKDIR /usr/src/app
  • CMDコンテナを起動したときに、デフォルトで実行されるコマンドを指定します。1つのDockerfileに複数書いた場合は、最後のものだけが有効になります。CMD ["node", "server.js"]
  • ENTRYPOINTCMDと同様に、コンテナ起動時に実行されるコマンドを指定します。CMDとのちがいは、ENTRYPOINTで指定したコマンドは上書きされにくく、CMDは引数として渡される点です。
  • EXPOSEコンテナが外部に公開するポート番号を指定します。これはあくまでドキュメントとしての意味合いが強く、実際にポートを公開するにはdocker runコマンドで-pオプションが必要です。EXPOSE 8080

命令の実行順序とレイヤー構造

前述したように、Dockerfileは上から下に書かれた命令を順に実行し、そのたびにレイヤーが作成されます。例えば、以下のようなDockerfileがあったとします。

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
COPY ./app /app

この場合、

  1. ubuntu:22.04のイメージを土台にする(レイヤー1)
  2. apt-get updateを実行する(レイヤー2)
  3. curlをインストールする(レイヤー3)
  4. appディレクトリをコピーする(レイヤー4)

というように、4つの層が積み重なったイメージが作られます。この仕組みを理解することが、Dockerfileの書き方をマスターし、最適化を行うための鍵となります。

Dockerfile実践:アプリケーションの構築

基本的な命令を学んだところで、次は実際のアプリケーションをコンテナ化するDockerfileの書き方を見ていきましょう。ここでは、Node.js、Python、Javaの3つの例を紹介します。

Node.jsアプリケーションのDockerfile

Node.jsアプリケーションでは、package.jsonを使って依存パッケージを管理するのが一般的です。効率的なDockerfileの書き方のポイントは、ソースコードのコピーより先にパッケージのインストールを行うことです。

# ベースイメージとして公式のNode.jsイメージを選択
FROM node:18

# アプリケーション用のディレクトリを作成
WORKDIR /usr/src/app

# 依存関係の定義ファイルを先にコピー
COPY package*.json ./

# 依存パッケージをインストール
# ソースコードが変更されても、package.jsonに変更がなければキャッシュが使われる
RUN npm install

# アプリケーションのソースコードをコピー
COPY . .

# アプリケーションが使用するポートを公開
EXPOSE 3000

# アプリケーションを起動
CMD [ "node", "server.js" ]

PythonアプリケーションのDockerfile

Pythonの例もNode.jsと似ています。依存関係を定義したrequirements.txtを先にコピーして、pip installを実行するのが定石です。

# ベースイメージとして公式のPythonイメージを選択
FROM python:3.10

# 作業ディレクトリを設定
WORKDIR /usr/src/app

# 依存関係の定義ファイルを先にコピー
COPY requirements.txt ./

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

# アプリケーションのソースコードをコピー
COPY . .

# アプリケーションが使用するポートを公開
EXPOSE 5000

# アプリケーションを起動
CMD [ "python", "./main.py" ]

JavaアプリケーションのDockerfile

Javaアプリケーション(ここではMavenを使う例)では、ビルド(コンパイル)と実行で必要な環境が異なります。ビルドにはJDKが必要ですが、実行時にはより軽量なJREだけで十分です。このような場合に、後述するマルチステージビルドというテクニックが非常に有効です。

# --- ビルドステージ ---
# ビルド用のベースイメージとしてMavenが含まれるものを選択
FROM maven:3.8.5-openjdk-17 AS builder

# 作業ディレクトリを設定
WORKDIR /app

# Mavenのプロジェクト定義ファイルをコピー
COPY pom.xml .

# 依存関係をダウンロード(ソースコードコピーより先に行う)
RUN mvn dependency:go-offline

# ソースコードをコピー
COPY src ./src

# アプリケーションをビルド
RUN mvn package

# --- 実行ステージ ---
# 実行用の軽量なベースイメージを選択
FROM openjdk:17-jre-slim

# 作業ディレクトリを設定
WORKDIR /app

# ビルドステージからビルドされたjarファイルのみをコピー
COPY --from=builder /app/target/*.jar app.jar

# アプリケーションが使用するポートを公開
EXPOSE 8080

# アプリケーションを起動
ENTRYPOINT ["java", "-jar", "app.jar"]

Dockerfileの最適化:効率的なイメージ作成

Dockerfileの書き方の基本をマスターしたら、次はより効率的なイメージを作成するための最適化手法を学びましょう。ビルド時間の短縮やイメージサイズの削減は、開発効率を大きく向上させます。

レイヤーキャッシュの活用

Dockerは、一度実行した命令とその結果をレイヤーとしてキャッシュします。Dockerfileを再度ビルドする際、変更がない行まではキャッシュされたレイヤーを再利用するため、ビルドが高速になります。

この仕組みを最大限に活用するための重要なテクニックは、変更頻度の低い命令を上に、変更頻度の高い命令を下に書くことです。

例えば、アプリケーションのソースコードは頻繁に変更されますが、npm installpip installでインストールするライブラリはそれほど頻繁には変わりません。

そのため、先にpackage.jsonrequirements.txtだけをコピーしてライブラリをインストールし、その後にソースコード全体をコピーすることで、ソースコードの小さな変更で毎回ライブラリをインストールし直す必要がなくなります。

マルチステージビルドの活用

先ほどのJavaの例でも使ったマルチステージビルドは、イメージサイズを劇的に削減できる強力なテクニックです。

1つのDockerfileの中に複数のFROM命令を記述することで、ビルド工程を複数のステージに分割します。例えば、最初のステージでソースコードのコンパイルや依存関係のインストールを行い、次のステージでは、前のステージで生成された実行可能なファイル(jarファイルやバイナリなど)だけを、より軽量な実行用のベースイメージにコピーします。

これにより、最終的なイメージにはビルドツール(JDK、Maven、コンパイラなど)や中間ファイルが含まれなくなり、必要最小限のファイルだけで構成された非常に小さなイメージを作成できます。

イメージサイズの削減

マルチステージビルド以外にも、イメージサイズを削減するためのDockerfileの書き方がいくつかあります。

  • .dockerignoreファイルの使用: COPYADD命令でイメージに含めたくないファイル(.gitディレクトリ、ログファイル、ローカルの設定ファイルなど)を.dockerignoreファイルに記述します。これにより、不要なファイルがイメージに含まれるのを防ぎ、イメージサイズを小さく保てます。
  • パッケージマネージャのキャッシュ削除: RUN命令でapt-getyumなどを使う際は、インストール後にキャッシュを削除することで数十〜数百MBのサイズを削減できます。RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*
  • 適切なベースイメージの選択: ubuntuのようなフル機能のOSイメージではなく、alpineslimといった、より軽量なイメージをベースにすることで、イメージの基本サイズを大幅に小さくできます。

Dockerfileベストプラクティスと応用

最後に、セキュリティや保守性を高めるための、より進んだDockerfileの書き方について紹介します。これらのプラクティスを実践することで、本番環境でも安心して使える、より堅牢なイメージを作成できます。

セキュリティ対策:ユーザー設定と権限

Dockerコンテナは、デフォルトではrootユーザーでプロセスを実行します。これはセキュリティ上のリスクとなりうるため、一般ユーザーを作成してアプリケーションを実行することが強く推奨されます。

FROM node:18-alpine

WORKDIR /app

# 一般ユーザー'node'がすでに存在するので、所有者を変更
# ユーザーが存在しない場合は'RUN addgroup'と'RUN adduser'で作成
COPY --chown=node:node . .

# ユーザーを切り替え
USER node

CMD [ "node", "server.js" ]

このようにUSER命令を使うことで、コンテナ内のプロセスをroot以外の権限で実行し、万が一アプリケーションに脆弱性があった場合のリスクを低減できます。

環境変数の設定と利用

アプリケーションの設定値(データベースの接続情報、APIキーなど)をDockerfileに直接書き込むのは避けるべきです。代わりにENV命令を使って環境変数を設定します。

# デフォルトの環境変数を設定
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE ${PORT}

CMD [ "node", "server.js" ]

ENVで設定した値は、コンテナ起動時にdocker runコマンドの-eオプションで上書きできます。これにより、イメージを再ビルドすることなく、環境ごとに設定を柔軟に変更できます。

Dockerfileのテストとデバッグ

Dockerfileを書いても、ビルドや実行がうまくいかないことがあります。デバッグの基本的な方法は、docker buildのログを確認することです。エラーメッセージをよく読み、どの命令で失敗したかを確認します。

また、うまく動かないコンテナの原因を調査したい場合は、docker execコマンドが役立ちます。

docker exec -it <コンテナID> /bin/sh

このコマンドを使うと、起動中のコンテナの中に入って、ファイルの状態を確認したり、コマンドを実行したりして、問題の原因を探れます。

Dockerfileの書き方を学ぶことは、単にアプリケーションをコンテナで動かすための手順を覚えるだけではありません。それは、アプリケーションの実行環境そのものをコードとして管理し、再現性、移植性、そして保守性を高めるための重要なスキルです。

今回紹介した基本的な命令から最適化のテクニック、そしてベストプラクティスまでを理解し実践することで、あなたの開発プロセスは大きく変わるでしょう。Dockerfileは、一度書いて終わりではなく、アプリケーションの成長と共に改善していくものです。

  • 再現性の確保: 誰がどこで実行しても同じ環境が手に入ります。
  • 効率的な開発: キャッシュの活用により、日々のビルド時間を短縮できます。
  • 軽量なデプロイ: マルチステージビルドでイメージサイズを削減し、デプロイや起動を高速化できます。
  • セキュリティの向上: 適切なユーザー設定で、より安全なアプリケーション実行環境を構築できます。

ぜひ、この記事を参考に、あなたのプロジェクトに最適なDockerfileの書き方を見つけてください。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Docker