この記事は、Rails のデフォルトの Dockerfile のすべての行を順に確認し、ベスト プラクティスと最適化について説明する一連の投稿の一部です。
Docker イメージは、イメージ サイズの縮小、ビルド パフォーマンスの最適化、セキュリティと保守性のベスト プラクティス、アプリケーション固有の最適化など、さまざまな方法で最適化できます。最初の記事では、イメージ サイズの縮小の最適化についてのみ触れ、それがなぜ重要であるかを説明します。
ソフトウェア開発の他のすべてのプロセスと同様に、各開発者は Docker ビルドを高速化したい理由をリストアップします。ここでは、私にとって最も重要な理由をリストアップします。
イメージが小さいほど、処理する必要のあるファイルとレイヤーが少なくなるため、ビルドが速くなります。これにより、特に反復的な開発サイクル中に開発者の生産性が向上します。イメージが小さいほど、レジストリへのプッシュとデプロイメント中のプルにかかる時間が短くなります。これは、コンテナが頻繁にビルドおよびデプロイされる CI/CD パイプラインでは特に重要です。
イメージが小さいほど、コンテナ レジストリ、ローカル開発マシン、および運用サーバー上のストレージの消費量が少なくなります。これにより、特に大規模なデプロイメントの場合、インフラストラクチャ コストが削減されます。イメージが小さいほど、サーバー間で転送されるときに使用する帯域幅が少なくなります。これは、ローカルまたは CI/CD パイプラインでイメージを構築してレジストリにプッシュする場合に特に重要です。
「2022年にクラウドに320万ドルを費やしました...クラウドからの撤退から5年間でサーバー費用を約700万ドル節約できる見込みです。」デビッド・ハイネマイヤー・ハンソン — HEY World
イメージが小さいほど、読み込みと実行に必要なリソース (CPU、RAM など) が少なくなり、コンテナ化されたアプリケーションの全体的なパフォーマンスが向上します。起動時間が短いということは、サービスがより早く準備されることを意味します。これは、スケーリングと高可用性システムにとって重要です。alpine やdebian-slim
などのalpine
のベース イメージには、プリインストールされたパッケージが少なく、パッチが適用されていないソフトウェアや不要なソフトウェアが悪用されるリスクが減ります。
上記のすべてに加えて、不要なファイルやツールを削除すると、問題の診断時に注意が散漫になることが最小限に抑えられ、保守性が向上し、技術的負債が軽減されます。
サイズを含むイメージのさまざまなパラメータを取得するには、Docker Desktop を確認するか、ターミナルでdocker images
コマンドを実行します。
➜ docker images REPOSITORY TAG IMAGE ID CREATED SIZE kamal-dashboard latest 673737b771cd 2 days ago 619MB kamal-proxy latest 5f6cd8983746 6 weeks ago 115MB docs-server latest a810244e3d88 6 weeks ago 1.18GB busybox latest 63cd0d5fb10d 3 months ago 4.04MB postgres latest 6c9aa6ecd71d 3 months ago 456MB postgres 16.4 ced3ad69d60c 3 months ago 453MB
イメージのサイズがわかっても、全体像はわかりません。イメージ内に何が含まれているか、レイヤーがいくつあるか、各レイヤーの大きさはわかりません。Dockerイメージ レイヤーは、読み取り専用の不変のファイル システム レイヤーで、Docker イメージのコンポーネントです。各レイヤーは、ファイルの追加、構成の変更、ソフトウェアのインストールなど、イメージのファイル システムに加えられた一連の変更を表します。
Docker イメージはレイヤーごとに増分的に構築され、各レイヤーはDockerfile
内の命令に対応します。イメージのレイヤーを取得するには、 docker history
コマンドを実行します。
➜ docker history kamal-dashboard:latest IMAGE CREATED CREATED BY SIZE COMMENT 673737b771cd 4 days ago CMD ["./bin/thrust" "./bin/rails" "server"] 0B buildkit.dockerfile.v0 <missing> 4 days ago EXPOSE map[80/tcp:{}] 0B buildkit.dockerfile.v0 <missing> 4 days ago ENTRYPOINT ["/rails/bin/docker-entrypoint"] 0B buildkit.dockerfile.v0 <missing> 4 days ago USER 1000:1000 0B buildkit.dockerfile.v0 <missing> 4 days ago RUN /bin/sh -c groupadd --system --gid 1000 … 54MB buildkit.dockerfile.v0 <missing> 4 days ago COPY /rails /rails # buildkit 56.2MB buildkit.dockerfile.v0 <missing> 4 days ago COPY /usr/local/bundle /usr/local/bundle # b… 153MB buildkit.dockerfile.v0 <missing> 4 days ago ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1… 0B buildkit.dockerfile.v0 <missing> 4 days ago RUN /bin/sh -c apt-get update -qq && apt… 137MB buildkit.dockerfile.v0 <missing> 4 days ago WORKDIR /rails 0B buildkit.dockerfile.v0 <missing> 3 weeks ago CMD ["irb"] 0B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; mkdir "$GEM_HOME";… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV PATH=/usr/local/bundle/bin:/usr/local/sb… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV GEM_HOME=/usr/local/bundle 0B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; savedAptMark="$(a… 78.1MB buildkit.dockerfile.v0 <missing> 3 weeks ago ENV RUBY_DOWNLOAD_SHA256=018d59ffb52be3c0a6d… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV RUBY_DOWNLOAD_URL=https://cache.ruby-lan… 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV RUBY_VERSION=3.4.1 0B buildkit.dockerfile.v0 <missing> 3 weeks ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; mkdir -p /usr/loca… 19B buildkit.dockerfile.v0 <missing> 3 weeks ago RUN /bin/sh -c set -eux; apt-get update; a… 43.9MB buildkit.dockerfile.v0 <missing> 3 weeks ago # debian.sh --arch 'arm64' out/ 'bookworm' '… 97.2MB debuerreotype 0.15
イメージとレイヤーに関する理論はすでに説明したので、次はDockerfile
について見ていきましょう。Rails 7.1 以降では、新しい Rails アプリケーションでDockerfile
が生成されます。以下は Dockerfile の例です。
# syntax=docker/dockerfile:1 # check=error=true # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.4.1 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails # Install base packages # Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Set production environment ENV RAILS_ENV="production" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" # Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build gems RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ bundle exec bootsnap precompile --gemfile # Copy application code COPY . . # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile # Final stage for app image FROM base # Copy built artifacts: gems, application COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ chown -R rails:rails db log storage tmp USER 1000:1000 # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start server via Thruster by default, this can be overwritten at runtime EXPOSE 80 CMD ["./bin/thrust", "./bin/rails", "server"]
以下では、最終的なイメージ サイズを効率的にするために上記のDockerfile
に適用されたアプローチとルールのリストを示します。
ローカル開発マシンには必要なソフトウェアだけを保存しているはずです。同じことが Docker イメージにも当てはまります。以下の例では、上記の Rails Dockerfile から抽出した Dockerfile を常に悪化させています。これを元のDockerfile
バージョンとして参照します。
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
ベース イメージはDockerfile
の開始点です。コンテナを作成するために使用されるイメージです。ベース イメージはDockerfile
の最初のレイヤーであり、 Dockerfile
自体によって作成されない唯一のレイヤーです。
ベース イメージは、 FROM
コマンドで指定され、その後にイメージ名とタグが続きます。タグはオプションであり、指定されていない場合はlatest
タグが使用されます。ベース イメージは、Docker Hub またはその他のレジストリで利用可能な任意のイメージにすることができます。
Dockerfile
about では、 3.4.1-slim
タグ付きのruby
イメージを使用しています。ruby イメージはruby
Docker Hub で入手できる公式 Ruby イメージです。3.4.1 3.4.1-slim
タグは、 debian-slim
イメージをベースにした Ruby イメージのスリム バージョンです。debian debian-slim
イメージは、サイズが最適化された Debian Linux イメージの最小バージョンです。以下の表を見ると、 slim
イメージがどれだけ小さいかがわかります。
➜ docker images --filter "reference=ruby" REPOSITORY TAG IMAGE ID CREATED SIZE ruby 3.4.1-slim 0bf957e453fd 5 days ago 219MB ruby 3.4.1-alpine cf9b1b8d4a0c 5 days ago 99.1MB ruby 3.4.1-bookworm 1e77081540c0 5 days ago 1.01GB
2024 年 1 月現在、現在の Debian リリースはbookwormと呼ばれ、以前のリリースはbullseye と呼ばれています。
1GB ではなく 219 MB です。これは大きな違いです。しかし、 alpine
イメージがさらに小さかったらどうなるでしょうか。 alpine
イメージは、サイズとセキュリティが最適化された超軽量 Linux ディストリビューションである Alpine Linux ディストリビューションに基づいています。 Alpine は、 glibc
の代わりにmusl
ライブラリを使用し、GNU の代わりにbusybox
(Unix ユーティリティのコンパクトなセット) を使用します。 alpine
イメージを使用して Rails を実行することは技術的には可能ですが、この記事では取り上げません。
RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives
Dockerfile
内の各RUN
、 COPY
、 FROM
命令は新しいレイヤーを作成します。レイヤーの数が増えるほど、イメージのサイズが大きくなります。このため、複数のコマンドを 1 つのRUN
命令にまとめることがベストプラクティスです。この点を説明するために、以下の例を見てみましょう。
# syntax=docker/dockerfile:1 # check=error=true # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.4.1 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base RUN apt-get update -qq RUN apt-get install --no-install-recommends -y curl RUN apt-get install --no-install-recommends -y libjemalloc2 RUN apt-get install --no-install-recommends -y libvips RUN apt-get install --no-install-recommends -y libpq-dev RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives CMD ["echo", "Whalecome!"]
RUN
命令を複数の行に分割しました。これにより、人間にとってより読みやすくなります。しかし、イメージのサイズにはどのような影響があるでしょうか? イメージをビルドして確認してみましょう。
➜ time docker build -t no-minimize-layers --no-cache -f no-minimize-layers.dockerfile . 0.31s user 0.28s system 2% cpu 28.577 total
イメージの構築には 28 秒かかりましたが、最小化されたレイヤーを使用した元のバージョンの構築には 19 秒しかかかりませんでした (約 33% 高速化)。
➜ time docker build -t original --no-cache -f original.dockerfile . 0.25s user 0.28s system 2% cpu 19.909 total
画像のサイズを確認してみましょう。
➜ docker images --filter "reference=*original*" --filter "reference=*no-minimize*" REPOSITORY TAG IMAGE ID CREATED SIZE original latest f1363df79c8a 8 seconds ago 356MB no-minimize-layers latest ad3945c8a8ee 43 seconds ago 379MB
最小化されたレイヤーを含むイメージは、最小化されたレイヤーを含まないイメージよりも 23 MB 小さくなります。これは、 6% のサイズ削減です。この例では小さな差のように見えますが、すべてのRUN
命令を複数の行に分割すると、差はさらに大きくなります。
デフォルトでは、 apt-get install
推奨パッケージとインストールを要求したパッケージをインストールします。-- --no-install-recommends
オプションは、 apt-get
に明示的に指定されたパッケージのみをインストールし、推奨パッケージはインストールしないように指示します。
➜ time docker build -t without-no-install-recommends --no-cache -f without-no-install-recommends.dockerfile . 0.33s user 0.30s system 2% cpu 29.786 total ➜ docker images --filter "reference=*original*" --filter "reference=*recommends*" REPOSITORY TAG IMAGE ID CREATED SIZE without-no-install-recommends latest 41e6e37f1e2b 3 minutes ago 426MB minimize-layers latest dff22c85d84c 17 minutes ago 356MB
ご覧のとおり、 --no-install-recommends
なしのイメージは元のイメージより 70 MB 大きくなっています。これは16% のサイズ増加です。
diveユーティリティを使用して、イメージに追加されたファイルを確認します。詳細については、記事の最後を参照してください。
元のDockerfile
はapt-get install
コマンドの後にrm -rf /var/lib/apt/lists/* /var/cache/apt/archives
コマンドが含まれています。このコマンドは、インストール後に不要になったパッケージ リストとアーカイブを削除します。これがイメージ サイズにどのように影響するかを確認しましょう。これを実現するために、クリーニング コマンドなしで新しいDockerfile
作成します。
RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev
イメージの構築には元のイメージとほぼ同じ時間がかかりますが、これは当然のことです。
➜ time docker build -t without-cleaning --no-cache -f without-cleaning.dockerfile . 0.28s user 0.30s system 2% cpu 21.658 total
画像のサイズを確認してみましょう。
➜ docker images --filter "reference=*original*" --filter "reference=*cleaning*" REPOSITORY TAG IMAGE ID CREATED SIZE without-cleaning latest 52884fe50773 2 minutes ago 375MB original latest f1363df79c8a 16 minutes ago 356MB
クリーニングなしの画像はクリーニングありの画像よりも 19 MB 大きくなり、サイズが 5% 増加します。
上記の 4 つの最適化がすべて適用されていない場合はどうなるでしょうか?最適化なしで新しいDockerfile
を作成し、イメージをビルドしてみましょう。
# syntax=docker/dockerfile:1 # check=error=true ARG RUBY_VERSION=3.4.1 FROM docker.io/library/ruby:$RUBY_VERSION AS base RUN apt-get update -qq RUN apt-get install -y curl RUN apt-get install -y libjemalloc2 RUN apt-get install -y libvips RUN apt-get install -y libpq-dev CMD ["echo", "Whalecome!"]
➜ time docker build -t without-optimizations --no-cache -f without-optimizations.dockerfile . 0.46s user 0.45s system 1% cpu 1:02.21 total
うわー、イメージの構築に1分以上かかりました。
➜ docker images --filter "reference=*original*" --filter "reference=*without-optimizations*" REPOSITORY TAG IMAGE ID CREATED SIZE without-optimizations latest 45671929c8e4 2 minutes ago 1.07GB original latest f1363df79c8a 27 hours ago 356MB
最適化されていないイメージは元のイメージよりも 714 MB 大きく、サイズが 200% 増加しています。これは、 Dockerfile
最適化することがいかに重要であるかを明確に示しています。イメージが大きいほど、ビルドに時間がかかり、消費するディスク容量も多くなります。
.dockerignore
ファイルは、Git で使用される.gitignore
ファイルに似ています。ビルドのコンテキストからファイルとディレクトリを除外するために使用されます。コンテキストとは、イメージをビルドするときに Docker デーモンに送信されるファイルとディレクトリのセットです。コンテキストは tarball として Docker デーモンに送信されるため、できるだけ小さく保つことが重要です。
何らかの理由でプロジェクトに.dockerignore
ファイルがない場合、手動で作成できます。出発点として、公式の Rails .dockerignore
ファイル テンプレートを使用することをお勧めします。以下は、その例です。
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. # Ignore git directory. /.git/ /.gitignore # Ignore bundler config. /.bundle # Ignore all environment files. /.env* # Ignore all default key files. /config/master.key /config/credentials/*.key # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore pidfiles, but keep the directory. /tmp/pids/* !/tmp/pids/.keep # Ignore storage (uploaded files in development and any SQLite databases). /storage/* !/storage/.keep /tmp/storage/* !/tmp/storage/.keep # Ignore assets. /node_modules/ /app/assets/builds/* !/app/assets/builds/.keep /public/assets # Ignore CI service files. /.github # Ignore development files /.devcontainer # Ignore Docker-related files /.dockerignore /Dockerfile*
プロジェクトに.dockerfile
ファイルがあると、コンテキストから不要なファイルやディレクトリ (例: .github
フォルダーの GitHub ワークフローやnode_modules
の JavaScript 依存関係) を除外できるだけでなく、イメージに機密情報を誤って追加するのを防ぐのにも役立ちます。たとえば、環境変数を含む.env
ファイルや、資格情報の暗号化解除に使用されるmaster.key
ファイルなどです。
上記の最適化はすべて、説明すれば明らかなように思えるかもしれません。すでに巨大な画像があり、どこから始めればよいかわからない場合はどうすればよいでしょうか。
私のお気に入りで最も便利なツールはDiveです。Dive は、Docker イメージ、レイヤーのコンテンツを調べ、イメージ サイズを縮小する方法を見つけるための TUI ツールです。Dive は、システム パッケージ マネージャーを使用してインストールすることも、公式の Docker イメージを使用して実行することもできます。最悪のシナリオのイメージを使用しましょう。
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations
上のスクリーンショットでは、最も最適でない画像の検査を見ることができます。Dive は、各レイヤーのサイズ、画像の合計サイズ、および各レイヤーで変更された (追加、変更、または削除された) ファイルを表示します。私にとって、これは Dive の最も便利な機能です。右側のパネルにファイルをリストすることで、不要なファイルを簡単に識別し、それらを画像に追加するコマンドを削除できます。
Dive の本当に気に入っている点は、ターミナル UI があることに加えて、CI に適した出力も提供できることです。これは、ローカル開発でも効果的です。これを使用するには、 CI
環境変数をtrue
に設定して Dive を実行します。コマンドの出力は以下のスクリーンショットのようになります。
docker run -e CI=true --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations
私の個人的な好みとしては、イメージが良好な状態であることを確認するために、たとえば週に 1 回など、スケジュールに従って Dive を使用することです。今後の記事では、Dive やHadolintなど、Dockerfile をチェックするために使用する自動化されたワークフローについて説明します。
私が目にしたイメージ サイズを最小化する 1 つの方法は、レイヤーを圧縮することです。複数のレイヤーを 1 つのレイヤーに結合してイメージ サイズを縮小するというアイデアです。Docker には実験的なオプション--squash
があり、これ以外にもdocker-squashなどのサードパーティ ツールがありました。
このアプローチは過去には機能していましたが、現在は非推奨であり、使用は推奨されていません。レイヤーを圧縮すると、レイヤー キャッシュという Docker の基本的な機能が破壊されます。それだけでなく、 --squash
使用すると、最終イメージに以前のレイヤーの機密ファイルや一時ファイルが意図せず含まれる可能性があります。これは、きめ細かな制御が欠けている、オール オア ナッシングのアプローチです。
レイヤーを圧縮する代わりに、マルチステージ ビルドを使用することをお勧めします。Rails Dockerfile
すでにマルチステージ ビルドが使用されていますが、次の記事でその仕組みを説明します。
Docker イメージの最適化は、他の最適化と同様に、一度実行して忘れることはできません。これは、定期的なチェックと改善を必要とする継続的なプロセスです。基本的な内容について説明しようとしましたが、知って理解しておくことは非常に重要です。次の記事では、Docker ビルドをより高速かつ効率的にするのに役立つ、より高度なテクニックとツールについて説明します。