はじめに
本記事は、エムスリーキャリア Advent Calendar 2022の15日目の記事です。
イメージレイヤーを意識しないDockerfile
の例
Rails&Webpackerなプロジェクトの本番環境を構築する際、おおまかに以下のような工程になります。
- Ruby・Node.js・Yarnのインストール
-
bundle install
&yarn install
で依存関係のインストール -
rails assets:precompile
でアセットプリコンパイル -
rails server
でRailsを起動
これをイメージレイヤーを意識せずにDockerfile
に落とし込むと以下のような構成になります。
※各ミドルウェアのバージョンは以下の想定
- Ruby 2.7.7
- Node.js 14.x
# Ruby 2.7のイメージからビルドする
FROM ruby:2.7.7
# プロジェクトのソースコードをコピー
WORKDIR /app
COPY . /app
# Node.js v14.xをインストール
# https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - &&\
apt-get install -y nodejs
# Yarnを有効化
# https://yarnpkg.com/getting-started/install
RUN corepack enable
# 依存関係をインストール
RUN bundle install
RUN yarn install --check-files
# アセットプリコンパイル
RUN rails assets:precompile
# Railsを起動
CMD rails server -b 0.0.0.0 -p 3000
しかし、このDockerfile
は以下のような問題点を抱えています。
-
ソースコードを少し変えただけで、毎回が以下実行される
- Node.jsのインストール・Yarnの有効化(初回だけ実行すればいい)
- 依存関係のインストール(依存関係の変更がなければ実行しなくていい)
そこでDockerのイメージレイヤーを意識したDockerfile
にすることで上記課題を解決していきます。
イメージレイヤーについて
ビルド後のDockerイメージは、そのファイルシステムの状態がレイヤー状に積み重ねて保存されます。
イメージレイヤーはdocker history
コマンドで確認することができます。
$ docker build -t rails_example .
$ docker history rails_example
IMAGE CREATED CREATED BY SIZE COMMENT
8d9cd0b316f1 22 seconds ago CMD ["/bin/sh" "-c" "rails server -b 0.0.0.0… 0B buildkit.dockerfile.v0
<missing> 22 seconds ago RUN /bin/sh -c rails assets:precompile # bui… 16.6MB buildkit.dockerfile.v0
<missing> 36 seconds ago RUN /bin/sh -c yarn install --check-files # … 207MB buildkit.dockerfile.v0
<missing> About a minute ago RUN /bin/sh -c bundle install # buildkit 425MB buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c corepack enable # buildkit 0B buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c curl -fsSL https://deb.nodeso… 143MB buildkit.dockerfile.v0
<missing> 4 minutes ago COPY . /app # buildkit 418kB buildkit.dockerfile.v0
<missing> 17 hours ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 8 days ago /bin/sh -c #(nop) CMD ["irb"] 0B
<missing> 8 days ago /bin/sh -c mkdir -p "$GEM_HOME" && chmod 777… 0B
<missing> 8 days ago /bin/sh -c #(nop) ENV PATH=/usr/local/bundl… 0B
<missing> 8 days ago /bin/sh -c #(nop) ENV BUNDLE_SILENCE_ROOT_W… 0B
<missing> 8 days ago /bin/sh -c #(nop) ENV GEM_HOME=/usr/local/b… 0B
<missing> 8 days ago /bin/sh -c set -eux; savedAptMark="$(apt-m… 30MB
<missing> 8 days ago /bin/sh -c #(nop) ENV RUBY_DOWNLOAD_SHA256=… 0B
<missing> 8 days ago /bin/sh -c #(nop) ENV RUBY_VERSION=2.7.7 0B
<missing> 8 days ago /bin/sh -c #(nop) ENV RUBY_MAJOR=2.7 0B
<missing> 8 days ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 8 days ago /bin/sh -c set -eux; mkdir -p /usr/local/et… 45B
<missing> 9 days ago /bin/sh -c set -ex; apt-get update; apt-ge… 529MB
<missing> 9 days ago /bin/sh -c apt-get update && apt-get install… 152MB
<missing> 9 days ago /bin/sh -c set -ex; if ! command -v gpg > /… 19MB
<missing> 9 days ago /bin/sh -c set -eux; apt-get update; apt-g… 10.7MB
<missing> 9 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 9 days ago /bin/sh -c #(nop) ADD file:553d20b03d45305e5… 124MB
Dockerfile
に書いた命令文(RUN
, COPY
など)ごとに逆順でレイヤーが作成されているのがわかります。
各レイヤーのサイズはその命令文で発生したファイルシステムの差分量であり、全レイヤーの合計がそのイメージのサイズとなります。
例えば、COPY . /app
はソースコードがコピーされるのでその量(上記例では418kB)、WORKDIR /app
やRUN corepack enable
はファイルシステムに影響を与えないので0Bとなっています。
Dockerfile
のTips集
レイヤーキャッシュを考慮して、変更頻度の少ないファイルからCOPY
・ADD
を書く
一度イメージをビルドすると、ビルドしたマシンに各イメージレイヤーがキャッシュされます。
次回ビルド時、前回ビルド時と変更がないレイヤーは、キャッシュが効いてスキップされます。
変更がないとは、命令文が変更されていない、またCOPY
・ADD
の場合は追加されるファイルが変更されていない場合を指します。
Dockerfile
の上から走査して、キャッシュが効かない命令文があると、その下の命令文もキャッシュが無効になります。
「イメージレイヤーを意識しないDockerfileの例」ではDockerfile
の上の方にCOPY . /app/
があるため、ソースコードを少し変更しただけでほぼ全てのキャッシュが無効になります。
「Node.js v14.xをインストール」「Yarnを有効化」にソースコードは不要のため、COPY
はこれらの後に記述するべきです。
また、「依存関係をインストール」の前には依存関係を記載したファイルのみCOPY
するべきです。
# Ruby 2.7のイメージからビルドする
FROM ruby:2.7.7
WORKDIR /app
- COPY . /app
# Node.js v14.xをインストール
# https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - &&\
apt-get install -y nodejs
# Yarnを有効化
# https://yarnpkg.com/getting-started/install
RUN corepack enable
+ # 必要なファイルのみコピーしてbundle install
+ COPY Gemfile Gemfile.lock /app/
RUN bundle install
+ # 必要なファイルのみコピーしてyarn install
+ COPY package.json yarn.lock /app/
RUN yarn install --check-files
+ # プロジェクトのソースコードをコピー
+ COPY . /app
# アセットプリコンパイル
RUN rails assets:precompile
#(以下略)
これにより、課題となっていた以下問題がレイヤーキャッシュにより解決します。
-
Node.jsのインストール・Yarnの有効化が毎回実行される
- 一度ビルドしたら次回以降のビルドはキャッシュされる
-
依存関係のインストールが毎回実行される
-
Gemfile
かGemfile.lock
が変更された時のみbundle install
される -
package.json
かyarn.lock
が変更された時、または↑のbundle install
された時のみyarn install
される
-
マルチステージビルド化する
レイヤーキャッシュを考慮することでビルドにかかる時間を大きく短縮することができましたが、まだ「Gemfile
かGemfile.lock
を変更したら、必要のないyarn install
も実行される」課題があります。
これを解決するため、Dockerfile
をマルチステージビルド化します。
マルチステージビルドにすることで、ステージ毎にビルドを並列実行できる (Docker 18.09以降導入されたbuildkit利用時)メリットもあります。
マルチステージビルドは、Dockerfile
にFROM
命令文を複数書くだけで実現できます。
以下のステージに分けた例を紹介します。
-
bundle install
する - Node.jsとYarnをインストールする
- 2.をベースに
yarn install
とアセットプリコンパイルする - 最終的に出力するイメージ
# このビルドステージでbundle installを行う
FROM ruby:2.7.7 as bundle_install
# bundle installに必要なファイルのみコピー
WORKDIR /app
COPY Gemfile Gemfile.lock /app/
RUN bundle install
# このビルドステージでNode.jsとYarnをインストール
FROM ruby:2.7.7 as ruby_with_nodejs
WORKDIR /app
# Node.js v14.xをインストール
# https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - &&\
apt-get install -y nodejs && \
# Yarnを有効化
# https://yarnpkg.com/getting-started/install
corepack enable
# このビルドステージでアセットプリコンパイル
FROM ruby_with_nodejs as assets_precompile
# yarn installに必要なファイルのみコピー
COPY package.json yarn.lock /app/
RUN yarn install
# プロジェクトのソースコードをコピー
COPY . /app/
# bundle_installビルドステージからインストールしたgemをコピー
# --fromで別のビルドステージからファイルをコピーできる
COPY --from=bundle_install /usr/local/bundle /usr/local/bundle
RUN rails assets:precompile
# このビルドステージでrailsを起動
FROM ruby_with_nodejs as rails
# bundle_installビルドステージでインストールしたgemをコピー
COPY --from=bundle_install /usr/local/bundle /usr/local/bundle
# assets_precompileビルドステージからプロジェクト全体をコピー
COPY --from=assets_precompile /app /app
# Railsを起動
CMD rails server -b 0.0.0.0 -p 3000
(ついでに)イメージに必要ないファイルは.dockerignore
で無視する
イメージレイヤーとは直接関係ありませんが、イメージのビルドやコンテナの実行に必要ないファイルを無視するよう.dockerignore
を設定します。
これで不用意にレイヤーキャッシュが無効になることを避けることができます。
また.env
のようなセキュリティ的に外出しするとマズいファイルや、public/packs
やキャッシュなど開発中に生成される中間ファイルを無視する役割もあります。
.git/
log/
node_modules/
tmp/
public/assets/
public/packs/
.dockerignore
.env
.gitignore
Dockerfile
README.md
最後に
エムスリーキャリアでは積極的にエンジニアを採用しております。
Dockerなど新めの技術を取り入れた開発環境で医療従事者の働き方を革新しませんか?
参考記事
本家大元、docker.comのベストプラクティス集を参考にしました!
https://docs.docker.com/develop/develop-images/dockerfile_best-practices