5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Dockerfileを書く際のベストプラクティスを学ぶ

Posted at

はじめに

アプリケーションを作成していく上で、dockerを用いることは多いと思います。
今回はそんなdockerを使用していく上でもDockerfileがどう書かれているべきかを勉強するためにDocker DocumentationBest practice for writing Dockerfilesを自分なりに和訳しつつ記載してみました。
よければ参考にしてもらえたらと思います。
(Dockerfile instructions以下は時間のある時に追記します、、)

Dockerfileを作成するためのベストプラクティス

Dockerは特定のイメージをビルドするために必要な全てのコマンドを順番に記述されたテキストファイル(Dockerfile)からの指示を読み取ることによりイメージを自動的にビルドします。
Dockerイメージは、それぞれがDockerfile命令を表す読み取りレイヤーで構成されます。レイヤーは積み重ねられ、各レイヤーは前のレイヤーからの差分となリます。

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

各命令は1つのレイヤーを作成します。

  • FROM : ubuntu:18.04のDockerイメージからレイヤーを作成します
  • COPY : Dockerクライアントの現在のディレクトリからファイルを追加します
    (COPY .(クライアントのディレクトリ) /app(Dockerイメージ内のディレクトリ))
  • RUN : makeコマンドでアプリケーションをビルドします
  • CMD : コンテナ内で実行するコマンを指定します

イメージを実行してコンテナを生成するときは、下にあるレイヤーの上に新しい書き込み可能なレイヤー(コンテナレイヤー)を追加します。新しいファイルの書き込み、既存のファイルの変更、ファイルの削除など、実行中のコンテナに加えられた全ての変更はこの書き込み可能なコンテナ層に書き込まれます。

イメージレイヤー(およびDockerがイメージを構築、保存する方法)についてはストレージドライバーについてを参照してください。

一般的なガイドラインと推奨事項

一時的なコンテナ作成

Dockerfileによって定義されたイメージは、可能な限り一時的なコンテナ(ephemeral)として生成される必要がある。ephemeral(エフェメラル)とはコンテナを停止して破棄し、再構築して、最小限のセットアップと構成に置き換えることができることを意味しています。
The Twelve-factor Appの方法論を参照することで、このようなステートレスなコンテナを動かすことの動機付けを感じることができます。

build context(ビルドコンテキスト)を理解する

docker buildコマンドを発行すると、コマンドを実行した作業ディレクトリはビルドコンテキストと呼ばれます。デフォルトの設定ではDockerfileはこのビルドコンテキスト内にあると認識するようになってますが、-fのフラグを用いることで別の場所を指定することもできます。
例:

docker build -f ./Dockerfile.test

実際にDockerfileがどこに存在するかに関わらず、現在ディレクトリ内のファイルとディレクトリに全ての再起的な内容は、ビルドコンテキストとしてDockerデーモンに送信されます。

コンテキストの例を作成する

ビルドコンテキスト用のディレクトリを作成し、その中にcdコマンドで入ります。helloと名付けたテキストファイル内にhelloと書き込み、そのファイルをcatした内容でDockerfileを作成します。そしてビルドコンテキスト内(.)からイメージのビルドを行います。

 $ mkdir myproject && cd myproject
 $ echo "hello" > hello
 $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
 $ docker build -t helloapp:v1 .

Dockerfilehelloのそれぞれのファイルをdockerfilesとcontextの2つのディレクトリに移動し、2つ目のバージョンのイメージをビルドします(最後のビルドのキャッシュには依存しません)。-fのオプションを使用してDockerfileの場所を指定し、ビルドコンテキストの場所を指定します。

$ mkdir -p dockerfiles context
$ mv Dockerfile dockerfiles && mv hello context
$ docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

上記の例のようにビルドに必要のないファイルを誤って含めてしまうと、ビルドコンテキストが大きくなってしまい、イメージサイズが大きくなります。これにより、イメージをビルドする時間、イメージをプルおよびプッシュする時間、コンテナのランタイムサイズが増加する可能性があります。ビルドコンテキストの大きさを確認するにはビルド時に以下のようなメッセージを探してください。

Sending build context to Docker daemon  187.8MB

標準入力とパイプ(|)を用いてDockerfileを構築する。

DockerではstdinをパイプすることでDockerfileを構築することができます。
例:

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -

これを用いることでDockerfile自体を作成する(ディスクに書き込む)ことなくイメージを作成することができ、さらに一回きりのイメージ作成を実現できます。ただしこれは永続化するべきではありません。

このセクションの例では、便宜上このドキュメントを使用していますが、stdin上でDockerfileを提供するあらゆる方法を可能にします。

例えば、以下の2つのコマンドは同等の意味になります。

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF

上記の例は、好みのユースケースまたはアプローチに置き換えることができます。

ビルドコンテキストを送信せずに、stdinからDockerfileを使用してイメージを構築する

以下の構文を使用することでstdinからDockerfileを使用したイメージのビルドが実現できるため、不必要なファイルをビルドコンテキストに送信することがありません。
本来ビルドコンテキストを指定するパスが書かれる場所にハイフン(-)を使用すると、ディレクトリの代わりにstdinからビルドコンテキスト(含まれるのはDockerファイルのみ)を読み取るようにDockerに指示します。

docker build [OPTIONS] -

次の例ではstdinからDockerfileを使用してイメージをビルドします。ビルドコンテキストからDockerデーモンにファイルは送信されません。

docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF

ビルドコンテキストを省略することでDockerfileをイメージにコピーする必要がない場合に役立ち、Dockerデーモンにファイルが送信されないためビルド速度が向上します。
ビルドコンテキストから一部のファイルを除外してビルド速度を向上させたい場合は、.dockerignoroで除外を確認してください。

COPYADDを使用するDockerfileでビルドを試みる場合、この構文を使用すると失敗します。以下の例でそれを示します。

# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

docker build -t myimage:latest -<<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF

# observe that the build fails
...
Step 2/3 : COPY somefile.txt ./
COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory

stdinでDockerfileを使用して、ローカルのビルドコンテキストから構築する

以下の構文を使用して、ローカルファイルシステム上のファイルを使ったイメージのビルドを行います、ただしstdinからDockerfileを用いたものです。この構文はDockerfileを特定するために-fもしくは--fileオプションを使用し、-(ハイフン)を使用してstdinからDockerfileを読み込ませるようにDockerに指示しています。

docker build [OPTIONS] -f- PATH

以下の例では、現在ディレクトリ(.)をビルドコンテキストとして使用し、イメージをビルドするDockerfileこのドキュメントを用いたstdinを使っています。

# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF

stdinでDockerfileを使用して、リモートののビルドコンテキストから構築する

以下の構文を使用することでstdinDockerfileを用いて、リモートのgitリポジトリからのファイルを使用したイメージのビルドができます。この構文はDockerfileを特定するために-fもしくは--fileオプションを使用し、-(ハイフン)を使用してstdinからDockerfileを読み込ませるようにDockerに指示しています。

docker build [OPTIONS] -f- PATH

上記の構文はDockerfileを含まないリポジトリからイメージをビルドする、または独自のフォークしたリポジトリのメンテナンスをしないでカスタムなDockerfileでビルドするという状況で役に立ちます。
以下の例ではstdinからDockerfileを用いてビルドを行い、"hello-world" Git repository on GitHubからhello.cファイルを追加します。

docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c ./
EOF

memo

リモートのGitリポジトリをビルドコンテキストとして使用してイメージをビルドする場合、Dockerはローカルマシン上でgit cloneを実行し、その結果ビルドコンテキストとしてファイルをデーモンに送信します。この機能はdocker buildコマンドを実行するホスト上にgitがインストールされている必要があります。

.dockerignoreで除外する

ビルドに関係ないファイルを(ソースリポジトリを構築せずに)除外するには、.dockerignoreファイルを使用します。このファイルは.gitignoreファイルと同様の除外パターンをサポートしています。作成時の詳細についてはこちらを確認してください。

マルチステージビルドを使用する

マルチステージビルドを使用すると、中間レイヤーとファイル数を減らすことに力を入れなくても最終的なイメージサイズを大幅に減らことができます。
イメージはビルドプロセスの最終段階でビルドされるため、ビルドキャッシュを利用してイメージレイヤーを最小限に抑えることができます。
例えば、ビルドに複数のレイヤーが含まれている場合、変更頻度の低いものから(ビルドキャッシュが再利用されることを確実にするために)頻繁に変更されているものに並び替えることができます。
例えば以下のような順番です。

  • アプリケーションの構築に必要なツールのインストール
  • ライブラリの依存関係のインストールまたは更新
  • アプリケーションの生成

GoアプリケーションのDockerfileは以下のようになります。

# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build

# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only

# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/project/
RUN go build -o /bin/project

# This results in a single layer image
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

不要なパッケージをインストールしない

複雑さ、依存関係、ファイルサイズ、およびビルドの時間を削減するためには、「あったほうが良い」という理由だけで余分であったり不要なパッケージをインストールしないようにします。例えば、データベースのイメージにテキストエディタを含める必要はありません。

アプリケーションを切り離す

各コンテナでは一つの意図のみをもたせるべきです。アプリケーションを複数のコンテナに分離すると、水平スケールやコンテナの再利用をしやすくなります。例えば、Webアプリケーションスタックは、Webアプリケーション、データベース、インメモリキャッシュのそれぞれが独自のイメージを持った3つのコンテナで構成されるべきでしょう。

各コンテナでプロセスを一つに制限することは良い経験則ですが、それは厳格なルールではありません。例えば、コンテナはinitプロセスを生成できるだけではなく、一部のプログラムは独自の方法で追加のプロセスを生成する場合があります。例えば、Celeryは複数のワーカープロセスを生成でき、Apacheはリクエストごとに1つのプロセスを作成できます。

コンテナをできるだけ綺麗に、基本単位に保つためにベストな判断をしてください。もしコンテナがそれぞれに依存する場合は、Dockerコンテナネットワークを使用して、これらのコンテナが通信できるようにすることができます。

レイヤーの数を最小限に抑える

古いバージョンのDockerでは、イメージの中のレイヤーの数を最小化することが性能を出すために大切なことでした。この制限を減らすために以下の機能が追加されました。

  • RUN,COPY,ADDの命令のみがレイヤーを作成します。他の命令は一時的な中間イメージを作成し、ビルドサイズを増加させることはありません。
  • 可能な場所では、マルチステージビルドを使用し、最終的なイメージ内で必要なもののみをコピーします。これによって、最終的なイメージサイズを増加させることなく、中間ビルドステージにツールやデバック情報を入れることができます。

複数行の引数を並び替える

可能な時には常に複数行の引数をアルファベット順で並び替えることで後の変更を容易にします。これはパッケージの被りを避けたり、リストをより簡単に更新することの助けになります。これはプルリクエストが読みやすくなったりレビューしやすくなることの大きな助けにもなります。バックスラッシュ(\)の前にスペースを追加することも役立ちます。
これがbuildpack-depsのイメージ例です。

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/*

ビルドキャッシュを活用する

イメージをビルドする時、DockerはDockerfileの命令を上から順に処理し、指定された順番でそれぞれの命令を実行します。Dockerは新しい(重複した)イメージを作成するよりも、キャッシュ内で再利用できる既存のイメージを探します。

仮にキャッシュを全く使用したくない場合は、docker buildコマンドで--no-chace=trueオプションを使用することができます。しかし、Dockerにキャッシュを使用させる場合には、イメージが見つかった時にキャッシュできるかできないかを理解することが大切です。基本的なルールとしてDockerは以下の内容に従います。

  • 既にキャッシュに存在する親イメージから始めて、次の命令とそのベースイメージから派生した全ての子イメージと比較され、全く同じ命令を使って構築しているものがないかどうかチェックします。もし一致しているものがなければ、キャッシュは無効化されます。
  • ほとんどの場合、Dockerfileの命令を一つの子イメージと比較するだけで十分です。ただし、特定の指示にはさらなる調査と説明が必要です。
  • ADDCOPYの命令では、イメージ内のファイルの中身を検査し、各ファイルのチェックサムが計算されます。これらのチェックサムではファイルの最終変更時間と最終アクセス時間は考慮されません。キャッシュが探している間、そのチェックサムは既存のイメージのチェックサムと比較されます。内容やメタデータなど、もしファイルに何かしらの変更があれば、キャッシュは無効化されます。
  • ADDCOPYの命令を除いて、キャッシュが行うチェックはキャッシュが一致するかどうか判断するためにコンテナ内のファイルまで見ることはありません。例えば、RUN apt-get -y updateコマンドが処理される時、コンテナ内でアップデートされたファイルについてはキャッシュが存在するかどうか判断するために調べられません。その場合、コマンド文字列自体はコマンドが一致するものを見つけるためにのみ使用されます。

キャッシュが無効になると、全ての後続のDockerfileコマンドはキャッシュを使用することなく新しいイメージを生成します。

宣伝

パーソルプロセス&テクノロジー株式会社(以下パーソルP&T)、システムソリューション(SSOL)事業部所属の戸田です。

私はモビリティソリューションデザインチームに所属しており、モビリティ(ここでは移動手段全般)に関するサービスを考えたり、アプリを構築したりしております。

いわゆる「MaaS」に取り組んでおります。

私たちが「MaaS」に取り組む中で、現在活用している、もしくは活用する予定の技術やサービスやとりあえず発信したいことなどなど、幅広くチームメンバーと共に執筆していきたいと思います。
メンバーごとに違った内容を発信していきますので、お楽しみに!

また、「MaaS」について詳しく知りたい方は、チームメンバーの吉田が記事を掲載しておりますので、
ぜひそちらをご覧ください。

「MaaSとは」でたどり着いて欲しい記事 (1/3 前編)
「MaaSとは」でたどり着いて欲しい記事 (2/3 中編)
「MaaSとは」でたどり着いて欲しい記事 (3/3 後編)

最後まで読んでいただき、ありがとうございました!

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?