74
61

More than 3 years have passed since last update.

【Dockerfile全解説】Rails本番環境のための一番シンプルなDockerイメージを作る

Last updated at Posted at 2020-02-03

何度か Rails の本番環境を Docker で構築してきて、イメージの設計図ともいえる Dockerfile も段々と洗練され、ここしばらくで一番シンプルなものに落ち着いた。シンプルでありながら、イメージサイズをコンパクトに、またビルド時間もなるべく抑えられるバランスの良い構成にできたので、紹介しようと思う。

方針

シンプルな Dockerfile を目指しつつも、十分実用(本番環境用)として使えることを意識している。具体的には、以下のような方針:

  • Dockerfileをなるべくシンプルに保つ → 読みやすく、保守しやすいように
    • 命令は基本的に最低限かつ素直に書いて、Docker特有の「トリック」は必要なときにだけ使う
    • Dockerfileが複雑になりがちなマルチステージビルドなどは、今回は不採用
  • イメージサイズをなるべく小さくする(が、最小にはこだわらない) → デプロイ時間の短縮、ストレージの節約のため
    • 不要なパッケージやキャッシュ等は極力削除する
    • ただし、Dockerfileが複雑になる場合は、イメージサイズよりDockerfileのシンプルさを優先
    • 参考: rails newしただけのプロジェクトのイメージサイズは415MB(詳細は後述)
  • ビルドキャッシュを効きやすくする → ビルド時間の短縮のため
    • ビルドに一番時間がかかるOSパッケージやgemのインストールは極力スキップする(使用するgemに変更がない場合)
  • 実用でよく使うWebpacker、Active Storage等の機能も考慮する
    • アセットのプリコンパイル、ImageMagickのインストールなど

Dockerfile全体

まず Dockerfile の全体を以下に記載する。コメントや空行を除くと32行というシンプルな構成。それぞれの命令の詳しい解説は後述する。

※Ruby 2.6.5 / Rails 6.0 / PostgreSQL を前提としているが、依存パッケージ等を変更すれば他の環境でも利用可能なはず。

Dockerfile
FROM ruby:2.6.5-alpine

WORKDIR /app

ENV RAILS_ENV="production"
ENV NODE_ENV="production"

# 依存パッケージのインストール
COPY Gemfile Gemfile.lock /app/
RUN apk add --no-cache -t .build-dependencies \
    build-base \
    libxml2-dev\
    libxslt-dev \
 && apk add --no-cache \
    bash \
    file \
    imagemagick \
    libpq \
    libxml2 \
    libxslt \
    nodejs \
    postgresql-dev \
    tini \
    tzdata \
    yarn \
 && gem install bundler:2.0.2 \
 && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
 && apk del --purge .build-dependencies

# アプリケーションコードのコピー
COPY . /app

# アセットのプリコンパイル
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile \
 && yarn cache clean \
 && rm -rf node_modules tmp/cache

# ランタイム設定
ENV RAILS_SERVE_STATIC_FILES="true"
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "3000"]
EXPOSE 3000

Dockerfile全解説

ベースイメージと基本設定

FROM ruby:2.6.5-alpine

WORKDIR /app

ENV RAILS_ENV="production"
ENV NODE_ENV="production"

ベースイメージには、Docker 公式の Ruby イメージを使用。この例では Ruby のバージョンは 2.6.5 を使っているが、もちろん他のバージョンでも構わない。タグに -alpine とつくイメージは Alpine Linux ベースで、サイズが 50MB 程度とコンパクトなので、最終的なイメージサイズの削減に寄与する。

その他、簡単な基本設定をここで。アプリケーションコードは /app に置く想定なので、そこをワーキングディレクトリにする。本番環境向けに環境変数 RAILS_ENV, NODE_ENV を設定する。

依存パッケージのインストール

COPY Gemfile Gemfile.lock /app/
RUN apk add --no-cache -t .build-dependencies \
    build-base \
    libxml2-dev\
    libxslt-dev \
 && apk add --no-cache \
    bash \
    file \
    imagemagick \
    libpq \
    libxml2 \
    libxslt \
    nodejs \
    postgresql-dev \
    tini \
    tzdata \
    yarn \
 && gem install bundler:2.0.2 \
 && bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
 && apk del --purge .build-dependencies

冒頭の COPY で、bundle install に必要な Gemfile, Gemfile.lock をイメージにコピー。依存パッケージのインストールがビルドで最も時間がかかるフェーズなので、なるべくビルドキャッシュが効くように、必要最低限のファイルのみコピーする。

残りの行は、Docker 特有の「トリック」を使っている部分の1つで、大きな1つの RUN 命令になっている。ここでOSのパッケージインストールと、bundle installをすべて一気にやる。1つの命令にまとめているのは、bundle install した後、ランタイムに不要なOSパッケージをアンインストールしてイメージの容量を削減するため1 2

apk は Alpine Linux のパッケージマネージャ。apk でインストールするのは、ネイティブ拡張付きの gem をコンパイルするのに必要なコンパイラ群、PostgreSQL, Actvie Storage, Nokogiri 等に必要なコマンドやライブラリ、Webpack の実行に必要な Node.js など。-t オプションで複数のパッケージをまとめて「タグ」をつけることができ、後からそのタグを指定して一括アンインストールができて便利。

アプリケーションコードのコピー

COPY . /app

Rails アプリケーションのコード(. にある想定)をすべてイメージ内の /app にコピーする。ほとんどのケースでアプリケーションコードになんらかの変更があると考えられるため、このフェーズ以降はビルドキャッシュが効きにくい。

アセットのプリコンパイル

RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile \
 && yarn cache clean \
 && rm -rf node_modules tmp/cache

rails assets:precompile を実行する。Webpacker を使っている場合は、このときに yarn install や Webpack のビルドなども同時に行われる。SECRET_KEY_BASE は、適当な値をセットしておかないと Missing `secret_key_base` エラーになってしまう。

コンパイルが終わったら、不要なキャッシュと node_modules を消してしまう。node_modules は、アセットをビルドした後なら削除してOK。これでだいぶイメージの容量が減る。ここも「トリック」を使っている部分で、不要なファイルの削除までを1つの RUN 命令にまとめて、イメージサイズを抑えている。

ランタイム設定

ENV RAILS_SERVE_STATIC_FILES="true"
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "3000"]
EXPOSE 3000

環境変数 RAILS_SERVE_STATIC_FILES は、Rails が静的アセットも配信するかどうかの設定。このイメージ単体で Web サーバーとして完結するように、デフォルトでは ON にしている。

エントリポイントには tini を使っている。tini は軽量な init の実装で、ゾンビプロセスの回収やシグナルの転送を行なってくれるプログラムである3Mastodon で採用されているのを参考にした。

残りはデフォルトのコマンド(サーバー起動)の設定と、露出ポートの設定。

参考: ビルド時間とイメージサイズ

参考として、手元の環境で rails new しただけのプロジェクトを、上記の Dockerfiledocker build したときの結果を以下に記載する。

$ rails -v
Rails 6.0.2.1
$ rails new sample-rails-project --database=postgresql
$ cd sample-rails-project
$ cp /path/to/Dockerfile /path/to/.dockerignore .
$ time docker build --no-cache .
...
Successfully built c88f89af77e5
docker build --no-cache .  1.81s user 1.40s system 0% cpu 6:05.71 total
$ docker images | grep c88f89af77e5
<none>                                   <none>              c88f89af77e5        8 minutes ago       415MB

ビルド時間は、手元の環境では約6分だった。環境の詳細は以下を参照。

イメージサイズは415MBとなった。もちろん環境やインストールする gem、NPM パッケージなどによって変わってくるが、最小構成ではこのくらいコンパクトになる。

環境:

  • macOS 10.15
  • Docker Desktop 2.1.0.5
  • MacBook Pro 13-inch, 2016
    • Dual-Core Intel Core i7 3.3GHz
    • Memory 16GB

まとめ

シンプルでありながら、本番環境向けに十分実用できる Rails の Dockerfile を作成した。プロジェクトによっては不足するパッケージや処理などもありそうだが、シンプルなのでこれをベースにしたカスタマイズもしやすいと思う。

アプリケーションコードに変更があるとアセットのプリコンパイルが毎回必要だったり、まだランタイムに不要なパッケージを完全に除去できていないなどの課題はあるため、シンプルさを損なわない範囲でできることがあれば改善していきたい。マルチステージ化は方向性の一つと考えている。

参考資料


  1. Dockerのイメージサイズは、各レイヤー(COPY, RUNなどの命令ごとに1レイヤーが作られる)のサイズの合計になる。そのため、1レイヤーごとのサイズを小さく保つことが、最終的なイメージサイズの削減に寄与する。 

  2. 厳密にはnodejsyarnはアセットプリコンパイル以降は不要だが、プリコンパイルまで含めて1命令にしてしまうとビルドキャッシュが効きにくくなるため、今回はインストールしたままにしている。このあたりはマルチステージにするのがうまい解決策だろうか。 

  3. 余談だが、以下のIssueでtiniの作者がinitの役割を詳しく解説していて参考になる: https://github.com/krallin/tini/issues/8 

74
61
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
74
61