Docker のマルチステージビルドで Rails イメージを軽くする

3 min read読了の目安(約2900字

昨今は本番環境にも当然のように Docker を使用すると思います。
その時、 Docker イメージが重いと CI/CD の速度も遅くなりますし、コンテナの起動も遅くなるので辛いです。Docker イメージを軽くすることの必要性は広まってきたように感じます。
そんなわけで今回は、 Docker のマルチステージビルドを使って Rails イメージを軽量化してみたいと思います🙌

前提

元々の Dockerfile は以下です。

FROM ruby:2.7.2-alpine3.12

ENV ROOT="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

RUN apk update && \
    apk add --no-cache \
        gcc \
        g++ \
        libc-dev \
        libxml2-dev \
        linux-headers \
        make \
        postgresql \
        postgresql-dev \
        tzdata && \
    apk add --virtual build-packs --no-cache \
        build-base \
        curl-dev

COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}

RUN bundle install
RUN apk del build-packs

COPY . ${ROOT}

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3001

CMD ["rails", "server", "-b", "0.0.0.0"]

alpine をベースにしており、 API モードのRailsアプリなので、 node.js とかは入れていません。フロントは別で用意します。

$ docker images
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
backend_web    latest    135f3d0ee69d   5 days ago      779MB

まだRailsにしては軽い方だと思いますが、重い。。。

最終的にどうなったか

FROM ruby:2.7.2-alpine3.12 as builder

ENV ROOT="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

RUN apk update && \
    apk add --no-cache \
        gcc \
        g++ \
        libc-dev \
        libxml2-dev \
        linux-headers \
        make \
        postgresql-dev \
        tzdata && \
    apk add --virtual build-packs --no-cache \
        build-base \
        curl-dev

COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}

RUN bundle install
RUN apk del build-packs


FROM ruby:2.7.2-alpine3.12

ENV ROOT="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

RUN apk update && \
    apk add \
        postgresql-dev \
        tzdata

WORKDIR ${ROOT}

COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY . ${ROOT}
COPY entrypoint.sh /usr/bin/

RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3001

CMD ["rails", "server", "-b", "0.0.0.0"]

こんな感じでダイエットしました!!
割と王道な使い方だけですが、ポイントは、

  • 1個目のステージの責務を bundle install を通すだけにする
  • 2個目のステージ(実際の起動用)では --from から1個目のステージを指定してライブラリをコピーする

というところです。

その結果👇

$ docker images
REPOSITORY     TAG        IMAGE ID       CREATED           SIZE
backend_web    latest     7dbd2aa9ebab   37 seconds ago    405MB

これだけで、 350MB 以上ダイエットに成功しました😊

地味に思うかもしれませんが、これだけの労力で 350MB 以上軽くなるなら結構お得なのではないでしょうか?

まとめ

「マルチステージビルド」と聞くと難しそうに聞こえますが、内容としては簡単です。

  • FROM を複数用意してそれぞれに名前を付ける(これが「ステージ」になります)
  • 後のステージから前のステージを --from を使って参照できる

別にマルチステージビルドを使わなくても、不要なファイルを rm とかで地道に消していけば同じようにダイエット効果が得られます。
ただ、それだと非常に Dockerfile の可読性が下がりますし、漏れがあったりします。何よりめんどくさい。。。

なので、「必要なものだけコピーしてあとは放置」というマルチステージビルドを利用することをお勧めします😄

参考にさせて頂いた資料