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