【翻訳】いいDockerイメージを構築するには? ーDockerfileのベストプラクティス

Dockerレジストリは、今やあふれんばかりの状況です。これを書いている時点で、”node”と検索すれば、1000件弱の結果がヒットします。どうやって選べばいいのでしょうか?

いいDockerイメージを構成するもの

いい悪いは主観ではありますが、私がいいと考えるDockerイメージには、いくつかの基準があります。

  • 実用的:以下に例を挙げます。

    • 最初にコンテナにアップデートを適用しなくても、Android SDKのイメージがプロジェクトをコンパイルできる。
    • MySQLのコンテナが、データベースとユーザを使用してサーバをブートする方法を明示する。
  • 最小限:コンテナの利点は、アプリケーションをサンドボックスできること(セキュリティがない場合には、ホストファイルシステム上で混乱を避けられること)です。ホストシステムにnode.jsをインストールしたり、JDK(Java開発キット)でシステムを汚染したりすることもできますが、私はどちらかと言えばディスクスペースやパフォーマンスに余裕を持たせ、それらと他のファイルが混ざらないよう注意しています。とはいうものの、こうした注意は最小限に抑えたいのが本音です。そのため、Dockerイメージは無駄のない最小限の機能でこの目的を達成すべきです。この原則に従えば、イメージはより拡張性を帯び、かつ破損も少なくなります。

  • ホワイトボックス:Dockerイメージにおいては、これはDockerfileを意味します。イメージを作った経緯を評価できますし、必要に応じて手を加えることも可能です。

残念ながらDockerレジストリでは、”いい”イメージを見分けるのは容易ではありませんし、特定のイメージを判別するのにも手間がかかる状況です。docker pull <...>の問題であることが多いですが、なぜ10メガバイトのnodeのバイナリに10ファイルのシステムレイヤが必要で、最終的には700メガバイトの仮想環境になるのか理由が分からなくなってきます。

いいDockerイメージを構築する

何が”いい”Dockerイメージかという普遍的な尺度はなく、Dockerレジストリに新たなイメージを追加する障壁も非常に低いため、状況はxkcd #927と全く同じです。つまり、それぞれが好き勝手にやっています。そこで、言語に特化した”オフィシャル”のDocker開発環境の導入は、いいスタートになると思います。この中に、私が愛用している手法(下記参照)のいくつかが含まれているのはうれしいですね。しかし、”数千ものnodeのイメージ”という状況は、Dockerレジストリの検索と評価メカニズムが強化されるまでは、恐らく改善しないでしょう。

そういう訳で、ここでは私が考えるDockerfileのベストプラクティスをまとめてみることにします。私もエキスパートという訳ではないので(Dockerがまだ出てきたばかりのサービスであることを考えると、現時点ではまだ誰もエキスパートではないとは思いますが)、どんなご意見やフィードバックも大歓迎です。

  • Debianを使ってコンパクトなイメージを書きましょう

このブログを書いている時点では、Ubuntu(バージョン14.04)は195MB、debian:Wheezyだと85MBになります。このUbuntuでの100メガバイトちょっとの超過分には実際なんの意味もありません(私が見る限り)。極端なケースだと、busyboxを使えば更に2MBほど削減することができます。これは、静的にリンクされたバイナリと一緒に使う時のみ現実的な手法でしょうか。progrium/logspoutリンクはこちらは14MBにまで小さくまとめられています。busyboxを使ったDockerイメージの良い例です。

  • 特に理由がなければビルドツールはインストールしないように

ビルドツールは大きなスペースを必要としますし、ソースコードを使うのでたいてい時間がかかります。既に普及しているソフトウェアを利用するだけの場合、普通はソースからビルドする必要はないでしょう。むしろお勧めしません。従って例えば、最新のnode.jsをアップしてDebianホストで動かすためだけに、PythonやGCCまでインストールする必要もありません。Node.jsソースコードのバイナリtarballなら、Node.jsのダウンロードサイトから手に入ります。同様にRedisについてもパッケージマネージャを使ってインストールすることができます。

ビルドツールを使った方がいいのは、少なくとも次のような理由がある時だけです。

  • 特定のバージョンを必要とする場合(例えば、Debianリポジトリに格納されているRedisは古いバージョンかもしれません)。
  • 特定のオプションでコンパイルする必要がある場合。
  • バイナリにコンパイルするモジュールをnpm install(もしくは同様のパッケージ管理ソフトウェアをインストール)する必要がある場合。

このうち2番目のケースにあたる場合には、本当に実行するべきかしっかり検討してください。3番目の理由によってビルドツールをインストールする場合には、最小のnode.jsイメージを素材にした別の”npm installer”イメージとしてインストールすることをお勧めします。

  • テンポラリファイルはそのまま放っておかないように

このDokerfileは、109MBものイメージサイズになっています。

FROM debian:wheezy
RUN apt-get update && apt-get install -y wget
RUN wget http://cachefly.cachefly.net/10mb.test
RUN rm 10mb.test

一方、同じように見えるこのDokerfileを確認すると、イメージサイズは99MBです。

FROM debian:wheezy
RUN apt-get update && apt-get install -y wget
RUN wget http://cachefly.cachefly.net/10mb.test && rm 10mb.test

要するに、Dokerfile上の各ステップでディスクにファイルを残したままにした場合、その分のスペースは後でファイルを消しても戻ってこないのです。コマンドのアウトプットをパイプで繋ぐだけで、テンポラリファイルを全く作らずにコマンド実行することも可能です。例えば、

wget -O - http://nodejs.org/dist/v0.10.32/node-v0.10.32-linux-x64.tar.gz | tar zxf -

を実行した場合、ファイルシステム上にファイルを保存することなく、直接tarballを抽出することができます。

  • パッケージマネージャの後にクリーンアップする

コンテナのセットアップ時にapt-get updateを実行すると、イメージが完成した時点で不要になるデータが/var/lib/apt/lists/に移動します。このディレクトリを安全に空にして、数メガバイト節約することができます。

次のDockerfileは99MBのイメージを生成します。

FROM debian:wheezy
RUN apt-get update && apt-get install -y wget

一方、こちらは90MBのイメージを生成します。

FROM debian:wheezy
RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*
  • パッケージのバージョンをピンする

Dockerイメージは不変ですが(その点は非常に優れていますが)、Dockerfileが実行時に毎回同じ出力結果が得られる保証はありません。問題はもちろん外部の状況なので、私たちにはほとんどどうすることもできません。ベストなのは、Dockerfileに関する外部の状況の影響を可能な限り最小限に抑えることです。そのためのシンプルな方法は、パッケージマネージャを通じてアップデートするときに、パッケージのバージョンをピンすることです。以下にその方法の例を示します。

# apt-get update
# apt-cache showpkg redis-server
Package: redis-server
Versions:
2:2.4.14-1
...

# apt-get install redis-server=2:2.4.14-1

パッケージのリポジトリが来年の今頃もこのバージョンを提供していればいいのですが、そういう保証はありません。いずれにせよ、あなたのイメージが依存しているソフトウェアのバージョンを明確に示すことは間違いなく有用です。

  • コマンドを連結する

もし関連性のあるコマンドが続く場合は、1つのRUNコマンドで連結するのがベストです。これはより重要なビルドキャッシュに効果的で(論理的にグループ化されたステップが1つのキャッシュステップにまとめられます)、ファイルシステムのレイヤ数を抑えます(一般的に望ましいことだと考えられますが、客観的に優れているかどうかは分かりません)。

以下のバックスラッシュ(\)は、読みやすいように使用しています。

RUN apt-get update && \
    apt-get install -y \
        wget=1.13.4-3+deb7u1 \
        ca-certificates=20130119 \
        ...
  • 同じ記述を避けるために環境変数を使う

これは”オフィシャル”のnode.js DockerイメージのDockerfileリンクはこちら)を読んでいて気付いたコツです。余談ですが、このDockerfileは優れています。1つだけ難点を言うと、巨大なbuildpack-depsリンクはこちら)イメージの上に私には不要なありとあらゆるものと一緒にあることです。


ENV NODE_VERSION 0.10.32

RUN curl -SLO “http://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz” \
&& tar -xzf “node-v$NODE_VERSION-linux-x64.tar.gz” -C /usr/local –strip-components=1 \
&& rm “node-v$NODE_VERSION-linux-x64.tar.gz”
“`


この記事については、こちらのHacker Newsの投稿でさらに検討しています。