本記事の趣旨
[令和時代のRails運用]
(https://speakerdeck.com/joker1007/ling-he-shi-dai-falserailsyun-yong)
こちらのスライドに掲載されている以下のDockerfileが、キャッシュやマルチステージビルドを利用したベストプラクティスとして参考になりました。
# syntax = docker/dockerfile:experimental
# Node.jsダウンロード用ビルドステージ
FROM ruby:2.6.5 AS nodejs
WORKDIR /tmp
# Node.jsのダウンロード
RUN curl -LO https://nodejs.org/dist/v12.18.0/node-v12.18.0-linux-x64.tar.xz
RUN tar xvf node-v12.18.0-linux-x64.tar.xz
RUN mv node-v12.18.0-linux-x64 node
FROM ruby:2.6.5
# nodejsをインストールしたイメージからnode.jsをコピーする
COPY --from=nodejs /tmp/node /opt/node
ENV PATH /opt/node/bin:$PATH
# アプリケーション起動用のユーザーを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails
# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH
# ruby-2.7.0でnewした場合を考慮
RUN gem install bundler
WORKDIR /app
# Dockerのビルドステップキャッシュを利用するため
# 先にGemfileを転送し、bundle installする
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/
RUN bundle config set app_config .bundle
RUN bundle config set path .cache/bundle
# mount cacheを利用する
RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle \
bundle install && \
mkdir -p vendor && \
cp -ar .cache/bundle vendor/bundle
RUN bundle config set path vendor/bundle
RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules \
bin/yarn install --modules-folder .cache/node_modules && \
cp -ar .cache/node_modules node_modules
COPY --chown=rails . /app
RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile
# 実行時にコマンド指定が無い場合に実行されるコマンド
CMD ["bin/rails", "s", "-b", "0.0.0.0"]
ここで使用されているBuildkitなどの要素について、勉強した内容をまとめたいと思います。
これらの方法により、開発効率の向上を実感しましたので、RailsとDockerを学習中の方のご参考になればと思います。
(投稿者はDockerを勉強中で、実務は未経験ですので、気になる点がありましたらコメントでご指摘をお願いします。)
注記:上記のDockerfileは、元スライドの物から、Node.jsのバージョンだけ、20/6/6時点での最新verに変更しています。
参考記事
Dockerfileを改善するためのBest Practice 2019年版
[Docker Buildにおけるリードタイム短縮のための3つの改善ポイント]
(https://tech.plaid.co.jp/improve_docker_build_efficiency)
開発環境
- Mac OS X 10.15.4
- Docker 19.03.8
- Docker Desktop for Mac 2.3.0.3
- Ruby 2.6.5 Rails 6.0.2
Dockerfileの解説
上記のDockerfileの要点を見ていきます。
1行目の# syntax =
という部分は後述するBuildkitに関する記述です。
その次の
# Node.jsダウンロード用ビルドステージ
FROM ruby:2.6.5 AS nodejs
WORKDIR /tmp
# Node.jsのダウンロード
RUN curl -LO https://nodejs.org/dist/v12.18.0/node-v12.18.0-linux-x64.tar.xz
RUN tar xvf node-v12.18.0-linux-x64.tar.xz
RUN mv node-v12.18.0-linux-x64 node
ここでは、Railsに必要なNode.jsをインストールしています。
- tmpに移動
- node-v12.18.0-linux-x64.tar.xzをダウンロード、展開
- node-v12.18.0-linux-x64をnodeにリネーム
の結果、/tmp/node
(本体), /tmp/node-v12.18.0-linux-x64.tar.xz
(不要)
が生成されます。
最終的なDockerイメージを軽量にするために、必要なnode本体だけを残す必要があります。Dockerはレイヤー構造で履歴が残っているため、ただ単にRUN rm node-v12.18.0-linux-x64.tar.xz
としても意味がないようです。
そこで、マルチステージビルドを利用しています。
マルチステージビルド
マルチステージビルドは、1つのDockerfileに複数のステージを分けて記述し、最後のステージの内容だけが最終イメージに含まれます。
例のDockerfileでは、FROM
行が2箇所、つまり2つのステージがあります。
FROM ruby:2.6.5 AS nodejs
...
FROM ruby:2.6.5
1つ目のAS nodejs
の記載で、ステージにnodejs
と名付けています。これによって、2つ目のステージで、
# nodejsをインストールしたイメージからnode.jsをコピーする
COPY --from=nodejs /tmp/node /opt/node
ENV PATH /opt/node/bin:$PATH
上でインストールしたtmp/node
だけをコピーする事ができます。
今回の場合は、これによって節約できるのは15MBほどですが、Goのようなコンパイル言語で、ビルド結果のファイルだけを次のステージに渡すと、かなりの軽量化ができるようです。
ちなみに、2つ目のステージのyarnインストールは、ファイルそのものではなく、install.shをダウンロードして実行しているだけで、不要なファイルが残らないため、このままで問題ないのだと思います。
# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH
ユーザーの追加
# アプリケーション起動用のユーザーを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails
ここでは、コンテナ内にユーザーを追加しています。デフォルトのrootユーザーのままでは、ホストとファイルを共有する際に権限の問題が発生するようです。
ただ、Docker for Macの場合、その問題は起こらないので、この設定は省略しても良いかもしれません。
(非rootユーザーにすると、vimを使いたい時にapt-getができないなど、色々と困る場面もありましたので..)
bundle install
WORKDIR /app
# Dockerのビルドステップキャッシュを利用するため
# 先にGemfileを転送し、bundle installする
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/
作業ディレクトリに必要なパッケージ管理ファイルを置いています。その後、
RUN bundle config set app_config .bundle
RUN bundle config set path .cache/bundle
まずbundle config
コマンドを使用して、installするpathを設定しています。
bundle config
例として、bundle config set path vendor/bundle
と設定しておくと、bundle install
の際に、bundle install --path vendor/bundle
とパスを指定した事と同じになります。
installの際に、--path
を指定する方法は非推奨となったようなので、今後はbundle config
を使いましょう。
Cache Mount
bundle install
で、ビルド効率のためにCache Mountを利用しています。
RUN bundle config set path .cache/bundle
# mount cacheを利用する
RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle \
bundle install && \
mkdir -p vendor && \
cp -ar .cache/bundle vendor/bundle
RUN bundle config set path vendor/bundle
まずbundleのinstall先を.cache/bundle
に設定します。
続く--mount=type=cache
〜target=/app/.cache/bundle
という記載が、Cache Mountを利用している部分です。
この記載を含むRUN命令の中では、targetに指定したpath(ここでは/app/.cache/bundle
)の中身が、ホスト内に保存され、次回以降のbuildでcacheとして利用されるようになります。
したがって、直後のbundle install
で.cache/bundle
にインストールされたgemは、ホスト内部に保存されています。
このままだと、コンテナ内にgemがない状態になってしまうため、続けてvendorディレクトリを作り、そこに.cacheディレクトリから中身をコピーしています。
最後にbundle config set path vendor/bundle
でpathを指定することで、bundlerがvendor/bundleを読みに行ってくれるようになります。
やや回りくどい気もしますが、これによってbuild時間が劇的に改善しました。こちらによると、
RUN --mount=type=cache
命令をうまく活用すると,従来のdocker build
より33倍以上速いビルドも可能です.
私の環境ではbuildのたびにbundle installで300秒以上かかっていました。cacheがあれば、build時のbundle installは変更差分のみですぐ終わるので、気軽にbuildできます。
Buildkit
上述のCache Mountを使うためには、Buildkitでbuildをする必要があります。
Buildkitとは、dockerのイメージビルドを便利にしてくれるビルドツールキットです。こちらにあるように、ビルドのそれぞれの過程ごとにかかった時間を表示してくれたりします。
他にもビルドの並列実行など、たくさんの機能があるようです。
[BuildKit によるイメージ構築]
(https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/build_enhancements)
Buildkitの導入
主に2つの方法があります。
- 環境変数
DOCKER_BUILDKIT=1
を設定する。 - 試験機能モードを有効にすることで
docker buildx
コマンドを使う(Docker 19.03以上)。
1つ目は、DOCKER_BUILDKIT=1 docker build .
のように環境変数を指定する簡単な方法です。
2つ目は、buildxというプラグインを利用する方法で、buildkitの全ての機能が有効になるとのことです。config.json (デフォルトでは~/.docker/config.json
) に次のように指定します。
{
"experimental": "enabled"
}
これにより、環境変数なしでdocker buildx build .
のようにビルドを実行する事ができます。
[BuildKitによりDockerとDocker Composeで外部キャッシュを使った効率的なビルドをする方法]
(https://qiita.com/tatsurou313/items/ad86da1bb9e8e570b6fa)
Buildkit Cache Mountの利用
--mount
は新しい構文のため、Dockerfileの1行目に次の記述をする必要があります。
# syntax = docker/dockerfile:experimental
Cacheの削除
docker builder prune
以上がbuildkitの使い方です。
簡単な設定をするだけで良いので、cacheを使わない場合も、取り入れてみるといいかもしれません。
yarn install
RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules \
bin/yarn install --modules-folder .cache/node_modules && \
cp -ar .cache/node_modules node_modules
yarn installも同じくCache Mountを使います。
asset precompile
COPY --chown=rails . /app
RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile
最後にホストのファイルを全てコピーして、Cache Mountを利用してアセットをプリコンパイルします。
開発環境と本番環境でさらにステージを分けて、本番環境でのみプリコンパイルを行うなどの設定も考えられます。