Rails
docker
alpine
alpinelinux

Rails on Dockerプロダクションイメージの容量削減をしてみた

More than 1 year has passed since last update.

重い

pushするのに時間がかかりすぎている.ビルドが遅いのは仕方がない.
いくらなんでもデプロイのたびに待つのはつらいので原因を探ってみると,
イメージ容量が800M!あれRubyってこんなに重いっけ???

ダイエット

目標は100MB台.

イメージ変遷

ruby:2.3.3(835.7MB) -> ruby:2.3.3-alpine(264.7MB) -> alpine:3.4(131.3MB)

約6分の1になりました.

FROM ruby:2.3.3

最初の状態.835.7MBうーん重い.流石に毎回これを転送するのはちょっと…

Dockerfile.debian
FROM ruby:2.3.3

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/

RUN apt-get update && apt-get install -y git nodejs && apt-get clean &&\
    gem install bundler --no-document && \
    bundle config build.nokogiri --use-system-libraries && \
    bundle install --without development test&& \
    apt-get clean

COPY . .

RUN rails assets:precompile RAILS_ENV=production

EXPOSE 3000

CMD rails s -p 3000 -b '0.0.0.0'

FROM ruby:2.3.3-alpine

alpineって軽いよねー,流行ってるよねー.
264.7MBかぁ,まだ重いね.

Dockerfile.alpine
FROM ruby:2.3.3-alpine

ENV RUNTIME_PACKAGES="libxml2-dev libxslt-dev libstdc++ tzdata mariadb-client-libs nodejs ca-certificates"\
    DEV_PACKAGES="build-base mariadb-dev"

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/

RUN apk add --update --no-cache $RUNTIME_PACKAGES &&\
    apk add --update\
    --virtual build-dependencies\
    --no-cache\
    $DEV_PACKAGES &&\
    gem install bundler --no-document &&\
    bundle config build.nokogiri --use-system-libraries &&\
    bundle install --without development test &&\
    apk del build-dependencies

COPY . .

RUN rails assets:precompile RAILS_ENV=production

EXPOSE 3000

CMD rails s -p 3000 -b '0.0.0.0'

FROM alpine:3.4

いや別にruby最新版使う必要ないし,ソースビルドしなくてもいいよね.
ビルド済みパッケージ使ったら軽くなるんじゃね?
131.3MB!やった!目標達成.これだったら,ローカルバイナリ並みの容量ですね.

FROM alpine:3.4

ENV RUNTIME_PACKAGES="ruby ruby-irb ruby-json ruby-rake ruby-bigdecimal ruby-io-console ruby-dev libxml2-dev libxslt-dev libstdc++ tzdata mariadb-client-libs nodejs ca-certificates"\
    DEV_PACKAGES="build-base mariadb-dev"

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

RUN apk add --update --no-cache $RUNTIME_PACKAGES

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/

RUN apk add --update\
    --virtual build-dependencies\
    --no-cache\
    $DEV_PACKAGES && \
    gem install bundler --no-document && \
    bundle config build.nokogiri --use-system-libraries && \
    bundle install --without development test&& \
    apk del build-dependencies

COPY . .

RUN rails assets:precompile RAILS_ENV=production

CMD rails s -p 3000 -b '0.0.0.0'

まとめ

rubyの公式イメージでも,debianベースで732MBくらい,alpineベースで136.3MBくらいとデカイ.
最新バージョンのrubyを使わなきゃいけない事情がない限り,ビルド済みのパッケージでrubyは入れたほうが楽早軽.

おまけ

データを削除しているはずなのにイメージ容量が減らない

最初ruby:2.3.3-alpineのイメージで作ったときに容量が驚きの648MB!
debian版に比べたら200MBほど減少はしているものの,軽量なはずのalpinelinuxのメリットを消してしまっている.
その時のDockerfileはこんな感じ.

Dockerfile.pokotsun
FROM ruby:2.3.3-alpine

ENV RUNTIME_PACKAGES="libxml2-dev libxslt-dev libstdc++ tzdata mariadb-client-libs nodejs ca-certificates"\
    DEV_PACKAGES="build-base mariadb-dev"

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/

RUN apk add --update --no-cache $RUNTIME_PACKAGES

RUN apk add --update\
    --virtual build-dependencies\
    --no-cache\
    $DEV_PACKAGES

RUN gem install bundler --no-document && \
    bundle config build.nokogiri --use-system-libraries && \
    bundle install --without development test

RUN apk del build-dependencies

COPY . .

RUN rails assets:precompile RAILS_ENV=production

EXPOSE 3000

CMD rails s -p 3000 -b '0.0.0.0'

DockerfileってRUNにコマンド書くときにシェルスクリプトにまとめたり,&&で結合してるのをよく見るけれども
なんか見づらいなって感じてた頃がありました.
で,何を思ったかRUNを複数行に書いてしまったわけです.

overlayfsだと,apt-get/apkで依存関係をインストール,bundle install後に削除というのを別のRUNで実行していたためそれぞれにレイヤーが作成され,ファイル削除したレイヤーはファイルを削除したという情報だけが残ります.よって追加されたファイルのデータ自体は消えないわけですね.

解決方法は依存関係のインストール→bundle install→依存関係のアンインストールを一つのRUNで実行するだけですね.お恥ずかしい.

alpineだとgem install bundlerで証明書エラーが出る

ERROR:  Could not find a valid gem 'bundler' (>= 0), here is why:
          Unable to download data from https://rubygems.org/ - SSL_connect returned=1 errno=0 state=error: certificate verify failed (https://api.rubygems.org/specs.4.8.gz)

ルート証明書が入ってなかったので,https通信に失敗しているみたい.
apk add ca-certificatesをしましょう.

bundle execできない

開発環境のcomposeが混ざっていた

意外とどこのサイトでも書いてないけど,docker-composeとかでローカルディレクトリをマウントしていた場合,
vendor/bundleがアーキテクチャ違うから動かない.

dockerで開発するならdockerのみで,ローカルで開発するならローカルのみで動かさないとハマりやすい.

dockerignoreについて

上でも述べた,ローカルマウントに関連してハマったポイント.
マイグレーションのテストをしようとdocker-compose run --rm --no-deps app shした時,
なぜかignoreしているはずのvendor/bundleがいる…と混乱した.
そのときに,何も考えないでrm -rf vendor/bundle && bundle installして,migrateする.
ローカルに戻るとbundle execできないみたいな状況に陥り,パニックに.

よく考えればbuild時は正常にvendor/bundleはignoreされ,add .しても特に問題はない.
ただ,その後にvolumeマウントしていたためにignoreしたはずのファイルがいるということ.

気をつけよう.