やってみたこと
RailsでWebサービスを行うプロジェクトに参画したがどんな風にプロダクション用のDocker化をするのが良いか勉強がてらやってみた。
これを書いてる人のスペック
ここ10年ぐらいの RubyとRailsの文化を知らない。出始めにちょっと触ったことはあるが、すごく流行っていた時期にはほとんど触れる機会がなく、今更だけどRailsを勉強中。
- 実はRailsによるアジャイルWebアプリケーション開発の初版本は読んでいた。
- プロダクションレベルでRailsアプリの開発運用経験なし。(ゼロ年代にPoCや簡易ツールの為には利用あり)
- gemパッケージを一つだけ登録したことがあるがゼロ年代の話(対象Adobeのサービスは終了済み)。
- Railsで書かれているRedmineからGitLabのプロジェクト移行ツールへのコントリビュートはしたが、このツール自体はRubyでなくPythonで書かれている。
- 現在Railsで書かれたコードを読んでいるが、
webpacker
(使われなくなるらしいが)とかsidekiq
とかその他便利な周辺ツールが盛り込まれていることを最近知った。 - AWSやAzureでPythonによるバックエンドやETL処理を書きTerraform/Serverless Frameworkでインフラを作る仕事がここ最近のメイン。CI/CDパイプラインやDockerfileなども書く。
環境構成
- 開発環境: M1 Mac
- Ruby 3.0 + Rails 6 + Node.js v16(LTS)
- Ruby 3.1 は 3.2で廃止予定のメソッドをRails側が対応していないようで警告が出たり、標準ライブラリーのgem化があったり…。なので2.6->2.7->3.0->3.1で試したが、無難そうな3.0に落ち着いた
- Rails 7 は webpackerの非標準化などの対応が面倒なので Rails 6 にした
- Ruby 2.6, 2.7 + Rails 6 でも大体同じ感じで動くとおもう(最初は2.6,2.7で試していた)
- Node.jsを入れてる通り、APIモードではなく、WebUI付きでWebPackしたいから
Dockerfile例
概要
- マルチステージビルドで構築する
- 余計なものはなるべく入れない
- Alpineは小さくなるがCライブラリーがmuslの件もあるので避けた(Rubyでの状況は知らないが他の言語で面倒なことがあったので)
ビルド
ビルド用イメージ準備
まずビルド用のイメージとして、nodeイメージからnode/yarnを持ってきて、rubyイメージのbuild-essential入りと合体させる。これで、gcc/g++などでnative extensionsをビルドでき、yarnでnpmパッケージを取得しトランスパイル等が可能になる。
FROM node:16.16.0-slim as node
FROM ruby:3.0.4 as builder
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn
ビルド実行
- gemのインストール
-
bundle install
にて/usr/local/bundle/
へgemパッケージ群をインストール - C言語等で書かれたnative extentionsはビルドされる
-
- JavaScriptとCSSなどの作成
-
webpacker:compile
とassets:precompile
にてpublic/packs
,public/assets
へトランスパイルと最小化されたJavaScriptやCSSなどがインストールされる
-
WORKDIR /src
# Copy source code
COPY . /src
# Ruby gem packages
RUN bundle install
# Node.js packages & webpack
RUN yarn install && \
bin/rails webpacker:compile && \
bin/rails assets:precompile
実行に必要なものをまとめる
Rubyコードと設定、上記でビルドされたJavaScript/CSSを一旦/dist
にまとめる。builderイメージからそれぞれをコピーしても良いが、後述の実際のイメージにコピーする際にまとめておいた方が楽なのでこうした。
# Create runtime distribution
RUN mkdir -p /dist && \
cp -pr Gemfile Gemfile.lock Rakefile config.ru app bin config db lib public \
/dist/
実行イメージ
OS・パッケージ基本環境
ビルド時のイメージではgcc/g++などの開発ツール入りのイメージを利用したが、実行時にコンパイラーなどの開発ツールは不要だし、JavaScript/CSSも変換済みなのでnode/yarnも不要、ruby:x.y.z-slim
のRubyだけのslim
イメージを利用する。
また、SQLite3, MySQL, PostgreSQLが主に使われる3大データベースだと思うが、それぞれのgemにはネイティブのライブラリーが必要になる。また、mysqlやpsqlなどのCLIでSQLのアクセスを行う必要があるならmysql-client, postgresql-clientなを入れる必要がある。(APKやRPM系は調べてません)。まあ、ここはケチってlib*-dev
でなく*-client
を入れれば良いですかね?
Database | gem | Library (apt) | SQL client (apt) |
---|---|---|---|
SQLite3 | sqlite3 | libsqlite3-dev | sqlite3 |
MySQL | mysql2 | default-libmysqlclient-dev | default-mysql-client |
PostgreSQL | pg | libpq-dev | postgresql-client |
とりあえず単純なWeb+DBのプロジェクトで試したが、その他のネイティブライブラリーを参照するgemを使ったプロジェクトではlibfoo-dev
などのインストールも必要。
FROM ruby:3.0.4-slim
RUN apt-get update && apt-get install -y \
libsqlite3-dev \
default-libmysqlclient-dev \
libpq-dev # sqlite3 postgresql-client default-mysql-client
ビルド済みのものをコピー
上に書いた通り、builderイメージの/usr/local/bundle
にgem群、/dist
にRubyコードや設定ファイル、変換済みJavaScript/CSSがあるので、こちらのイメージにコピーし、tmp
やlog
などの空ディレクトリーを作成しておく。
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /dist /app
RUN mkdir log storage tmp
エントリーポイントとサーバー実行
ここは、Docker公式ドキュメントある通りにしてみる。(tmp/pids/server.pid
を消すentriypoint.sh
)
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["/app/bin/rails", "server", "-b", "0.0.0.0"]
Dockerfileまとめ
こんな感じか? nodeとrubyのバージョンは適宜変更し、ライブラリーのパッケージも適宜追加変更。
#
# Builder image
# = Ruby + build-essential(gcc/g++) for native extensions
# + Node.js for webpack
#
FROM node:16.16.0-slim as node
FROM ruby:3.0.4 as builder
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn
WORKDIR /src
# Copy source code
COPY . /src
# Ruby gem packages
RUN bundle install
# Node.js packages & webpack
RUN yarn install && \
bin/rails webpacker:compile && \
bin/rails assets:precompile
# Create runtime distribution
RUN mkdir -p /dist && \
cp -pr Gemfile Gemfile.lock Rakefile config.ru app bin config db lib public \
/dist/
#
# Runtime image
#
FROM ruby:3.0.4-slim
RUN apt-get update && apt-get install -y \
libsqlite3-dev \
default-libmysqlclient-dev \
libpq-dev # sqlite3 postgresql-client default-mysql-client
WORKDIR /app
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder /dist /app
RUN mkdir log storage tmp
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["/app/bin/rails", "server", "-b", "0.0.0.0"]
その他
運用向け環境変数
-
RAILS_ENV
: development, productionなどの環境を渡す。 -
RAILS_SERVE_STATIC_FILES
: 開発環境ではnode/yarnでJavaScript/CSSをその場でビルドする様だが、上記の様にproduction用実行環境には入れないと思うので、この値をtrue
にすることにより、事前生成済みのファイルを配信する。 -
RAILS_LOG_TO_STDOUT
: productionだとデフォルトでログを吐かないし、コンテナ内のlog
に吐かれても困るので標準出力に出したい。この値をtrue
にする。 -
DATABASE_URL
: コンテナ内のconfig/database.yml
にusername
password
などを直接クレデンシャル情報を記述するのも良くないのでこちらで渡す(database.yml
よりも環境変数が優先)- MySQL:
mysql2://username:password@mysql:3306/database?encoding=utf8mb4
(mysql
でなくgemのmysql2
であることに注意) - PostgreSQL:
postgres://username:password@postgres:5432/database?encoding=unicode
- MySQL:
bundle -> Gemfile.lock
M1/M2 Macなどarm64アーキテクチャで開発するが、実行環境はx86_64/amd64ということが多いと思う。すると次のようなlockファイルになってしまうので、
...
nokogiri (1.13.8-arm64-darwin)
...
PLATFORMS
arm64-darwin-21
$ bundle config set force_ruby_platform true
を実行し(~/.bundle/config
に反映)。プラットフォームをrubyに強制するとCPUアーキテクチャの情報が入らないのでDocker環境には良いと思われる。
...
nokogiri (1.13.8)
...
PLATFORMS
ruby
その他
あと、なんかヒントになることあったら教えてください。