以下の通り、Rails v7.1 から新規アプリケーション生成時にDockerfileが生成されるようになりました。
2.1 新規RailsアプリケーションでDockerfileが生成されるようになった 新規Railsアプリケーションでは、デフォルトでDockerがサポートされるようになりました(#46762)。 新しいアプリケーションを生成すると、そのアプリケーションにDocker関連ファイルも含まれます。 https://railsguides.jp/7_1_release_notes.html#%E6%96%B0%E8%A6%8Frails%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7dockerfile%E3%81%8C%E7%94%9F%E6%88%90%E3%81%95%E3%82%8C%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F
今回は先日リリースされたRails v7.2で生成されるDockerfileがどんな感じなのか読んでみたいと思います。
Rails v7.2で生成されるDockerfile
Rails v7.2で生成されるDockerfileは以下のTemplateを元に生成されます。
$ rails -v Rails 7.2.0 $ rails new myapp
以下のようなDockerfileが生成されます。
# syntax = docker/dockerfile:1 # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: # docker build -t my-app . # docker run -d -p 80:80 -p 443:443 --name my-app -e RAILS_MASTER_KEY=<value from config/master.key> my-app # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.3.1 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails # Install base packages RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 libsqlite3-0 libvips && \ 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 git pkg-config && \ 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 the server by default, this can be overwritten at runtime EXPOSE 3000 CMD ["./bin/rails", "server"]
RailsのDockerfileのtemplateを読んでみる
RailsのDockerfileのtemplateは以下のようなマルチステージビルドを行うような構成になっています。
- base : イメージの指定と環境変数、ベースとなるライブラリのinstall
- build : gem/npmのinstall及びビルド
- final stage : ビルド成果物のコピーとカスタムユーザーへの権限追加、起動コマンドの指定
それぞれサラッとみていきます。
base
base部分のDockerfileは以下の通りです。
ARG RUBY_VERSION=<%= gem_ruby_version %> FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails # Install base packages RUN apt-get update -qq && \ apt-get install --no-install-recommends -y <%= dockerfile_base_packages.join(" ") %> && \ 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"
イメージにはruby:<version>-slim
のRubyのイメージが利用されます。Rubyに実行に必要な最低限のパッケージのみが含まれているためイメージサイズを小さくすることができます。
ruby:<version>-slim
This image does not contain the common packages contained in the default tag and only contains the minimal packages needed to run ruby. Unless you are working in an environment where only the ruby image will be deployed and you have space constraints, we highly recommend using the default image of this repository.
イメージに利用するRubyのバージョンはARG
でビルド時に指定可能になっていますが、
デフォルトではgem_ruby_versionが実行されRails newした際に利用しているRubyのバージョンが設定されます。
apt-get
でinstallされるライブラリはdockerfile_base_packagesで設定され、基本はcurl
とlibjemalloc2
がインストールされ、あとはActiveRecordを使う場合にはdatabase関連の必要なライブラリ、ActiveStrageを利用するならlibvips
がインストールされます。
その後は環境変数としてRAILS_ENV
及びBubdler周りの設定用の環境変数が設定されます。
build
build部分のDockerfileは以下の通りです。gemのinstallやアセットのビルド等を行い最終的なイメージに必要となる成果物を作成します。
# Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build gems<%= using_node? ? " and node modules" : "" %> RUN apt-get update -qq && \ apt-get install --no-install-recommends -y <%= dockerfile_build_packages.join(" ") %> && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives <% if using_node? -%> # Install JavaScript dependencies ARG NODE_VERSION=<%= node_version %> ARG YARN_VERSION=<%= dockerfile_yarn_version %> ENV PATH=/usr/local/node/bin:$PATH RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ /tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \ npm install -g yarn@$YARN_VERSION && \ rm -rf /tmp/node-build-master <% end -%> <% if using_bun? -%> ENV BUN_INSTALL=/usr/local/bun ENV PATH=/usr/local/bun/bin:$PATH ARG BUN_VERSION=<%= dockerfile_bun_version %> RUN curl -fsSL https://bun.sh/install | bash -s -- "bun-v${BUN_VERSION}" <% end -%> # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git<% if depend_on_bootsnap? -%> && \ bundle exec bootsnap precompile --gemfile<% end %> <% if using_node? -%> # Install node modules COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile <% end -%> <% if using_bun? -%> # Install node modules COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile <% end -%> # Copy application code COPY . . <% if depend_on_bootsnap? -%> # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ <% end -%> <% unless dockerfile_binfile_fixups.empty? -%> # Adjust binfiles to be executable on Linux <%= "RUN " + dockerfile_binfile_fixups.join(" && \\\n ") %> <% end -%> <% unless options.api? || skip_asset_pipeline? -%> # Precompiling assets for production without requiring secret RAILS_MASTER_KEY RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile <% end -%> <% if using_node? -%> RUN rm -rf node_modules <% end %>
まず先ほどのbase
をベースイメージとしてbuild
と名前をつけます。
そしてdockerfile_build_packagesで必要となるライブラリをインストールします。
基本はnative拡張等のgemをinstallするためのbuild-essential
,git
,pkg-config
がinstallされますが、Node.jsが必要となる場合にはnpmのnative modulesをinstallするためのnode-gyp
やpython-is-python3
もインストールされます。
Node.jsが必要となるかどうかは、using_node?で判定されており、実態はimportmapを利用せずにJavaScriptを利用するかで判定しています。
Node.jsを使った環境構築を行う場合にはnpm packageのinstallを行うために必要なpackage.json
とyarn.lock
をホストからコピーしyarn install
を行います。(bunを使う場合にはbun install
が行われる)
その後COPY . .
でホストからすべてのファイルをコピーし、bootsnap
に依存していればbootsnap precompile
を行いapp
,lim
のcacheを生成します。
dockerfile_binfile_fixups
での処理は、OS間でのbin
系ファイルの互換性を保持するようにしているようです。
https://github.com/rails/rails/pull/46944
その後Asset Pipelineを利用している場合にはSECRET_KEY_BASE_DUMMY=1
を指定して、bin/rails assets:precompile
を実行して必要なアセット類のビルドを行います。
ENV["SECRET_KEY_BASE_DUMMY"]
を設定すると、一時ファイルに保存されるランダム生成のsecret_key_baseが使われるようになります。これは、ビルド中にproduction用のsecretsにアクセスせずに、production用のアセットをプリコンパイルしたい場合に便利です。 https://railsguides.jp/asset_pipeline.html
最後にRUN rm -rf node_modules
を実行してビルドも終わり不要になったnode_modules
を削除しています。(最終的ステージでCOPY --from=build /rails /rails
をした際にnode_modules
が最終イメージに含まれないようにするため)
final stage
final stageのDockerfileは以下の通りです。buildで生成した成果物を取得し、カスタムユーザーに必要な権限を与え、起動コマンドを設定しています。
# 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 <%= dockerfile_chown_directories.join(" ") %> USER 1000:1000 # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start the server by default, this can be overwritten at runtime EXPOSE 3000 CMD ["./bin/rails", "server"]
COPY --from=build
でbundle installしたgemとprojectのrootディレクトリである/rails
をbuidステージから取得します。(ここで実行時に必要なファイルだけ取得してるので最終的なイメージサイズを削減できます)
その後、カスタムユーザーrails
を作成しchown
でdockerfile_chown_directoriesで取得したディレクトリの所有者を変更し権限を付与し、実行時のユーザーとしてrails
を設定します。
カスタムユーザー(non-root user)での実行は以下のPRによって導入されており、
Docker公式でもセキュリティ面で推奨されているようです。
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
が設定されイメージからコンテナが起動された際にdocker-entrypointが実行され、デフォルトではjemallocの有効化とbin/rails db:prepare
が行われます。
最後に3000番ポート公開し、デフォルトの実行コマンドとしてRailsの起動コマンドであるbin/rails server
を設定します。
おわりに
RailsのDockerfileを見てみましたが、今までおまじない的にインストールしていたライブラリ群をRails公式が整理してくれたことにより自信を持てるようになったのと、並列実行のためにbundle installとnpmはステージを分けたりとカスタマイズのしがいはありそうですが、マルチステージビルドでいい感じに最終的なイメージサイズを削減してくれていたり初期の段階では十分そうな印象を持ちました。
また、Railsで本番デプロイする際のDockerfileがテンプレートとして管理されるようになったことによって、今まで各社で個別に行われていたDockerfile周りの知見が集まっていくのは非常にありがたいなと思いました🙏
おわり