TinyCoreLinux の Docker イメージを作った話

これは NSEG Advent Calendar 2015 -Adventar 15 日目の記事です。

前回 に引き続き Docker の話ですが、 Tiny Core LinuxDocker イメージとその仲間(たとえば tinycore-ruby)を作った話をまとめておこうと思います。

Tiny Core Linux とは?

The Core Project で作成されている Linux ディストリビューションの一つで、有名どころでは Docker Machine の OS としても使われていたりします。コア機能に絞り込むと 10MB 以下、 GUI 関連パッケージまで含めても 100MB 以下と、ディスク消費が非常に小さいながらデスクトップ環境まで構築でき、独自形式ながらパッケージ機構も持っているため、他のメジャーなディストリビューションと比べて使用が難しすぎるということもなく、なかなか便利なディストリビューションです。

イメージ作成動機

Docker をある程度使っていると感じることかと思いますが、公式で提供されている各種ディストリビューションの Docker イメージは容量が少なくとも 200MB 越えしているものがほとんどで、かつ、それを元にした各言語の公式実行環境はそれ以上の容量となることが一般的です。

一方で、特に Node.js などは比較的頻繁にリリースされ、その都度環境を更新することも多くなりますが、その度に大容量のイメージをダウンロード・展開することになります。

そこで、もっとディスク容量を抑えた環境はできないかなと思っていたところ、 Tiny Core Linux の存在を知り、特定の一言語の実行環境であればもっとコンパクトなものが作れるのではないかということで、イメージ作成を試してみることにしました。

結果、基本の OS イメージでは 7MB 弱、先に上げた tinycore-ruby イメージでも 60MB そこそこと、一通りの機能を組み込んだ上でもかなり容量を削ることに成功しています。

Tiny Core Linux 基本イメージ

各言語の環境を作る前に、まずは基本となる OS イメージが必要なので、それを x86, x86_64 それぞれで配布されている rootfs.gz ないし rootfs64.gz から構築しています。

この rootfs.gz は cpio 形式でアーカイブされていますが、 Docker に読み込ませるには tar 形式である必要があるため、一度これを展開してアーカイブし直す必要があります。

また、 Tiny Core Linux のパッケージは squashfs を利用して作成されており、パッケージインストール時にはこの squashfs をマウントしてファイルを取りだす仕組みになっているため、そのまま rootfs.gz を展開してイメージを作ってしまうと、パッケージ機構を利用する時にはコンテナに privileged 権限をつけて起動する必要が出てきます。これでは日常利用で不便ですので、それを回避するために、 unsquashfs を入れると同時にパッケージプログラムに少々手を加え、 privileged なしでもパッケージを扱えるようにしています

イメージ生成に Docker の Automated Build を使えるようにするため、イメージ作成に必要な処理は Github にて公開していますので、詳しくはそちらを参照してください。

tinycore-ruby イメージ

OS イメージができたので、それを元にして使いたい言語の環境を作るのですが、ここでは Ruby のものを例として見ていきます。

基本的には公式の Ruby イメージと同じような使い勝手になるよう、公開されている Dockerfile の手順と同様に記述していくことになりますが、元々の目的が容量削減でしたので、一度インストールしたパッケージも動作に必須でなければ極力アンインストールするなどしています。

また、公式でも同様ですが、 RUN の処理を、できるだけ

RUN gem install bundler --version "$BUNDLER_VERSION" \
    && bundle config --global path "$GEM_HOME" \
    && bundle config --global bin "$GEM_HOME/bin"

のように \ && でつなげて書いています。これは、同一のシェルコンテキストで実行とするという意味合いもありますが、それぞれを分けて、たとえば

RUN gem install bundler --version "$BUNDLER_VERSION"
RUN bundle config --global path "$GEM_HOME"
RUN bundle config --global bin "$GEM_HOME/bin"

のように書くと、それぞれの RUN の処理中で生成されたデータがそのままイメージ容量に加算されていき、後の処理で不要なデータを削除したとしてもイメージ全体のサイズが減らなくなってしまうため、それを回避する目的が主です。同一コンテキスト内で最終的に不要なデータを削除しておけば、その分はイメージ容量には加算されないので、容量肥大化を防ぐことができます。

ただし、このように書いてしまうと、一度イメージ作成処理に失敗すると、次に実行する時もキャッシュが効かず、同一コンテキストの最初から(つまり RUN の最初から)やり直しになってしまい、特に大きなパッケージファイルをダウンロードしている時など実行時間がかかって待たされることになるため、試行錯誤している時などは個別の RUN に分割しておくと、キャッシュの恩恵を受けて効率良く作業ができます。処理が固まったら改めて \ && でひとまとめにするといいでしょう。

こちらも Github にてイメージ生成過程は公開してますので、具体的な内容などはそちらを参照してください。

まとめ

以上のような流れで、 Ruby 以外の言語のイメージや、特定のプログラムだけを配置したイメージも比較的簡単に作ることができます。容量だけで見れば公式で配布されている busybox イメージを使う、という方法もありますが、それだけでは機能不足であるとか、容量は抑えつつ欲しい機能を手軽に追加したい、という向きには、このイメージは便利に使っていただけるかと思います。

余談ですが、現在公開中の Ruby, Python, Node.js 以外にも、 PHP, Go などの作成も試してみたのですが、 PHP は TinyCore のパッケージに含まれないライブラリへの依存関係が多く挫折、 Go は作成できたものの公式と容量が 100MB も違いがなく(主に go get などの内部で使われる git, mercurial, subversion を組み込んだ影響)、あえて作る意味もないということで公開はしないこととしました。これらの言語では素直に公式イメージを使うのが吉、ということのようです。