Work Records

日々の作業記録です。ソフトウェアエンジニアリング全般から、趣味の話まで。

RailsのDockerイメージを小さくしたい

Docker imageを小さくする

Dockerイメージは小さいほど良いです。これは自明です。
プログラムを動かすための必要最低限の実行ファイルがあれば良いのです。

RailsのDockerイメージ

そもそもバイナリだけで動くgoみたいな言語と違って、イメージが大きくなりがちなんですが
それでも多少は頑張りたいと思い試行錯誤してみる。

現状のDockerイメージ

Railsに関係する部分だけが削減対象かというとそうでもなく、
Dockerを導入し始めたばかりですのでイメージの最小化まで手がつけられていなかったのが現実で、基本的なところから色々と試して見ます。

今使っているDockerfileは恥ずかしながら、ansibleで実行しているコマンドをそのまま置き換えただけのものなので、インストーラなど実行ファイル以外のものがたくさん含まれています。

ubuntuのbase imageに

  • apt-getで大量のパッケージを入れ
  • rbenvをinstallし
  • rubyの特定バージョンをinstallして
  • bundle installする

という感じ。

一部省略していますが、Dockerfileは以下のようになっています。
/appというディレクトリの下に、sample(仮名)というソースを配置しています。

FROM ubuntu:trusty
RUN apt-get update && \
    apt-get -y install git curl make openssl zlib1g-dev libssl-dev libreadline-dev sysv-rc-conf build-essential mysql-client libmysqlclient-dev && \
    mkdir -p /app && \
    git clone https://github.com/sstephenson/rbenv.git ~/.rbenv && \
    git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build && \
    echo 'export PATH="~/.rbenv/bin:$PATH"' >> ~/.bashrc && \
    echo 'eval "$(rbenv init -)"' >> ~/.bashrc && \
    CONFIGURE_OPTS='--disable-install-rdoc' /root/.rbenv/bin/rbenv install 2.3.1 && \
    /root/.rbenv/bin/rbenv global 2.3.1
ADD . /app/sample
RUN /root/.rbenv/shims/gem install bundle && \
    cd /app/sample && /root/.rbenv/shims/bundle install

なんとイメージサイズは892MBもありました。

rbenvのinstallとrubyのinstallが無駄ではないか?

そもそも真面目にDockerfile内でinstallしていることに疑問を持ち(インストール用に余計にパッケージが必要)、dockerhubからrubyのbaseimageを使ってみることにしました。
rubyが既に入っているので、それを入れるために必要だったのもを全て排除して見た結果のDockerfileがこちら。

FROM ruby:2.3.1
RUN  mkdir -p /app
ADD . /app/sample
RUN cd /app/sample && /usr/local/bin/bundle install

なんとサイズが1.05GBGBに!なぜ増える!

ruby-alpineに変えてみる

よくよくみると、rubyのimage sizeが730MBもあることに気づきます。
結局rubyをinstallするために色々とパッケージを入れている模様。
ということで、127MBしかないruby-alpineを使ってみることに。
以下がDockerfile。alpineなので多少必要なパッケージを追加で入れています。

FROM ruby:2.3.1-alpine
RUN apk add --update\
    build-base \
    git \
    openssh \
    mysql-dev \
    linux-headers \
    && \
    mkdir -p /app
RUN  mkdir -p /app
ADD . /app/sample
RUN cd /app/sample && /usr/local/bin/bundle install

そしてイメージサイズは、857MB、、、あれ、、、、

結局何がサイズを大きくしてんの??

ruby:2.3.1-alpineのサイズ : 127MB
apk add した後 545MB
sample app ソースを追加した後 563MB
bundle install後 857MB

apk add と bundle installが大きい。まあ予想通り。

Multi-Stage Buildsを使おう

本題はここから。

Multi-Stage Buildsは、簡単にいうと、ビルド用のイメージを作った後に必要なファイルだけコピーして新しいデプロイ用などのイメージを作成できる機能です。

注) 諸々必要なものをinstallすると、ubuntuとalpineに大差がないと判明したので以後は何かと都合の良いubuntuで話を進めることに。

まず試したのは、今までのbuildイメージを先に作ってbundle installまでした後に、アプリのソース(/app以下)とruby(/root以下)のライブラリだけをコピーして来るパターン。

注2) Dockerfileを簡単にするために/rootにrbenv入れてますが本来は別ユーザー作ってそっちに入れた方がいいと思います。

FROM ubuntu:trusty as build-env
RUN apt-get update && \
    apt-get -y install git curl make openssl zlib1g-dev libssl-dev libreadline-dev sysv-rc-conf build-essential mysql-client libmysqlclient-dev && \
    mkdir -p /app && \
    git clone https://github.com/sstephenson/rbenv.git ~/.rbenv && \
    git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build && \
    echo 'export PATH="~/.rbenv/bin:$PATH"' >> ~/.bashrc && \
    echo 'eval "$(rbenv init -)"' >> ~/.bashrc && \
    CONFIGURE_OPTS='--disable-install-rdoc' /root/.rbenv/bin/rbenv install 2.3.1 && \
    /root/.rbenv/bin/rbenv global 2.3.1
ADD . /app/sample
RUN echo 'gem: --no-rdoc --no-ri' > /root/.gemrc && \
    /root/.rbenv/shims/gem install bundle && \
    cd /app/sample && /root/.rbenv/shims/bundle install

FROM ubuntu:trusty
RUN apt-get update && \
    apt-get -y install mysql-client
COPY --from=build-env /root /root
COPY --from=build-env /app /app

mysql-clientだけ必要だったのでそれは追加。
これでイメージサイズが727MBに!
元々が892MBだったので165Mも削減!

こっそり.gemrc追加しましたが、これは1MBくらいしか削減効果ありませんでした。

OSを変える

Dockerfileを眺めていてふと気づく。
ubuntuが古い。
試しに、ubuntu:zestyをdocker pullしてみると、trustyよりもサイズが小さい!
昔誰かに、ubuntuを使っているならとりあえずdebianに変えて見たらdockerイメージが小さくなるよって言われたのを思い出しましたが、
debianのイメージサイズよりもubuntu:zestyの方が小さいようです。

ということで書き換え。

FROM ubuntu:zesty as build-env
RUN apt-get update && \
    apt-get -y install git curl make openssl zlib1g-dev libssl-dev libreadline-dev build-essential mysql-client libmysqlclient-dev && \
    mkdir -p /app && \
    git clone https://github.com/sstephenson/rbenv.git ~/.rbenv && \
    git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build && \
    echo 'export PATH="~/.rbenv/bin:$PATH"' >> ~/.bashrc && \
    echo 'eval "$(rbenv init -)"' >> ~/.bashrc && \
    CONFIGURE_OPTS='--disable-install-rdoc' /root/.rbenv/bin/rbenv install 2.3.1 && \
    /root/.rbenv/bin/rbenv global 2.3.1
ADD . /app/sample
RUN echo 'gem: --no-rdoc --no-ri' > /root/.gemrc && \
    /root/.rbenv/shims/gem install bundle && \
    cd /app/sample && /root/.rbenv/shims/bundle install

FROM ubuntu:zesty
RUN apt-get update && \
    apt-get -y install tzdata openssl libmysqlclient-dev
COPY --from=build-env /root /root
COPY --from=build-env /app /app

多少必要なパッケージが変わったのでapt-getの部分を少し修正。
イメージサイズは614MBに!
OSを変えるだけでこんなに変わるとは、、、

いらなそうなファイルがないか探す

あんまりなさそうだったが、この辺は要らないので Dockerfileの最後にこれを追加。
bundle install後にできるgemsの中身はもっとせめて消せそうなものがありそうだけど、攻めすぎて壊しそうなので一旦断念。
c extensionを含むgemが割とサイズが大きそうで、実行ファイル以外消したらもうちょと小さくなりそうだけど、ROI低そうな感じがした。

RUN rm -fr /var/lib/apt/lists/*.lz4 && \
        rm -fr ~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/cache/*

これを消してもそこまで小さくならず、613MBに。

とても大事なことに最後に気づく、、、

配布用のイメージなので、bundle installは --without development test をつけてあげないとダメでした。。。
ということで最終的には、547MBになりました。
gemの削減が一番効果あるな、これは、、、

892MB => 547MB なので 345MBの削減になりました!

まとめ

  • Multi-Stage Buildsは使った方がいい
  • alpineとかじゃなくてもOS変えるだけでイメージ小さくなることがある
  • 使わないgemとかバンバン消していきましょう!

ちなみに

出来上がったイメージが問題なく動作するかどうかは、rspecが成功するかどうかで見ています。
実際にミドルウェアレベルの変更を行なったものをリリースする場合には、QAなど手厚くすることをお勧めします。