Dockerでサーバーを構築し始めました。
alpine linuxってなんやねんと最初の最初は思っていましたが、割と直ぐに慣れてきて
実際に案件でDockerファイルを書き出した所、もっとdeployしやすいイメージを作りたいと下の記事を見つけて、イメージを小さくすることにしました。
Dockerイメージは、気を抜くとすぐにGBを超えるサイズになります。
実際、「&&」を使ってレイヤーのまとめ込みくらいはしていたのですが、最初に作ったDockerイメージは2.19GBになりました。
ここから、実際の案件で使うrailsプロジェクトを、妥当な工数で小さなイメージへ変更していきます。
基本
まずは基本的なことをさらっとおさらい。
- 不要なファイルを作らない
- .dockerignoreで、いらないファイルを含めない
- 命令をまとめて、無駄なレイヤリングを排除する
この辺りのDocker全般の話は既にほかの記事で触れらているので、ここから先の話をします。
Dockerのイメージの選択
Webフレームワークは、ここまでの歴史の中である程度正解の出ている世界で、他のフレームワークで行っている事をある程度参考にできますので、今回は、ググっている最中に見つけたLaravelの記事と近い正解になっています。
Dockerコンテナイメージのダイエット - Laravel編
ということで、まずは無駄なもののない軽いDockerイメージを選択します。
dockerhubのrubyのリポジトリを見てみると分かりますが、イメージ自体はかなり複数の種類があります。
今回自分の案件はruby2.5ベースだったのでこちらを参考にしますが2.5のイメージでも、debianからalpineベースまで複数種類あります。一番小さいのはalpine linuxですが、イメージのサイズはこんな感じで大分違います。
version | サイズ |
---|---|
2.5.1 | 869MB |
2.5.1-slim | 178MB |
2.5.1-alpine3.7 | 45.3MB |
通常のrubyのDockerfileは、railsの実行に不要なファイルを割と含んでいるので
出来るだけ、軽いイメージをベースに作成を行って行きます。
このあたりは、既にRails on Dockerプロダクションイメージの容量削減をしてみた
で同じことが書いてあります。
ちなみに、以下の様にversionが上がるとイメージの容量は下がる傾向があります
version | サイズ |
---|---|
2.6.0 | 868MB |
2.6.0-slim | 126MB |
2.6.0:alpine-3.8 | 40MB |
まずここで、最初rubyのベースを使って2.19GBだったのがalpine-linuxをベースにすることで、1.42GBまで削減出来ました。
やっぱりイメージを変更するのは大きい。
ただ、alpineのイメージ自体は50MBもないのに、
「あ、gccが無い」とか「あ、mysqlの開発者向け入れないと」とかすると、せっかくのサイズがみるみる肥えていって、ちゃんとbundle install可能な頃には、40MB のイメージが 700MBくらいまで増えます。やっぱりビルドにはいろんなものが必要でこれにbundle install
したりyarn install
したりした結果、結局1GBは余裕で超えてしまいました。
さらにダイエット
ここからがこの記事の本題です。
イメージが大きいという事は何かしら無駄なファイルがあるので
この後、ビルドしたイメージにdocker exec -it コンテナID /bin/sh
でログインして内部のディレクトリを以下のコマンドで表示してきます。
シンプルにdu / -hd 1
でディレクトリ別の容量を見てみます。
1.7M /etc
2.8M /tmp
16.0K /media
636.0K /sbin
5.2M /lib
2.2M /bin
4.0K /srv
4.0K /mnt
4.0K /run
0 /proc
4.0K /home
27.6M /root
0 /sys
889.8M /usr
0 /dev
300.0K /var
618.5M /rails
500.0K /share
結果としては、/usr
以下が889MBで最大。これにrailsを配置している/rails
自体の容量が618MBと大きい様です.
さらに、そこからディレクトリを掘り込んで調べたら、結果として以下のディレクトリが容量の原因でした。
- 326.3M /usr/local/share/.cache/yarn
- 245.1M /rails/node_modules
- 262.2M /rails/vendor
正直yarnのキャッシュとか絶対不要なので、これは消さないといけません。
これ以外にも、/usr/
以下を探すとgccやgitなど、bundle install
に必要だけど、railsの実行には使わないなファイルがありました。
ということで、不要なファイルを以下の様に設定します。
- /usr以下にある、gccなどのbundle installを成功させるためのファイル
- /node_modulesにあるjsはassets:precompileするために必要なファイル、それが終われば本番では不要
- yarnのcacheも正直不要
ただ、alpine-sdk
などの、apk
でinstallしたものをbundle install
実行後に消し去るのは、いろんなところにファイルが散らばっているので正直しんどいです。
なので、まずは、bundle install
やassets:precompile
を行って
その後に、マルチステージビルドを行って、railsの動作に必要なファイルだけを新規にinstallし直して、その後そこにbuildの終わったファイルをコピーする方針で考えました。
マルチステージビルド
同じレイヤー上で作ったファイルをすぐ消せば、Dockerのファイル使用量は増えません。
ですが、gccなどビルド全般にわたって必要なものまでそれを適応しようとするのはしんどいので、マルチステージビルドで書きました。
この作戦で作ったDockerファイル、最初2.19GBだったものは、イメージの変更で1.42GB。
さらに不要なファイルを削除した結果464MBまで減りました。
実際のDockerfileの案件依存の部分を取り除くとこんな感じになりました。
Railsのイメージを作るときの雛形に使ってください。
FROM node:8.9.4-alpine as node
FROM ruby:2.6-alpine as builder
# 依存関係のあるパッケージのinstall
# gccやgitなど、ビルドに必要なものもすべて含まれている
RUN apk --update --no-cache add shadow sudo busybox-suid mariadb-connector-c-dev tzdata alpine-sdk
WORKDIR /rails
COPY Gemfile Gemfile.lock ./
# rails5案件なのでwebpackerの動作環境を整えておく
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/include/node /usr/local/include/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /opt/yarn /opt/yarn
RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs && \
ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn
# bundle installした後、makeで発生した不要なファイルは削除。
RUN gem install bundler --version 1.16.1 && \
bundle install --without development test --path vendor/bundle && \
find vendor/bundle/ruby -path '*/gems/*/ext/*/Makefile' -exec dirname {} \; | xargs -n1 -P$(nproc) -I{} make -C {} clean
# yarn install
COPY package.json yarn.lock ./
RUN yarn install && yarn cache clean
# assets precompile
COPY Rakefile .babelrc ./
COPY app/javascript app/javascript
COPY app/assets app/assets
COPY bin bin
COPY config config
RUN RAILS_ENV=production bundle exec rails assets:precompile
FROM ruby:2.6-alpine
# パッケージ全体を軽量化して、railsが起動する最低限のものにする
RUN apk --update --no-cache add shadow sudo busybox-suid execline tzdata mariadb-connector-c-dev && \
cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
apk del --purge tzdata
EXPOSE 3000
WORKDIR /rails
# gemやassets:precompileの終わったファイルはbuilderからコピーしてくる
COPY --from=builder /rails/vendor/bundle vendor/bundle
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /rails/public/assets/ /rails/public/assets/
COPY --from=builder /rails/public/packs/ /rails/public/packs/
COPY . /rails
# rails立ち上げのためのコマンドはうちの案件ではシェルスクリプトにまとめています。各社好きに書いてください
# 普通はpumaを立ち上げるとかそんな感じだと思います
ENTRYPOINT ["docker/rails/entrypoint.sh"]
CMD exec bundle exec puma -t "$PUMA_THREADS":"$PUMA_THREADS" -p 3000 -e "$RAILS_ENV" -C config/puma.rb
ここまでやった結果の中身ですが、内部をdu / -hd 1
して覗いてみた結果464MBのイメージの中身は以下のような状態。
- 262.2MB /rails/vendor
- 319.8MB /rails
300MB以上が大半がrailsのコードのあるディレクトリでinstallしたgemが占めています。
その他になるOSと基礎的なライブラリで100MBちょっと。
OSと、railsのためのCライブラリとしては割と最小構成に近いところまできた様です。
ここまでくると、あとはrailsの中の細かいファイルを丁寧に削っていくことになります。
具体的には本番で使わない.rubocop.yml
とかを、丁寧に.dockerignore
に丁寧に入れていって。
最後に、vendor/bundle以下のファイルに手を出します。
gemの中の、spec
ディレクトリ、README.md
、あとgithubから直接取ってくるgemの場合.git
を削ればさらに減らせますけどgemの中身に手を出すのはリスクと隣り合わせでしょう。
ちなみに、この案件で上記のファイルを消してみましたが、結果、5Mしか減りませんでした。
正直、数MBをダイエットさせるために、工数を割いてもあんまり意味ないのでここらへんが折り合い点かなって思います。
以上、現場からの報告でした。