Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

更新情報

  • 2019/09/06: 初版公開
  • 2021/04/20: 原文更新を反映
  • 2022/04/07: 原文更新のため全文を再翻訳
  • 2023/11/09: 原文更新(2.0.3 (2023-09-21))を反映

参考: 2021/04/20の旧版記事は以下で参照できます。

[旧版]クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

訳注

Ruby on Whalesは、Ruby on Railsの他に、もしかすると「Boy on a Dolphin」にかけているのかもしれないと思いました。Whales(クジラ)はもちろんDockerのシンボルです。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(更新翻訳)

本記事では、著者がRuby on Railsプロジェクトの開発に用いているDockerの設定を紹介いたします。この設定はEvil Martiansのproduction development環境で生まれ、さらに進化を遂げたものです。本記事の内容の利用や共有はご自由にどうぞ。存分に楽しみましょう!

原文お知らせ

英語版記事は最新の推奨事項に合わせて更新を繰り返しています。詳しくは本記事末尾のChangelogをご覧ください(参考: 原文Changelog)。

さて、どこからお話を始めましょうか。ここに到達するまでに長い長い旅路をたどりました。かつて私は開発にVagrantを使っていましたが、当時のVMは私の4GB RAMのノートPCでは少々重すぎました。そして2017年にコンテナへの乗り換えを決意したときに、やっとDockerを使い始めました。

しかしDockerで問題がたちまち解決したという気持ちではありません。自分自身やチーム、そしてすべての人々にとって完璧な設定を追求し続けてきましたが、「これでよし」と言える究極の設定はありません。標準的なアプローチを見出すまでにかなりの時間を要しました(2019年に本記事を最初に公開した時点でも相当の時間を費やしていました)。

本記事を最初に公開して私の秘密を隅々までオープンにして以来、多くのRailsチームや開発者が私の手法を採用し、さらに改良や貢献にもご協力をいただきました。

前置きはこのぐらいにして、いよいよ設定そのものをご覧に入れたいと思います。設定のほぼすべての行に解説を付けています(「Dockerをわかっている」前提のわかりにくいチュートリアルにしたくなかったので)。

本記事は元々RailsConf 2019『“Terraforming legacy Rails applications”』での私の発表に合わせて公開されました。


本記事のソースコードは、GitHubのevilmartians/ruby-on-whalesでご覧いただけます。

evilmartians/ruby-on-whales - GitHub


その前に、この設定例では以下のような最新バージョンのソフトウェアを使っている点にご留意ください。

  • Docker Desktop 20.10以降
  • Docker Compose v2
  • Ruby 3.1.0
  • PostgreSQL 14
  • etc.

本記事の大半はコメント付きコードと設定例で構成されており、以下のようになっています。

🔗 基本的な設定

🔗 Dockerfile

Dockerfileは、Rubyアプリケーションの環境を定義します。この環境でサーバーを実行したり、rails cでコンソールを実行したり、テストやrakeタスクを走らせたり、その他にも開発者としてあらゆる形でのコードとのやりとりを行います。

# syntax=docker/dockerfile:1

ARG RUBY_VERSION
ARG DISTRO_NAME=bullseye

FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME

ARG DISTRO_NAME

# 共通の依存関係
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  rm -f /etc/apt/apt.conf.d/docker-clean; \
  echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \
  apt-get update -qq \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    curl \
    less \
    git

# PostgreSQLの依存関係をインストール
ARG PG_MAJOR
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
    gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg \
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" \
    $DISTRO_NAME-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    libpq-dev \
    postgresql-client-$PG_MAJOR

# NodeJSとYarnをインストール
ARG NODE_MAJOR
ARG YARN_VERSION=latest
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    --mount=type=tmpfs,target=/var/log \
    apt-get update && \
    apt-get install -y curl software-properties-common && \
    curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
    echo "deb https://deb.nodesource.com/node_${NODE_MAJOR}.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/nodesource.list && \
    apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends nodejs
RUN npm install -g yarn@$YARN_VERSION

# アプリケーションの依存関係をインストール
# 外部のAptfileを利用して行う(後述)
COPY Aptfile /tmp/Aptfile
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    $(grep -Ev '^\s*#' /tmp/Aptfile | xargs)

# Bundlerの設定
ENV LANG=C.UTF-8 \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3

# Bundlerの設定をプロジェクトのルートフォルダに保存する
ENV BUNDLE_APP_CONFIG=.bundle

# `bin/`や`bundle exec`をプレフィックスせずに
# binstubを実行したい場合は以下のコメントを解除する
# ENV PATH /app/bin:$PATH

# RubyGemをアップグレードして最新のBundlerをインストールする
RUN gem update --system && \
    gem install bundler

# アプリケーションコード用のディレクトリを作成する
RUN mkdir -p /app
WORKDIR /app

# ポート3000を公開することを明示する
EXPOSE 3000
# デフォルトコマンドをBashにする
CMD ["/bin/bash"]

この設定には必要なものしか含まれていないので、これを土台にできます。設定内容をもう少し詳しく解説します。

以下の3行は少々見慣れないかもしれません。

ARG RUBY_VERSION
ARG DISTRO_NAME=bullseye
FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME

いつものようにFROM ruby:3.1.0などでその時点の安定版Rubyを指定すればよさそうなものなのに、そうしない理由がおわかりでしょうか?その答えは、Dockerfileを一種のテンプレートとして利用し、環境をDockerfileの外部で設定できるようにしたいからです。

  • 実行時における依存関係の正確なバージョンはdocker-compose.ymlで指定します(後述👇)。
  • aptコマンドで追加インストール可能な依存関係のリストは別ファイルに保存します(これも後述👇👇)。

さらに、Debianのリリースもパラメータで指定可能にし(デフォルトはbullseye)、PostgreSQLなどの他の依存関係と整合する正しい取得元を確実に追加するようにしています。

さて、FROMステートメントでは以下のように引数を再度宣言していることにご注目ください。

FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME
ARG DISTRO_NAME

ここがDockerfileのしくみの厄介な点で、FROM以降では引数がリセットされてしまうので再宣言が必要になります。詳しくはmobyのissue #34129を参照してください。

上に続くファイルの残りの部分で、実際のビルド手順が記述されています。ここではコンテナサイズを小さくするためにslimベースのDockerイメージを使っているので、最初に一般的なシステム依存関係(gitやcurlなど)をいくつか手動でインストールしておく必要があります。

# 共通の依存関係
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    curl \
    less \
    git

システムの依存関係のインストールについて詳しくは、この後アプリケーション固有の依存関係で合わせて説明します。

aptコマンドでPostgreSQLやNodeJSをインストールするには、それらのdebパッケージをソース一覧に追加しておく必要があります。

以下はPostgreSQLのインストールです(公式ドキュメントに基づいています)。

ARG PG_MAJOR
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
    gpg --dearmor -o /usr/share/keyrings/postgres-archive-keyring.gpg \
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/postgres-archive-keyring.gpg] https://apt.postgresql.org/pub/repos/apt/" \
    $DISTRO_NAME-pgdg main $PG_MAJOR | tee /etc/apt/sources.list.d/postgres.list > /dev/null
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    libpq-dev \
    postgresql-client-$PG_MAJOR

このDockerfileはDocker Composeなしでの利用を想定していないので、PG_MAJOR引数にはデフォルト値を指定していません(後述のNODE_MAJORYARN_VERSIONも同様)。

また、上のコードではDockerfileの冒頭で定義したDISTRO_NAME引数が再利用されている点にもご注目ください。

さらに、ここでもapt-get ... apt-get cleanという呪文を再度唱えます。そうする理由は、環境の主要な部分が独立した形で構築されるようにしたいからです(こうしておくと、アップグレード時にDockerのキャッシュレイヤがよく効くようになります)。

以下はNodeJSのインストールです(NodeSourceリポジトリより)。

ARG NODE_MAJOR
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    --mount=type=tmpfs,target=/var/log \
    apt-get update && \
    apt-get install -y curl software-properties-common && \
    curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
    echo "deb https://deb.nodesource.com/node_${NODE_MAJOR}.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/nodesource.list && \
    apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends nodejs

続いてYarnをNPMでインストールします。

ARG YARN_VERSION
RUN npm install -g yarn@$YARN_VERSION

NodeJSとYarnを最初の時点でDockerfile経由でインストールしている理由がおわかりでしょうか?Rails 7ではimport mapsや(プリコンパイル済みバイナリを用いる)tailwindcss-railsを用いることで、NodeJSなしのセットアップが使えるようになりますが、ここでNodeJSとYarnを追加しておけば、従来のレガシーパイプラインのサポートや最新のWebpackerオルタナティブを追加するという選択肢を増やせます。

お次はアプリケーション固有の依存関係をインストールします。

COPY Aptfile /tmp/Aptfile
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    $(grep -Ev '^\s*#' /tmp/Aptfile | xargs)

Aptfileを使った小技について少し補足しておきます。

COPY Aptfile /tmp/Aptfile
RUN apt-get install\
    $(grep -Ev '^\s*#' /tmp/Aptfile | xargs)

このアイデアは、Herokuに追加パッケージをインストールできるheroku-buildpack-aptから拝借しました。このビルドパックを使うと、ローカル環境とproduction環境で同じAptfileを再利用することも可能になります。

デフォルトのAptfileには以下のパッケージが1個あるだけです(Railsのcredentialを編集するときにVimを使えるようにするため)。

vim

なお、著者が以前担当したプロジェクトでは、LaTeXとTexLiveでPDFを生成していました。この場合のAptfileは以下のような感じになるでしょう。

vim
# TeXパッケージ
texlive
texlive-latex-recommended
texlive-fonts-recommended
texlive-lang-cyrillic

このように依存関係のリストをAptfileに切り出しておくことで、タスク固有の依存関係を別ファイルに保存できるようになり、Dockerfileの普遍性が高まります。

DEBIAN_FRONTEND=noninteractiveという記述についてはAsk Ubuntuの回答をご覧いただければと思います。

--no-install-recommendsオプションを指定して推奨パッケージのインストールを無効にしておくと容量を節約でき、Dockerイメージのサイズも小さくなります。詳しくは以下の記事をご覧ください。

参考: Save disk space with apt-get option "no-install-recommends" in Xubuntu

どのRUNステートメントの冒頭部分もかなり謎な感じに見えますが、これも目的は同じく容量の節約で、取得したパッケージ ファイルのローカルリポジトリを、ビルドとビルドの間に保存されるキャッシュに移動するためのものです。この特定のDockerレイヤにゴミが残らないようにするには、パッケージをインストールするすべてのRUNステートメントにこのマジックを含めておく必要があります。これにより、Dockerイメージのビルドも大幅に高速化されます!

RUN --mountは Dockerの比較的新しい機能です。以前だったら、パッケージのインストール手順には必ずと言ってよいほど、一時ファイルをクリーンアップするための rmコマンドやtruncateコマンドだらけの闇の呪文が追加されていたものです。

Dockerfileの残りの部分は、ほぼBundlerのためです。

# Bundlerの設定
ENV LANG=C.UTF-8\
  BUNDLE_JOBS=4\
  BUNDLE_RETRY=3

# Bundlerの設定をプロジェクトのルートフォルダに保存する
ENV BUNDLE_APP_CONFIG=.bundle

# `bin/`や`bundle exec`をプレフィックスせずに
# binstubを実行したい場合は以下のコメントを解除する
# ENV PATH /app/bin:$PATH

# RubyGemをアップグレードして最新のBundlerをインストールする
RUN gem update --system &&\
    gem install bundler

LANG=C.UTF-8でデフォルトのロケールをUTF-8に設定しています。これは気分を損なわないための設定です。これを指定しておかないとRubyの文字列がUS-ASCIiになってしまい、いとしい絵文字たちとサヨナラしなければならなくなります👋

プロジェクト固有のBundler設定(社内gemで使うcredentialなど)を<root>/.bundleフォルダに保存するのであれば、BUNDLE_APP_CONFIGの設定が必要です。Docker Hubにある公式Rubyイメージにはデフォルトでこの変数が定義されているので、Bundlerの設定がローカル設定にフォールバックせずに済みます。

オプションとして、<root>/binフォルダをPATHに追加すればbundle execをプレフィックスせずにコマンドを実行できるようになります。私たちはこの振る舞いをデフォルトでは設定していません。理由は、マルチプロジェクト環境で壊れる可能性があるからです(Railsアプリでローカルなgemやエンジンが使われている場合など)。

従来はBundlerのバージョンも指定する必要がありました(システムで拾われるように若干のハックを利用します)。ありがたいことに、Bundler 2.3.0以降はGemfile.lockのBUNDLED_WITHで定義されているのと同じバージョンのBundlerをわざわざ手動でインストールする必要がなくなりました。コンフリクトの回避はBundlerが代わりにやってくれます(#4076)。

🔗 compose.yml

訳注

Docker Compose V2から、compose.ymlがデフォルトのファイル名になりました。なおcompose-specでは従来のdocker-compose.ymlもサポートすることとなっています。

参考: compose-spec/spec.md at master · compose-spec/compose-spec

Docker Composeは、コンテナ環境のオーケストレーションに利用できるツールです。コンテナ同士を連携させることも、永続的なボリュームやサービスを定義することも可能です。

以下は、データベースにPostgreSQL、バックグラウンドジョブにSidekiqを用いる典型的なRailsアプリケーションを開発するときのComposeファイルです。

x-app: &app
  build:
    context: .
    args:
      RUBY_VERSION: '3.1.0'
      PG_MAJOR: '14'
      NODE_MAJOR: '16'
      YARN_VERSION: '1.22.17'
  image: example-dev:1.0.0
  environment: &env
    NODE_ENV: ${NODE_ENV:-development}
    RAILS_ENV: ${RAILS_ENV:-development}
  tmpfs:
    - /tmp
    - /app/tmp/pids

x-backend: &backend
  <<: *app
  stdin_open: true
  tty: true
  volumes:
    - ..:/app:cached
    - bundle:/usr/local/bundle
    - rails_cache:/app/tmp/caches
    - node_modules:/app/node_modules
    - packs:/app/public/packs
    - packs-test:/app/public/packs-test
    - history:/usr/local/hist
    - ./.psqlrc:/root/.psqlrc:ro
    - ./.bashrc:/root/.bashrc:ro
  environment: &backend_environment
    <<: *env
    REDIS_URL: redis://redis:6379/
    DATABASE_URL: postgres://postgres:postgres@postgres:5432
    WEBPACKER_DEV_SERVER_HOST: webpacker
    MALLOC_ARENA_MAX: 2
    WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
    BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
    XDG_DATA_HOME: /app/tmp/caches
    YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    HISTFILE: /usr/local/hist/.bash_history
    PSQL_HISTFILE: /usr/local/hist/.psql_history
    IRB_HISTFILE: /usr/local/hist/.irb_history
    EDITOR: vi
  depends_on: &backend_depends_on
    postgres:
      condition: service_healthy
    redis:
      condition: service_healthy

services:
  rails:
    <<: *backend
    command: bundle exec rails

  web:
    <<: *backend
    command: bundle exec rails server -b 0.0.0.0
    ports:
      - '3000:3000'
    depends_on:
      webpacker:
        condition: service_started
      sidekiq:
        condition: service_started

  sidekiq:
    <<: *backend
    command: bundle exec sidekiq -C config/sidekiq.yml

  postgres:
    image: postgres:14
    volumes:
      - .psqlrc:/root/.psqlrc:ro
      - postgres:/var/lib/postgresql/data
      - history:/user/local/hist
    environment:
      PSQL_HISTFILE: /user/local/hist/.psql_history
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432
    healthcheck:
      test: pg_isready -U postgres -h 127.0.0.1
      interval: 5s

  redis:
    image: redis:6.2-alpine
    volumes:
      - redis:/data
    ports:
      - 6379
    healthcheck:
      test: redis-cli ping
      interval: 1s
      timeout: 3s
      retries: 30

  webpacker:
    <<: *app
    command: bundle exec ./bin/webpack-dev-server
    ports:
      - '3035:3035'
    volumes:
      - ..:/app:cached
      - bundle:/usr/local/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
      - packs-test:/app/public/packs-test
    environment:
      <<: *env
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
      YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache

volumes:
  bundle:
  node_modules:
  history:
  rails_cache:
  postgres:
  redis:
  packs:
  packs-test:

このComposeファイルでは、6つのサービスと2つの拡張フィールドx-appx-backend)を定義しています。拡張フィールドを使うと、設定内で共通で利用できる部分を定義できます。これらの拡張フィールドにYAMLアンカーをアタッチすると、後からComposeファイル内のどの部分にでも埋め込めるようになります。

原注

本記事では、最終的にDocker Composeを使うこともdocker compose upを実行することもありません。代わりに後述のDipというツールを使うので、このcompose.ymlファイルは単なる「サービスレジストリ」としてのみ振る舞うことになります。もうひとつ重要な点は、このcompose.ymlは(通常と異なり).dockerdev/フォルダの下に置くようにしていることです。これが、このcompose.ymlファイルでソースコードを.:/appではなく..:/appでマウントしている理由です。Dipツールを使わない設定(本記事ではおすすめしません)を検討する場合は、どうかこの点をお忘れなく。

以上を踏まえたうえで、各サービスを徹底的に見ていくことにしましょう。

🔗 x-appサービス

x-app拡張の主な目的は、上記のDockerfileで定義されているアプリケーションコンテナをビルドするのに必要なすべての情報を提供することです。

x-app: &app
  build:
    context: .
    args:
      RUBY_VERSION: '3.1.0'
      PG_MAJOR: '14'
      NODE_MAJOR: '16'
      YARN_VERSION: '1.22.17'

さて、このcontextとは何でしょうか?contextディレクトリは、Dockerで使うビルドコンテキストを定義するもので、ビルドプロセス(COPYコマンドを実行するなど)で使われる一種のワーキングディレクトリのようなものです。

contextディレクトリは、イメージをビルドするたびにパッケージ化されてDockerデーモンに送信されるので、なるべく小さく保つ方がよいでしょう。私たちのコンテキストには.dockerdevディレクトリしかないので、これについては問題ありません。

上述したように、次はDockerfileで宣言されているargsを用いて、依存関係のバージョンを正確に指定します。

イメージにタグ付けする方法にもぜひご注目ください。

image: example-dev:1.0.0

Dockerを開発に使うメリットのひとつは、設定を変更すればチーム内で自動的に同期できることです。つまり、上の1.0.0のようなローカルイメージのバージョン番号は、イメージそのもの(またはイメージが依存する引数)を更新したときだけ更新が必要です。よくあるexample-dev:latestのようなうかつな設定は自分の足を撃ち抜くようなものなので、くれぐれも使わないでください。

イメージのバージョン番号をタグで指定して管理しておくことで、異なる環境で作業するときも余分な作業を強いられずに済むようになります。たとえば"chore/upgrade-to-ruby-3"ブランチで長時間作業している最中でも、イメージのバージョン番号を適切に更新しておけば、いつでもmainブランチにさっと切り替えて古いイメージやRubyを使えるようになります。これならリビルドは一切不要です。


経験則: Dockerfileやその引数を更新する場合(依存関係を更新する場合など)は、必ずその都度イメージタグのバージョン番号を更新すること。


次に、複数のサービス(RailsとWebpackerなど)で共有される環境変数を追加します。

environment: &env
  NODE_ENV: ${NODE_ENV:-development}
  RAILS_ENV: ${RAILS_ENV:-development}

ここにはさまざまな工夫が盛り込まれていますが、その中で X=${X:-何ちゃら}という構文に注目したいと思います。

これは「コンテナ内の変数Xについて、ホストマシンに環境変数Xが存在すればその値を使い、存在しない場合は指定の値を使う」という意味です。これを活用すれば、RAILS_ENV=test docker-compose up railsのようにコマンドで別の環境を指定してサービスを実行できるようになります。

environmentフィールドでは値をリスト形式(- NODE_ENV=xxx)ではなく、辞書形式(NODE_ENV: xxx)で記述している点にもご注意ください。この書式にすることで、共通設定を使い回せるようになります(後述)。

また、コンテナ内の/tmp(およびアプリケーションのtmp/pids)にtmpfsを利用するようDockerに指示しています。こうすることで、コンテナ終了時にserver.pidが残らなくなり、いまいましい"A server is already running"エラーとおさらばできます。

tmpfs:
  - /tmp
  - /app/tmp/pids
🔗 x-backendサービス

いよいよ本記事で一番オイシイ部分にたどり着きました。

x-backendサービスは、すべてのRubyサービスに共通する振る舞いを定義します。

まずボリュームについて見ていきましょう。

x-backend: &backend
  <<: *app
  stdin_open: true
  tty: true
  volumes:
    - ..:/app:cached
    - rails_cache:/app/tmp/caches
    - bundle:/usr/local/bundle
    - history:/usr/local/hist
    - node_modules:/app/node_modules
    - packs:/app/public/packs
    - packs-test:/app/public/packs-test
    - ./.psqlrc:/root/.psqlrc:ro
    - ./.bashrc:/root/.bashrc:ro
    - ./.pryrc:/root/.pryrc:ro
  environment: &backend_environment
    <<: *env
    REDIS_URL: redis://redis:6379/
    DATABASE_URL: postgres://postgres:postgres@postgres:5432
    WEBPACKER_DEV_SERVER_HOST: webpacker
    MALLOC_ARENA_MAX: 2
    WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
    BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
    XDG_DATA_HOME: /app/tmp/caches
    YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
    HISTFILE: /usr/local/hist/.bash_history
    PSQL_HISTFILE: /usr/local/hist/.psql_history
    IRB_HISTFILE: /usr/local/hist/.irb_history
    EDITOR: vi
  depends_on: &backend_depends_on
    postgres:
      condition: service_healthy
    redis:
      condition: service_healthy
volumes:
  - ..:/app:cached
  - bundle:/usr/local/bundle
  - rails_cache:/app/tmp/caches
  - node_modules:/app/node_modules
  - packs:/app/public/packs
  - packs-test:/app/public/packs-test
  - history:/usr/local/hist
  - ./.psqlrc:/root/.psqlrc:ro
  - ./.bashrc:/root/.bashrc:ro

さて、ボリュームリストの冒頭にある項目..:/app:cachedでは、cached戦略を用いてコンテナ内の/appフォルダにプロジェクトディレクトリをマウントしています。このcachedは、macOSでDocker開発を効率化するときの鍵でした。

「え、『でした』ってどういうこと?」

そう、そんな時代もありました。このcached指定は、gRPC FUSE同期がリリースされて以来不要になりました(#5402)。

しかし、私は以下の2つの理由からcachedを当面残しておくつもりです。

  1. Docker Desktopのバージョンが古いメンバーがチーム内にいる可能性がある。
  2. いくつかベンチマークを走らせてみると、古いosxfsファイル共有を使うとパフォーマンスがよくなることがわかった(ただし:cachedを指定した場合のみ)。

そういうわけで、Dockerが最新であっても「Preference」の「Use gRPC FUSE for file sharing」をオフにしておく意味がある可能性もあります。

Dockerチームは、macOS上でDockerがより高速に動作するよう努力を重ねています。最新リリース(4.6.0以降)では、VirtioFSによるディレクトリ共有の高速化と、virtualization.frameworkのサポートが提供されるようになりました。この2つはDocker Desktopの「Preference」の「Experimental Features」タブで設定できます。この設定を有効にしたときのパフォーマンス改善には目をみはることでしょう(おそらく通常操作が最大で2倍は速くなります↓)


その次の行のbundle:/usr/local/bundleでは、/usr/local/bundleディレクトリの内容をbundleというボリュームに保存するようコンテナに指示しています(gemはデフォルトでここに保存されます)。こうすることで、gemデータが永続化されてどの実行でも使われるようになります。compose.ymlで定義したすべてのボリュームは、compose down --volumesを実行するまで維持されます。

以下の行は、「DockerがMacで遅い」という呪いをお祓いするためにわざわざ配置されています。生成されたファイルをすべてDockerボリュームに保存して、ホストマシンのディスク操作が重くならないようにしています。

- rails_cache:/app/tmp/caches
- node_modules:/app/node_modules
- packs:/app/public/packs
- packs-test:/app/public/packs-test

DockerをmacOS上で十分高速化するには、次の2つのルールを守ること。ソースファイルをマウントするときは:cachedを指定し(gRPC FUSEを使わない場合)、生成されたコンテンツ(アセットやバンドルなど)はボリュームに保存すること。


原注

SprocketsやPropshaftを使う場合は、専用ボリューム(assets:/app/public/assets)を追加してそこにアセットを保存するのをお忘れなく。tailwindcss-railsの場合はassets_builds:/app/assets/buildsのように追加します。

次は、さまざまなコマンドラインツールの設定ファイルや履歴を永続化するボリュームをマウントします。

- history:/usr/local/hist
- ./.psqlrc:/root/.psqlrc:ro
- ./.bashrc:/root/.bashrc:ro

Rubyコンテナなのにpsqlがある理由は、rails dbconsoleの内部で使われるからです。

私たちの.psqlrcファイルには以下の仕掛けがあり、履歴ファイルのパスを環境変数で指定できるようになっています。このおかげで、履歴ファイルのパスにPSQL_HISTFILE環境変数が使われ、環境変数が指定されていない場合はデフォルトの$HOME/.psql_historyが使われます。

\set HISTFILE `[[ -z $PSQL_HISTFILE ]] && echo $HOME/.psql_history || echo $PSQL_HISTFILE`

.bashrcファイルには、コンテナ内でターミナルをカスタマイズする設定を以下のように追加できます。

alias be="bundle exec"

それでは、環境変数について解説します。

environment: &backend_environment
  <<: *env
  # ----
  # サービスの検出
  # ----
  REDIS_URL: redis://redis:6379/
  DATABASE_URL: postgres://postgres:postgres@postgres:5432
  WEBPACKER_DEV_SERVER_HOST: webpacker
  # ----
  # アプリケーションの設定
  # ----
  MALLOC_ARENA_MAX: 2
  WEB_CONCURRENCY: ${WEB_CONCURRENCY:-1}
  # -----
  # キャッシュ
  # -----
  BOOTSNAP_CACHE_DIR: /usr/local/bundle/_bootsnap
  # この環境変数はRuboCopなどののツールでキャッシュの保存に使われる
  XDG_DATA_HOME: /app/tmp/cache
  # マウントしたボリュームyにYarnのキャッシュを保存して高速化を図る
  YARN_CACHE_FOLDER: /app/node_modules/.yarn-cache
  # ----
  # 開発ツール
  # ----
  HISTFILE: /usr/local/hist/.bash_history
  PSQL_HISTFILE: /usr/local/hist/.psql_history
  IRB_HISTFILE: /usr/local/hist/.irb_history
  EDITOR: vi

冒頭では、<<: *envで共通の環境変数から変数を「継承」しています。

変数の最初のグループ(DATABASE_URL、 REDIS_URLWEBPACKER_DEV_SERVER_HOST)は、Rubyアプリケーションを他のサービスに接続します。

DATABASE_URLはRailsのActive Recordで、WEBPACKER_DEV_SERVER_HOSTはRailsのWebpackerで、それぞれすぐ使えるようサポートされている変数です。REDIS_URLをサポートするライブラリ(Sidekiq)もありますが、すべてでサポートされているわけではありません。たとえばAction Cableでは明示的な設定が必要です。

Rubyのメモリ関連の裏技については以下をご覧ください。

参考: Cables vs. malloc_trim, or yet another Ruby memory usage benchmark — Martian Chronicles, Evil Martians’ team blog

次のグループには、アプリケーションを対象とする設定の一部が含まれています。たとえば、MALLOC_ARENA_MAXWEB_CONCURRENCYはRubyのメモリの扱いをチェックするのに便利です。

また、Dockerボリュームにキャッシュを保存する変数(BOOTSNAP_CACHE_DIRXDG_DATA_HOMEYARN_CACHE_FOLDER)も用意してあります。

bootsnapはアプリケーションの読み込み時間を短縮するのに使われます。bootsnapのキャッシュはBundlerのデータと同じボリュームに保存しています。このキャッシュにはもっぱらgemのデータが保存されるので、Bundlerのボリュームを削除するとき(Rubyのバージョンアップ時など)にキャッシュも確実にリセットされるようにするためです。

Shopify/bootsnap - GitHub

変数の最後のグループは、開発のエクスペリエンスを向上させるためのものです。中でも重要なのはHISTFILE: /usr/local/hist/.bash_historyです。Bashの履歴の保存場所を指定して、履歴が消えないようにします。PSQL_HISTFILEIRB_HISTFILEも同様です。

原注

IRBの履歴を指定の場所に保存するには、IRB設定ファイル(.irbrc)に以下を記述する必要があります。

IRB.conf[:HISTORY_FILE] = ENV["IRB_HISTFILE"] if ENV["IRB_HISTFILE"]

最後のEDITOR: viは、rails credentials:editコマンドでcredentialファイルを管理するときなどに使われます。

これで、x-backendの説明は以下を残すだけとなりました。

stdin_open: true
tty: true

上を指定すると、このサービスがインタラクティブになります。つまりTTYが提供されます。これは、コンテナにログインしてRailsコンソールやBashを実行するのに必要です。

これは、-itオプションを指定してDockerコンテナを実行するのと同じです。

🔗 railsサービス

railsはデフォルトのバックエンドサービスです。このサーバーは、実行するコマンドだけを上書きします。

rails:
  <<: *backend
  command: bundle exec rails

railsサービスは、開発で必要なすべてのコマンド(rails db:migraterspecなど)を実行するためのものです。

🔗 webサービス

 webサービスはWebサーバーの起動用です。ここでは、公開するポート番号や、アプリの実行に必要な依存関係を定義します。

🔗 webpackerサービス

ここではWEBPACKER_DEV_SERVER_HOST: 0.0.0.0についてだけ説明しておきたいと思います。これを指定することで、webpack-dev-serverが外部からアクセス可能になります(デフォルトではlocalhostで実行されますが、この場合は外部からアクセスできません)。

🔗 ヘルスチェック

Railsでdb:migrateなどのコマンドを実行するときは、データベースが確実に起動していてコネクションが受け付け可能であることを確認しておきたいと思います。ヘルスチェック機能を利用すれば、依存するサービスの準備が整うまで待機するようDocker Composeに指示できます。

そろそろ皆さんも、設定にあるdepends_onの定義がサービスの単なるリストではないことにお気づきかもしれませんね。

backend:
  # ...
  depends_on:
    postgres:
      condition: service_healthy
    redis:
      condition: service_healthy

postgres:
  # ...
  healthcheck:
    test: pg_isready -U postgres -h 127.0.0.1
    interval: 5s

redis:
  # ...
  healthcheck:
    test: redis-cli ping
    interval: 1s
    timeout: 3s
    retries: 30

🔗 Dipツールを導入する

Docker Composeの操作が面倒に感じられるのであれば、開発者のエクスペリエンスを快適にするDipという強い味方があります(Evil Martiansの同僚が開発しました)。

bibendi/dip - GitHub

訳注

dipについては以下の記事もどうぞ。

docker-composeを便利にするツール「dip」を使ってみた

Dipはdocker composeコマンドの薄いラッパーで、従来のインフラ指向のフローから開発指向のフローに乗り換えられます。Dipを使うと以下のようなメリットを得られます。

  • アプリケーション固有の対話型コマンドやサブコマンドを定義する機能
  • 開発環境をゼロから一発で構築できるdip provisionフロー
  • 複数のcompose.ymlファイルをサポート(OS固有の設定も含む)

以下の2つのコマンドを実行するだけで、ローカル環境にDipを導入して即座にアプリで作業を開始できるようになります。

# Dockerイメージがなければビルドし、追加コマンドも実行する
$ dip provision
# 定義済みの依存関係を用いてRailsサーバーを起動する
$ dip rails s
=> Booting Puma
=> Rails 7.0.2.2 application starting in development
=> Run `bin/rails server --help` for more startup options
[1] Puma starting in cluster mode...
...
[1] - Worker 0 (PID: 9) booted in 0.0s, phase: 0

以下は典型的なdip.ymlファイルの設定例です。

version: '7.1'

# Docker Composeに渡すデフォルトの環境変数を定義する
environment:
  RAILS_ENV: development

compose:
  files:
    - .dockerdev/compose.yml
  project_name: example_demo

interaction:
  # 必要な依存関係(データベースなど)とともに
  # Railsコンテナを起動し、コンテナ内でターミナルを開く
  runner:
    description: Open a Bash shell within a Rails container (with dependencies up)
    service: rails
    command: /bin/bash

  # 依存するサービスを起動せずにRailsコンテナを実行する
  # (Railsと無関係のスクリプトの実行に便利)
  bash:
    description: Run an arbitrary script within a container (or open a shell without deps)
    service: rails
    command: /bin/bash
    compose_run_options: [ no-deps ]

  # Bundlerコマンドのショートカット
  bundle:
    description: Run Bundler commands
    service: rails
    command: bundle
    compose_run_options: [ no-deps ]

  # RSpecを実行するショートカット(RAILS_ENVを上書きする)
  rspec:
    description: Run RSpec commands
    service: rails
    environment:
      RAILS_ENV: test
    command: bundle exec rspec

  rails:
    description: Run Rails commands
    service: rails
    command: bundle exec rails
    subcommands:
      s:
        description: Run Rails server at http://localhost:3000
        service: web
        compose:
          run_options: [service-ports, use-aliases]

  yarn:
    description: Run Yarn commands
    service: rails
    command: yarn
    compose_run_options: [ no-deps ]

  psql:
    description: Run Postgres psql console
    service: postgres
    default_args: anycasts_dev
    command: psql -h postgres -U postgres

  'redis-cli':
    description: Run Redis console
    service: redis
    command: redis-cli -h redis

provision:
  - dip compose down --volumes
  - dip compose up -d postgres redis
  - dip bash -c bin/setup

いくつかの設定について詳しく説明します。

最初はcomposeセクションについてです。

compose:
  files:
    - .dockerdev/compose.yml
  project_name: example_demo

ここでは私たちのDocker Composeの設定ファイル(.dockerdev/compose.yml)へのパスを指定します。これで、プロジェクトのルートディレクトリでdipを実行すれば正しい設定が読み込まれます。

project_nameは重要です。これを指定しておかないと、compose.ymlファイルが置かれているフォルダ(ここでは.dockerdev)がプロジェクト名として認識されてしまい、複数のプロジェクト同士がコンフリクトする可能性があります。

railsコマンドにも注目したい点がいくつかあります。

rails:
  description: Run Rails commands
  service: rails
  command: bundle exec rails
  subcommands:
    s:
      description: Run Rails server at http://localhost:3000
      service: web
      compose:
        run_options: [service-ports, use-aliases]

デフォルトでは、dip railsを実行するとRailsコンテナ内でbundle exec railsが実行されます。しかしここではDipのサブコマンド機能を使ってdip rails sを別扱いにしています。

  • ここではrailsサービスではなくwebサービスを使う(つまり依存関係も起動する)
  • サービスのポートを公開する(ここでは3000
  • ネットワークエイリアスも有効にして、他のサービスがwebというホスト名でコンテナにアクセスできるようにする

dip rails sの背後では、以下のDocker Composeコマンドを生成します。

docker compose run --rm --service-ports --use-aliases web

上ではupではなくrunが使われている点にご注目ください。runにすることで、サーバーがターミナルからアクセス可能になります。つまり、たとえばデバッガをアタッチしても問題なく利用できるということです(upコマンドではターミナルがインタラクティブになりません)。

🔗 対話的なプロビジョニング

Dockerイメージをビルドしてデータベースをセットアップしただけで開発を始められるアプリケーションはめったにありません。ほとんどの場合、その他にも何らかの秘密鍵やcredentialや.envファイルなどが必要になります。私たちはDipツールでインタラクティブなデプロイを実現し、プロジェクトの新メンバーが細かい追加設定であくせくせずに短時間で環境構築を完了することに成功しました。

設定を適切に管理する方法を学ぶには、以下の「Terraforming Railsシリーズ」記事をご覧ください。

Rails: anyway_config gemでRubyの設定を正しく整理しよう(翻訳)


たとえば、.env.development.localファイルに秘密情報をいくつか保存して、Sidekiq Proなどのプライベートレジストリからgemをダウンロードするように設定しなければならないとします。こんなとき、私なら以下のようなプロビジョニングスクリプトを書くでしょう。

# 単独でも実行できるように、このコマンドを切り出してある
configure_bundler:
  command: |
    (test -f .bundle/config && cat .bundle/config | \
      grep BUNDLE_ENTERPRISE__CONTRIBSYS__COM > /dev/null) ||
    \
      (echo "Sidekiq ent credentials: "; read -r creds; dip bundle config --local enterprise.contribsys.com $creds)

provision:
  - (test -f .env.development.local) || (echo "\n\n ⚠️  .env.development.local file is missing\n\n"; exit 1)
  - dip compose down --volumes
  - dip configure_bundler
  - (test -f config/database.yml) || (cp .dockerdev/database.yml.example config/database.yml)
  - dip compose up -d postgres redis
  - dip bash -c bin/setup

以下は、dip provisionコマンドを実際に実行している様子です。

Dipによる対話的なプロビジョニングの例

🔗 開発における(マイクロ)サービス vs Docker

開発環境を標準化するもうひとつのユースケースは、複数の独立したサービスをローカルで立ち上げられるようにすることです。これをDipで行う簡単なデモをお見せします。

まず、本記事に沿って個別のアプリケーションをDocker化します。次に、アプリ同士を接続する必要があります。これはDocker Composeの外部ネットワーク機能でできます。

今述べたようなことは伝統あるMakefileでもできますが、Dipのような専用ツールであらゆることを宣言的に定義する方がずっと効率がよいこともわかってきました。

以下のdip.ymlファイルを各アプリに追加します。

# ...
provision:
  # 名前付きネットワークが存在していることを確認する
  - docker network inspect my_project > /dev/null 2>&1 ||\
    docker network create my_project
# ...

最後に、compose.ymlファイル内のエイリアスを経由してサービスをネットワークにアタッチします。

# サービスAのcompose.yml
service:
  ruby:
    # ...
    networks:
      default:
      project:
        aliases:
          - project-a

networks:
  project:
    external:
      name: my_project
# サービスBのcompose.yml
service:
  web:
    # ...
    environment:
      # 外部ネットワーク用に定義されたエイリアスを介して
      # サービスAにアクセスできる
      SERVICE_URL: http://project-a:3000

networks:
  project:
    external:
      name: my_project

🔗 development環境からproduction環境へ

本記事の最初のバージョンを公開して以来最も多く寄せられた質問が「Dockerをproduction環境で使う方法」です。この質問にお答えするにはまったく別の記事を書く必要があります(書きます😉)。

それまでのつなぎとして、現在のdevelopment環境を拡張してproduction環境にも応用する方法を先行紹介いたします。

最初にお断りしておきますが、今はDocker Composeベースのデプロイについては扱いませんので、compose.ymlは無関係です。必要なのは、Dockerイメージを更新してdevelopment環境とproduction環境の違いを反映することだけです。

  1. セキュリティ上の理由で、rootユーザーではなく一般ユーザーの代理としてコードを実行すること
  2. 必要な依存関係や成果物はイメージにすべて格納しておくこと
  3. ソースコードはコンテナ内にコピーする形で保存すること
  4. イメージがなるべく太らないように作ること

上の要件を満たすために、既存のDockerfileをリファクタリングして複数の「ステージ」を定義します(Dockerのマルチステージビルドもサポートします)。以下はコメント付きの例です。

# syntax=docker/dockerfile:1

ARG RUBY_VERSION
ARG DISTRO_NAME=bullseye

# ここでbaseという名前のステージを追加
FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME AS base

ARG PG_MAJOR
ARG NODE_MAJOR
ARG YARN_VERSION

# 共通の依存関係
# ...
# 以下は上とまったく同じ
# ...
# ...
WORKDIR /app

EXPOSE 3000
CMD ["/usr/bin/bash"]

# 次にbaseからdevelopmentステージを定義する
FROM base AS development

ENV RAILS_ENV=development

# baseイメージとの主な違いは、developmentのみのシステム依存関係を
# 持てる点(Vimやgraphvizなど)。
# これらはAptfile.devファイルから抽出する
COPY Aptfile.dev /tmp/Aptfile.dev
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    $(grep -Ev '^\s*#' /tmp/Aptfile.dev | xargs)

# production-builderイメージは依存関係のインストールや
# アセットのコンパイルを担当する
FROM base as production-builder

# まずアプリケーションを実行する専用のユーザーを作成。設定する
RUN groupadd --gid 1005 my_user \
  && useradd --uid 1005 --gid my_user --shell /bin/bash --create-home my_user
USER my_user
RUN mkdir /home/my_user/app
WORKDIR /home/my_user/app

# 次にBundlerを再設定する
ENV RAILS_ENV=production \
  LANG=C.UTF-8 \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3 \
  BUNDLE_APP_CONFIG=/home/my_user/bundle \
  BUNDLE_PATH=/home/my_user/bundle \
  GEM_HOME=/home/my_user/bundle

# gemをインストール
COPY --chown=my_user:my_user Gemfile Gemfile.lock ./
RUN mkdir $BUNDLE_PATH \
  && bundle config --local deployment 'true' \
  && bundle config --local path "${BUNDLE_PATH}" \
  && bundle config --local without 'development test' \
  && bundle config --local clean 'true' \
  && bundle config --local no-cache 'true' \
  && bundle install --jobs=${BUNDLE_JOBS} \
  && rm -rf $BUNDLE_PATH/ruby/3.1.0/cache/* \
  && rm -rf /home/my_user/.bundle/cache/*

# JavaScriptパッケージをインストール
COPY --chown=my_user:my_user package.json yarn.lock ./
RUN yarn install --check-files

# コードをコピーする
COPY --chown=my_user:my_user . .

# アセットをプリコンパイルする
# 注: credentialを使う場合はSECRET_KEY_BASEなどの
# 環境変数も必要になる可能性がある
RUN bundle exec rails assets:precompile

# 最後にproductionイメージを定義する
# 注: これはbaseイメージを拡張したものではなく新規イメージである
FROM ruby:$RUBY_VERSION-slim-$DISTRO_NAME AS production

# productionのみで必要な依存関係
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  apt-get update -qq \
  && apt-get dist-upgrade -y \
  && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    curl \
    gnupg2 \
    less \
    tzdata \
    time \
    locales \
  && update-locale LANG=C.UTF-8 LC_ALL=C.UTF-8

# gemをアップグレードして最新バージョンのBundlerもインストールする
RUN gem update --system &&\
    gem install bundler

# production-builderイメージのときと同じ名前の専用ユーザーを
# 作成・設定する
RUN groupadd --gid 1005 my_user \
  && useradd --uid 1005 --gid my_user --shell /bin/bash --create-home my_user
RUN mkdir /home/my_user/app
WORKDIR /home/my_user/app
USER my_user

# Ruby/Railsの環境変数を設定する
ENV RAILS_ENV=production\
  BUNDLE_APP_CONFIG=/home/my_user/bundle \
  BUNDLE_PATH=/home/my_user/bundle \
  GEM_HOME=/home/my_user/bundle \
  PATH="/home/my_user/app/bin:${PATH}" \
  LANG=C.UTF-8 \
  LC_ALL=C.UTF-8

EXPOSE 3000

# コードをコピーする
COPY --chown=my_user:my_user . .

# 成果物をコピーする
# 1) インストールされたgem
COPY --from=production-builder $BUNDLE_PATH $BUNDLE_PATH
# 2) コンパイル済みアセット(ここではWebpackerでコンパイル)
COPY --from=production-builder /home/my_user/app/public/packs /home/my_user/app/public/packs
# 3) BootsnapのキャッシュもコピーしてRailsサーバーの起動を高速化する
COPY --chown=my_user:my_user --from=production-builder /home/my_user/app/tmp/cache/bootsnap* /home/my_user/app/tmp/cache/

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

🔗 対話的ジェネレータ「Ruby on Whales」の紹介

evilmartians/ruby-on-whales - GitHub

本記事のボーナスとして、私たちが公開した Ruby on Whalesをご紹介します。 Ruby on WhalesにはRailsテンプレートが含まれており(Rails Bytesで公開されています)、わずか1個のコマンドを実行していくつかの質問に回答するだけで開発で即座にDockerを導入できるよう支援します。

何はともあれ、以下のデモ動画をご覧ください。

Ruby on Whalesインストーラの対話的操作

以下のコマンドをどうぞお試しください。

rails app:template LOCATION='https://railsbytes.com/script/z5OsoB'

感謝

以下の皆さまにお礼の言葉を申し上げます。

🔗 原文Changelog

2.0.3 (2023-09-21)
Node.jsのインストールスクリプトを更新
2.0.2 (2022-11-30)
ビルド中のクリーンアップを手動からRUN --mountによるパッケージのキャッシュに変更
2.0.1 (2022-03-22)
非推奨のapt-keygpgに置き換え
2.0.0 (2022-03-02)
全面的な更新および新章の追加
1.1.4 (2021-10-12)
tmpfsにtmp/pidsを追加(A server is already running”エラー対策)
1.1.3 (2021-03-30)
minimagic gemのライセンス問題緩和のためDockerfileを更新(#35
docker-compose設定の環境変数を辞書形式に変更(#6
1.1.2 (2021-02-26)
依存関係のバージョンを更新(#28
Aptfileにコメントを書けるようになった(#31
Dockerfile内のAptfileへのパスを修正(#33
1.1.1 (2020-09-15)
.dockerdevディレクトリをプロジェクトディレクトリではなくビルドコンテキストとして使用(#26
1.1.0 (2019-12-10)
Rubyのベースイメージをslimに変更
Rubyバージョン用のDebianリリースを明示的に指定し、busterにアップグレード
bundlerのパスを/bundleからbundler標準の/usr/local/bundleに変更
Docker Composeのファイル形式はv2.4を使用
postgresサービスとredisサービスにヘルスチェックを追加

関連記事

Rails 7: importmap-rails gem README(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。