Dockerfile 記述のベストプラクティス
読む時間の目安: 12 分
このドキュメントは、効果的なイメージを構築する方法、お勧めのベストプラクティスについて示します。
Docker はDockerfileに書かれた指示を読み込んで、自動的にイメージを構築します。
このファイルはあらゆる命令を含んだテキストファイルであり、順に処理することで指定されたイメージを構築するために必要となるものです。
Dockerfileは所定のフォーマットや各種の命令に従います。
その内容は Dockerfile リファレンス に示しています。
Docker イメージは読み取り専用のレイヤーにより構成されます。
個々のレイヤーは Dockerfile の各命令を表現しています。
レイヤーは順に積み上げられ、それぞれは直前のレイヤーからの差分を表わします。
以下のようなDockerfileを見てみます。
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
各コマンドからは 1 つずつレイヤーが生成されます。
FROMは Docker イメージubuntu:18.04からレイヤーを 1 つ生成します。COPYは Docker クライアントのカレントディレクトリからファイルをコピーします。RUNはmakeを使ってアプリケーションをビルドします。CMDはコンテナー内にて実行するコマンドを指定します。
イメージを実行してコンテナーが生成されると、それまであったレイヤーの上に 書き込み可能なレイヤー(「コンテナーレイヤー」)が加えられます。 実行されているコンテナーへの変更、つまり新規ファイル生成や既存ファイル編集、ファイル削除などはすべて、この書き込みレイヤーに書き込まれます。
イメージレイヤー(また Docker がイメージをどう作り保存するか)については ストレージドライバーについて を参照してください。
一般的なガイドラインとアドバイス
エフェメラルなコンテナーの生成
Dockerfileによって定義されるイメージからコンテナーが作り出されます。
このコンテナーはできるだけエフェメラルな(ephemeral; はかない)ものとして生成されます。
「エフェメラル」という語を使うのは、コンテナーが停止、破棄されて、すぐに新たなものが作り出されるからです。
最小限の構成や設定を行うだけで、新たなものに置き換えられます。
The Twelve-factor App 手法にあるプロセスを見てみると、コンテナーの実行の仕方をステートレス(stateless)にしている理由がつかめると思います。
ビルドコンテキストの理解
docker buildコマンドを実行したときの、カレントなワーキングディレクトリのことを ビルドコンテキスト(build context)と呼びます。
デフォルトで Dockerfile は、カレントなワーキングディレクトリにあるものとみなされます。
ただしファイルフラグ(-f)を使って別のディレクトリとすることもできます。
Dockerfileが実際にどこにあったとしても、カレントディレクトリ配下にあるファイルやディレクトリの内容がすべて、ビルドコンテキストとして Docker デーモンに送られることになります。
ビルドコンテキストの例
ビルドコンテキストとするディレクトリを生成してそこに
cdで移動します。 テキストファイルhelloに “hello” と書き込み、Dockerfile 上でそのファイルに対してcatコマンドを与えるようにします。 ビルドコンテキスト(.)の中からイメージをビルドします。$ mkdir myproject && cd myproject $ echo "hello" > hello $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile $ docker build -t helloapp:v1 .
Dockerfileとhelloをそれぞれ別のディレクトリに移動させて、(上でビルドした際のキャッシュは用いずに)2 つめのイメージをビルドします。 Dockerfile に対して-fを使い、ビルドコンテキストとなるディレクトリを指定します。$ mkdir -p dockerfiles context $ mv Dockerfile dockerfiles && mv hello context $ docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
イメージのビルドには必要のないファイルを誤って含めてしまうと、ビルドコンテキストがそれだけ大きくなり、結果としてサイズの大きなイメージが生成されることになります。
こうしてしまうと、イメージビルドの時間、このイメージをプッシュしたりプルしたりする時間が、その分だけ要することとなり、コンテナーの実行時の容量も増えてしまいます。
ビルドコンテキストのサイズがどれだけになったかは、Dockerfileを使ってビルド処理を行った際の以下のようなメッセージを確認すればわかります。
Sending build context to Docker daemon 187.8MB
stdinを通じた Dockerfile のパイプ
Docker には、Dockerfileをstdinからパイプ入力してイメージをビルドできる機能があります。
その際には ローカルの、あるいはリモートのビルドコンテキスト を利用することができます。
stdinからのDockerfileのパイプ入力機能は、Dockerfile をディスクに書き込む必要のない、1 度きりのイメージビルドを行う場合に利用できます。
あるいは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 を指定してイメージをビルドしますが、その際にビルドコンテキストとして余計なファイルは送信しないようになります。
ハイフン(-)をPATHの指定場所に記述します。
こうすると Docker は、ディレクトリからではなくstdinからビルドコンテキストを読み込むことになります。
(そこにはDockerfileしかありません。)
docker build [OPTIONS] -
以下の例はstdinから受け渡されたDockerfileを使ってイメージをビルドします。
ビルドコンテキストからデーモンに送信されるファイルは 1 つもありません。
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
ビルドコンテキストを省略するのは、イメージにコピーするファイルが何もないようなDockerfileを用いる際に利用できます。
その場合はデーモンへのファイル送信がない分だけ、ビルド時間が短縮されます。
ビルドコンテキストからファイルを除外することによって、イメージビルドの時間を短縮しようとする場合は、.dockerignore によるファイル除外の指定 を参照してください。
メモ: この構文を用いる際に、Dockerfile に
COPYやADDを含めるとビルドに失敗します。 以下にその例を説明します。# 作業ディレクトリを生成します。 mkdir example cd example # サンプルのファイルを生成します。 touch somefile.txt docker build -t myimage:latest -<<EOF FROM busybox COPY somefile.txt ./ RUN cat /somefile.txt EOF # ビルドが失敗する様子を確認します。 ... Step 2/3 : COPY somefile.txt ./ COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory
ローカルのビルドコンテキストを使ったビルド、stdioからの Dockerfile 利用
以下の構文により、ローカルファイルシステム上のファイルを使ってイメージをビルドします。
ただしDockerfileはstdinからの入力とします。
この構文では -f(または --file)オプションによってDockerfileを指定します。
そしてファイル名にはハイフン(-)を指定することで、Dockerfileをstdinから読み込むことを指示しています。
docker build [OPTIONS] -f- PATH
以下の例ではビルドコンテキストとしてカレントディレクトリ(.)を利用します。
そしてstdinから受け渡されるDockerfileを使ってイメージをビルドします。
この際には ヒアドキュメント を使っています。
# 作業ディレクトリを生成します。
mkdir example
cd example
# サンプルのファイルを生成します。
touch somefile.txt
# カレントディレクトリをコンテキストとして stdin から Dockerfile をビルドします。
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF
リモートのビルドコンテキストを使ったビルド、stdioからの Dockerfile 利用
以下の構文により、リモートの git リポジトリ上のファイルを使ってイメージをビルドします。
ここでもDockerfileはstdinからの入力とします。
この構文では-f(または--file)オプションによってDockerfileを指定します。
そしてファイル名にはハイフン(-)を指定することで、Dockerfileをstdinから読み込むことを指示しています。
docker build [OPTIONS] -f- PATH
この構文は、イメージをビルドするために利用するリポジトリにDockerfileが含まれていないような場合に用いることができます。
あるいはDockerfileには独自のものを使ってビルドを行いたい場合です。
つまりフォークしたリポジトリを変更しなくて済みます。
以下の例はstdinからDockerfileを指定してイメージをビルドします。
そして GitHub 上の git リポジトリ “hello-world” からhello.cファイルを取得して加えます。
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c ./
EOF
内部の処理
ビルドコンテキストとしてリモートの git リポジトリを使ってイメージをビルドする場合、Docker はローカルマシン上において、そのリポジトリに対して
git cloneを実行します。 そして取得されるファイルをビルドコンテキストとしてデーモンに送ります。 つまりこの機能を実行するには、docker buildコマンドを実行するホスト上にgitをインストールしていることが必要です。
.dockerignore を使ったファイル除外の指定
ビルドに関係のないファイルを(ソースリポジトリを変更することなく)除外するには.dockerignoreファイルを利用します。
このファイルは.gitignoreファイルと同様のファイル除外指定パターンに対応しています。
ファイルの生成に関しては .dockerignore ファイル を参照してください。
マルチステージビルドの利用
マルチステージビルド は、最終的なイメージサイズを激減させることができます。 中間的に生成されるレイヤーやファイルの数を減らすような苦労は必要ありません。
イメージというものは、ビルド処理の最終段階で生成されるものです。 したがってイメージのレイヤーは ビルドキャッシュの活用 によって最小限に抑えることができます。
たとえばビルドするイメージにレイヤーがいくつかある場合、変更があまり行われないもの(ビルドキャッシュが確実に再利用されるもの)から、頻繁に変更されるものへと並び順を定めることができます。
-
アプリケーションのビルドに必要なツールのインストール
-
依存するライブラリのインストールまたはアップデート
-
アプリケーションのビルド
たとえば Go 言語アプリケーションの Dockerfile は以下のようになります。
# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build
# 本プロジェクトに必要なツールをインストール。
# `docker build --no-cache .`を実行して依存パッケージのアップデート。
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# プロジェクトの依存関係を Gopkg.toml と Gopkg.lock に列記。
# これに対応するレイヤーは Gopkg ファイルの更新時のみ再ビルド。
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# 依存ライブラリをインストール。
RUN dep ensure -vendor-only
# プロジェクト全体をコピーしてビルド。
# このレイヤーはプロジェクトディレクトリ内のファイル変更時に再ビルド。
COPY . /go/src/project/
RUN go build -o /bin/project
# 以下によりイメージを 1 レイヤーに。
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
不要なパッケージをインストールしない
Dockerfile をわかりやすくして、依存関係、ファイルサイズ、構築時間をいずれも減らすためには、余分で必須ではない「あった方が良いだろう」程度のパッケージはインストールしないようにします。 例えば、データベースイメージであればテキストエディターは不要のはずです。
アプリケーションの分割
1 つのコンテナーが取り扱う内容は 1 つにしぼるべきです。 アプリケーションを複数のコンテナーに分けると、スケールアウトやコンテナーの再利用がしやすくなります。 たとえばウェブアプリケーションが 3 つの独立したコンテナーにより成り立っているとします。 それらは個々のイメージを持ち、それぞれに分かれてウェブアプリケーション、データベース、メモリキャッシュを管理するようになります。
個々のコンテナーを 1 つのプロセスのみに限定して割り当てることは、優れた経験則となることがあります。 しかし決して厳密な規則というわけでもありません。 たとえばコンテナーは 初期プロセスにおいて起動 することが可能であり、プログラムの中には必要に応じて追加のプロセスを起動するようなものもあります。 例をあげると、Celery はワーカープロセスを複数起動し、Apache はリクエストごとにプロセスを生成します。
コンテナーはできるかぎりすっきりとモジュール分割されるように、適切に判断してください。 コンテナーが互いに依存している場合は、Docker container ネットワーク を用いることで、コンテナー間の通信を確実に行うことができます。
レイヤー数は最小に
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-cache=trueオプションをつけて実行します。
一方で Docker のキャッシュを利用する場合、Docker が適切なイメージを見つけた上で、どのようなときにキャッシュを利用し、どのようなときには利用しないのかを理解しておくことが必要です。
Docker が従っている規則は以下のとおりです。
-
キャッシュ内にすでに存在している親イメージから処理を始めます。 そのベースとなるイメージから派生した子イメージに対して、次の命令が合致するかどうかが比較され、子イメージのいずれかが同一の命令によって構築されているかを確認します。 そのようなものが存在しなければ、キャッシュは無効になります。
-
ほとんどの場合
Dockerfile内の命令と子イメージのどれかを単純に比較するだけで十分です。 しかし命令によっては、多少の検査や解釈が必要となるものもあります。 -
ADD命令やCOPY命令では、イメージに含まれるファイルの内容が検査され、個々のファイルについてチェックサムが計算されます。 この計算において、ファイルの最終更新時刻、最終アクセス時刻は考慮されません。 キャッシュを探す際に、このチェックサムと既存イメージのチェックサムが比較されます。 ファイル内の何かが変更になったとき、たとえばファイル内容やメタデータが変わっていれば、キャッシュは無効になります。 -
ADDとCOPY以外のコマンドの場合、キャッシュのチェックは、コンテナー内のファイル内容を見ることはなく、それによってキャッシュと合致しているかどうかが決定されるわけでありません。 たとえばRUN apt-get -y updateコマンドの処理が行われる際には、コンテナー内にて更新されたファイルは、キャッシュが合致するかどうかの判断のために用いられません。 この場合にはコマンド文字列そのものが、キャッシュの合致判断に用いられます。
キャッシュが無効化されると、以降のDockerfile命令ではキャッシュは使われず、新しいイメージを生成します。
Dockerfile コマンド
効率のよい保守性に優れたDockerfileを生成するために、推奨する内容を以下に示します。
FROM
イメージのベースは、できるだけ現時点での公式リポジトリを利用してください。 Alpine イメージ がお勧めです。 このイメージはしっかりと管理されていて、充実した Linux ディストリビューションであるにもかかわらず、非常にコンパクトなものになっています(現在 6 MB 以下)。
LABEL
イメージにラベルを追加するのは、プロジェクト内でのイメージ管理をしやすくしたり、ライセンス情報の記録や自動化の助けとするなど、さまざまな目的があります。
ラベルを指定するには、LABELで始まる行を追加して、そこにキーバリューペアをいくつか設定します。
以下に示す例は、いずれも正しい構文です。
説明をコメントとしてつけています。
文字列に空白が含まれる場合は、引用符でくくるか、あるいはエスケープする必要があります。 文字列内に引用符がある場合も、同様にエスケープしてください。
# 個々にラベルを設定
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""
イメージには複数のラベルを設定することができます。
Docker 1.10 以前のバージョンでは、ラベルをすべてまとめて 1 つのLABEL命令にすることが推奨されていました。
これによって余分なレイヤーが生成されることを防ぐためです。
このことは、現在は必要ではなくなっています。
ただしラベルをまとめる機能は今もサポートされています。
# 1 行でラベルを設定
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
上は以下のように書くこともできます。
# 複数のラベルを一度に設定、ただし行継続の文字を使い、長い文字列を改行する
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
利用可能なラベルのキーおよび値に関するガイドラインが オブジェクトラベルの理解 に示されています。 ラベルを検索する方法については、オブジェクト内のラベル管理 に示されているフィルタリングに関する項目を参照してください。 また Dockerfile リファレンスの LABEL も参考になります。
RUN
RUNコマンドが複数行にわたって長く複雑になるようであれば、バックスラッシュを使って行を分けてください。
Dockerfileを読みやすく理解しやすく、そして保守しやすくするためです。
apt-get
おそらくRUNにおいて一番利用する使い方がapt-getアプリケーションの実行です。
RUN apt-getはパッケージをインストールするものであるため、注意点がいくつかあります。
RUN apt-get updateとapt-get installは、同一のRUNコマンド内にて同時実行するようにしてください。
たとえば以下のようにします。
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo \
&& rm -rf /var/lib/apt/lists/*
1つのRUNコマンド内でapt-get updateだけを使うとキャッシュに問題が発生し、その後のapt-get installコマンドが失敗します。
たとえば Dockerfile を以下のように記述したとします。
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
イメージが構築されると、レイヤーがすべて Docker のキャッシュに入ります。
この次にapt-get installを編集して別のパッケージを追加したとします。
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl nginx
Docker は当初のコマンドと修正後のコマンドを見て、同一のコマンドであると判断するので、前回の処理において作られたキャッシュを再利用します。
キャッシュされたものを利用して処理が行われるわけですから、結果としてapt-get updateは実行されません。
apt-get updateが実行されないということは、つまりcurlにしてもnginxにしても、古いバージョンのまま利用する可能性が出てくるということです。
RUN apt-get update && apt-get install -yというコマンドにすると、 Dockerfile が確実に最新バージョンをインストールしてくれるものとなり、さらにコードを書いたり手作業を加えたりする必要がなくなります。
これは「キャッシュバスティング(cache busting)」と呼ばれる技術です。
この技術は、パッケージのバージョンを指定することによっても利用することができます。
これはバージョンピニング(version pinning)というものです。
以下に例を示します。
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.*
バージョンピニングでは、キャッシュにどのようなイメージがあろうとも、指定されたバージョンを使ってビルドが行われます。 この手法を用いれば、そのパッケージの最新版に、思いもよらない変更が加わっていたとしても、ビルド失敗を回避できることもあります。
以下のRUNコマンドはきれいに整えられていてapt-getの推奨する利用方法を示しています。
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*
s3cmdのコマンド行は、バージョン1.1.*を指定しています。
以前に作られたイメージが古いバージョンを使っていたとしても、新たなバージョンの指定によりapt-get updateのキャッシュバスティングが働いて、確実に新バージョンがインストールされるようになります。
パッケージを各行に分けて記述しているのは、パッケージを重複して書くようなミスを防ぐためです。
apt キャッシュをクリーンアップし/var/lib/apt/listsを削除するのは、イメージサイズを小さくするためです。
そもそも apt キャッシュはレイヤー内に保存されません。
RUNコマンドをapt-get updateから始めているので、apt-get installの前に必ずパッケージのキャッシュが更新されることになります。
公式の Debian と Ubuntu のイメージは自動的に
apt-get cleanを実行するので、明示的にこのコマンドを実行する必要はありません。
パイプの利用
RUNコマンドの中には、その出力をパイプを使って他のコマンドへ受け渡すことを前提としているものがあります。
そのときにはパイプを行う文字(|)を使います。
たとえば以下のような例があります。
RUN wget -O - https://some.site | wc -l > /number
Docker はこういったコマンドを/bin/sh -cというインタープリター実行により実現します。
正常処理されたかどうかは、パイプの最後の処理の終了コードにより評価されます。
上の例では、このビルド処理が成功して新たなイメージが生成されるかどうかは、wc -lコマンドの成功にかかっています。
つまりwgetコマンドが成功するかどうかは関係がありません。
パイプ内のどの段階でも、エラーが発生したらコマンド失敗としたい場合は、頭にset -o pipefail &&をつけて実行します。
こうしておくと、予期しないエラーが発生して、それに気づかずにビルドされてしまう、といったことはなくなります。
たとえば以下です。
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
すべてのシェルが
-o pipefailオプションをサポートしているわけではありません。その場合(例えば Debian ベースのイメージにおけるデフォルトシェル
dashである場合)、RUNコマンドにおける exec 形式の利用を考えてみてください。 これはpipefailオプションをサポートしているシェルを明示的に指示するものです。 たとえば以下です。RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
CMD
CMDコマンドは、イメージ内に含まれるソフトウェアを実行するために用いるもので、引数を指定して実行します。
CMDはほぼ、CMD ["実行モジュール名", "引数1", "引数2"…]の形式をとります。
Apache や Rails のようにサービスをともなうイメージに対しては、たとえばCMD ["apache2","-DFOREGROUND"]といったコマンド実行になります。
実際にサービスベースのイメージに対しては、この実行形式が推奨されます。
上記以外では、CMDに対して bash、python、perl などインタラクティブシェルを与えることが行われます。
たとえばCMD ["perl", "-de0"]、CMD ["python"]、CMD ["php", "-a"]といった具合です。
この実行形式を利用するということは、たとえばdocker run -it pythonというコマンドを実行したときに、指定したシェルの中に入り込んで、処理を進めていくことを意味します。
CMDと ENTRYPOINT を組み合わせて用いるCMD ["引数", "引数"]という実行形式がありますが、これを利用するのはまれです。
開発者自身や利用者にとってENTRYPOINTがどのように動作するのかが十分に分かっていないなら、用いないようにしましょう。
EXPOSE
Dockerfile リファレンスの EXPOSE コマンド
EXPOSEコマンドは、コンテナーが接続のためにリッスンするポートを指定します。
当然のことながらアプリケーションにおいては、標準的なポートを利用します。
たとえば Apache ウェブサーバーを含んでいるイメージに対してはEXPOSE 80を使います。
また MongoDB を含んでいればEXPOSE 27017を使うことになります。
外部からアクセスできるようにするため、これを実行するユーザーはdocker runにフラグをつけて実行します。
そのフラグとは、指定されているポートを、自分が取り決めるどのようなポートに割り当てるかを指示するものです。
Docker のリンク機能においては環境変数が利用できます。
受け側のコンテナーが提供元をたどることができるようにするものです(例: MYSQL_PORT_3306_TCP)。
ENV
新しいソフトウェアに対してはENVを用いれば簡単にそのソフトウェアを実行できます。
コンテナーがインストールするソフトウェアに必要な環境変数PATHを、このENVを使って更新します。
たとえばENV PATH=/usr/local/nginx/bin:$PATHを実行すれば、CMD ["nginx"]が確実に動作するようになります。
ENVコマンドは、必要となる環境変数を設定するときにも利用します。
たとえば Postgres のPGDATAのように、コンテナー化したいサービスに固有の環境変数が設定できます。
またENVは普段利用している各種バージョン番号を設定しておくときにも利用されます。
これによってバージョンを混同することなく、管理が容易になります。
たとえば以下がその例です。
ENV PG_MAJOR=9.3
ENV PG_VERSION=9.3.4
RUN curl -SL https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH
プログラムにおける(ハードコーディングではない)定数定義と同じことで、この方法をとっておくのが便利です。
ただ 1 つのENVコマンドを変更するだけで、コンテナー内のソフトウェアバージョンは、いとも簡単に変えてしまうことができるからです。
それぞれのENV行からは新たな中間レイヤーが生成されます。
RUNコマンドと同じです。
ということはつまり、環境変数を先々のレイヤーにおいて無効化したとしても、その中間レイヤーに変数データは残ることになり、この値を引き出すことができてしまいます。
このことを確認するには、以下のような Dockerfile を生成してビルドを行ってみればわかります。
# syntax=docker/dockerfile:1
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
$ docker run --rm test sh -c 'echo $ADMIN_USER'
mark
このようにはならないように、つまり本当に環境変数を無効化したい場合には、シェルコマンドを用いたRUNコマンドを利用します。
そして環境変数への値設定、利用、無効化を、すべて 1 つのレイヤー内にて行うようにします。
各コマンドの区切りには;や&&を使います。
この 2 つめの方法をとった場合、コマンドが 1 つでも失敗すればdocker buildも失敗します。
この方が適切なやり方です。
Linux における Dockerfile では行継続文字を表わす\を用いると、読みやすくなります。
あるいは実行する命令をすべてシェルスクリプトに書き入れて、RUNコマンドによってそのシェルスクリプトを実行するようなこともできます。
# syntax=docker/dockerfile:1
FROM alpine
RUN export ADMIN_USER="mark" \
&& echo $ADMIN_USER > ./mark \
&& unset ADMIN_USER
CMD sh
$ docker run --rm test sh -c 'echo $ADMIN_USER'
ADD と COPY
ADDとCOPYの機能は似ていますが、一般的にはCOPYが選ばれます。
それはADDよりも機能がはっきりしているからです。
COPYは単に、基本的なコピー機能を使ってローカルファイルをコンテナーにコピーするだけです。
一方ADDには特定の機能(ローカルでの tar 展開やリモート URL サポート)があり、これはすぐにわかるものではありません。
結局ADDの最も適切な利用場面は、ローカルの tar ファイルを自動的に展開してイメージに書き込むときです。
たとえばADD rootfs.tar.xz /といったコマンドになります。
Dockerfile内の複数ステップにおいて異なるファイルをコピーするときには、一度にすべてをコピーするのではなく、COPYを使って個別にコピーしてください。
こうしておくと、個々のステップに対するキャッシュのビルドは最低限に抑えることができます。
つまり指定されているファイルが変更になったときのみキャッシュが無効化されます(そのステップは再実行されます)。
例
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
RUNコマンドのステップより前にCOPY . /tmp/を実行していたとしたら、それに比べて上の例はキャッシュ無効化の可能性が低くなっています。
イメージサイズの問題があるので、ADDを用いてリモート URL からパッケージを取得することはやめてください。
かわりにcurlやwgetを使ってください。
こうしておくことで、ファイルを取得し展開した後や、イメージ内の他のレイヤーにファイルを加える必要がないのであれば、その後にファイルを削除することができます。
たとえば以下に示すのは、やってはいけない例です。
ADD https://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all
そのかわり、次のように記述します。
RUN mkdir -p /usr/src/things \
&& curl -SL https://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
ADDの自動展開機能を必要としないもの(ファイルやディレクトリ)に対しては、常にCOPYを使うようにしてください。
ENTRYPOINT
Dockerfile リファレンスの ENTRYPOINT コマンド
ENTRYPOINTの最適な利用方法は、イメージに対してメインのコマンドを設定することです。
これを設定すると、イメージをそのコマンドそのものであるかのようにして実行できます(その次にCMDを使ってデフォルトフラグを指定します)。
コマンドラインツールs3cmdのイメージ例から始めます。
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
このイメージが実行されると、コマンドのヘルプが表示されます。
$ docker run s3cmd
あるいは適正なパラメーターを指定してコマンドを実行します。
$ docker run s3cmd ls s3://mybucket
このコマンドのようにして、イメージ名がバイナリへの参照としても使えるので便利です。
ENTRYPOINTコマンドはヘルパースクリプトとの組み合わせにより利用することもできます。
そのスクリプトは、上記のコマンド例と同じように機能させられます。
たとえ対象ツールの起動に複数ステップを要するような場合でも、それが可能です。
たとえば Postgres 公式イメージ ではENTRYPOINTとして以下のスクリプトを利用しています。
#!/bin/bash
set -e
if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"
if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fi
exec gosu postgres "$@"
fi
exec "$@"
アプリを PID 1 として実行
このスクリプトでは Bash の
execコマンド を利用しています。 このため最後に実行されるアプリケーションが、コンテナーの PID 1 になります。 したがってコンテナーに送信される Unix シグナルは、そのアプリケーションが受け取ることになります。 詳しくはENTRYPOINTリファレンス を参照してください。
ヘルパースクリプトはコンテナーの中にコピーされ、コンテナー開始時にENTRYPOINTから実行されます。
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]
このスクリプトを用いると、Postgres との間で、ユーザーがいろいろな方法でやり取りできるようになります。
以下は単純に Postgres を起動します。
$ docker run postgres
あるいは PostgreSQL 実行時にサーバーに対してパラメーターを渡すことができます。
$ docker run postgres postgres --help
または Bash のような全く異なるツールを起動するために利用することもできます。
$ docker run --rm -it postgres bash
VOLUME
Dockerfile リファレンスの VOLUME コマンド
VOLUMEコマンドは、データベースストレージ領域、設定用ストレージ、Docker コンテナーによって作成されるファイルやフォルダの公開に使います。
イメージの可変的な部分、あるいはユーザーが設定可能な部分については VOLUME の利用が強く推奨されます。
USER
サービスが特権ユーザーでなくても実行できる場合は、USERを用いて非 root ユーザーに変更します。
ユーザーとグループを生成するところから始めてください。
Dockerfile内にてたとえばRUN groupadd -r postgres && useradd -r -g postgres postgresのようなコマンドを実行します。
明示的な UID、GID 指定
イメージ内のユーザとグループに割り当てられる UID、GID は確定的なものではありません。 イメージが再構築されるかどうかには関係なく、「次の」値が UID、GID に割り当てられます。 これが問題となる場合は、UID、GID を明示的に割り当ててください。
Go 言語の archive/tar パッケージが取り扱うスパースファイルにおいて 未解決のバグがあります。 これは Docker コンテナー内にて非常に大きな値の UID を使ってユーザーを生成しようとするため、ディスク消費が異常に発生します。 コンテナーレイヤー内の
/var/log/faillogが NUL (\0) キャラクターにより埋められてしまいます。 useradd に対して--no-log-initフラグをつけることで、とりあえずこの問題は回避できます。 ただし Debian/Ubuntu のadduserラッパーは--no-log-initフラグをサポートしていないため、利用することはできません。
sudoのインストールやその利用は避けてください。
これは予期できない TTY 動作やシグナルフォワーディングが起こってしまい問題となるためです。
たとえば デーモンはrootで初期化するものの、実行はroot以外で行うといったように、sudoと同等の機能が本当に必要なのであれば、“gosu” の利用を検討してみてください。
最後に、レイヤー数を減らしてわかりやすいものとなるように、USERを何度も切り替えるようなことは避けてください。
WORKDIR
Dockerfile リファレンスの WORKDIR コマンド
WORKDIRに設定するパスは、わかりやすく確実なものとするために、絶対パス指定としてください。
またRUN cd … && do-somethingといった長くなる一方のコマンドを書くくらいなら、WORKDIRを利用してください。
そのような書き方は読みにくいため、トラブル発生時には解決を遅らせ保守が困難になるためです。
ONBUILD
Dockerfile リファレンスの ONBUILD コマンド
ONBUILDコマンドは、Dockerfileによるビルドが完了した後に実行されます。
ONBUILDは、現在のイメージからFROMによって派生した子イメージにおいて実行されます。
つまりONBUILDとは、親のDockerfileから子どものDockerfileへ与える命令であると言えます。
Docker によるビルドにおいてはONBUILDの実行が済んでから、子イメージのコマンド実行が行われます。
ONBUILDは、所定のイメージからFROMを使ってイメージをビルドしようとするときに利用できます。
たとえば特定言語のスタックイメージはONBUILDを利用します。
Dockerfile内にて、その言語で書かれたどのようなユーザーソフトウェアであってもビルドすることができます。
その例として Ruby’s ONBUILD variants があります。
ONBUILDによって構築するイメージは、異なったタグを指定してください。
たとえばruby:1.9-onbuildとruby:2.0-onbuildなどです。
ONBUILDにおいてADDやCOPYを用いるときは注意してください。
“onbuild” イメージが新たにビルドされる際に、追加しようとしているリソースが見つからなかったとしたら、このイメージは復旧できない状態になります。上に示したように個別にタグをつけておけば、Dockerfileの開発者にとっても判断ができるようになるので、不測の事態は軽減されます。
Docker 公式イメージの例
以下に示すのは代表的なDockerfileの例です。
その他の情報
- Dockerfile リファレンス
- ベースイメージの詳細
- 自動ビルドの詳細
- Docker 公式イメージ作成のガイドライン
- Best practices to containerize Node.js web applications with Docker