🚊

Docker で rails のプロジェクトを作成する手順(PostgreSQL)

2023/10/15に公開

Docker で rails プロジェクトを作成して起動できるまでの手順です。
WEB で調べると何種類かやり方が出てくるのですが、作成されるプロジェクトのバージョンが意図したものが作れなかったり、手順通りにしてもすんなり起動できなったりとしっくりくるものがありませんでした。

手順通りにしてもすんなり起動できない場合にエラーを理解して自分で対処することもできず困っていました。
そもそも Docker や rails についても初学者であるため、まずは基本的な構築方法を習得し、そこからカスタマイズして理解したいと思ったので基本的な状態でのプロジェクトの起動手順をまとめました。

この手順の目的は rails プロジェクトを新規作成し、その状態で起動確認できるところまでなので、この手順通りに作成したプロジェクトをそのまま使うのではなく、あくまで基礎としてプロジェクトに合わせてカスタマイズするためのものです。

最初からプロジェクトに合わせた設定をして構築できればいいのですが、そもそも起動できる状態のものがなければカスタマイズできないので、その素体として使用します。

作成するプロジェクトのバージョン

最初に rails プロジェクトをローカル環境に構築してから Docker コンテナにプロジェクトをコピーするのでローカル環境の構築が必要になります。

  • MacOS
    • ruby 3.2.2
    • rails 7.1.1
    • postgres 16

構築手順

  1. ローカル環境を構築
    1. rbenv をインストール
    2. ruby と rails のインストール
    3. postgresql のインストール
    4. ローカル環境に rails new でプロジェクトを作成
  2. Docker でビルドしプロジェクトを起動
    1. env ファイルと comopse.yaml を作成
    2. database.yml を編集
    3. docker でビルド
    4. DB を作成する
    5. docker-compose up で起動して rails の初期画面を確認

環境ができてしまえば手間は env ファイルの作成compose.yaml の作成database.ymlの修正、DB の作成 だけです。

ローカル環境を構築

rbenv をインストール

# Homebrew を使って rbenv をインストール
% brew install rbenv
# rbenvをシェルに組み込む設定
% echo 'eval "$(rbenv init --path)"' >> ~/.zshrc
# シェルをリロード
% source ~/.zshrc

ruby と rails をインストール

# Rubyをインストール
% rbenv install 3.2.2
% rbenv global 3.2.2
# Railsをインストール
% gem install rails -v 7.1.1

PostgreSQL をインストール

# PostgreSQLをインストール
brew install postgresql

rails プロジェクトを作成

Database に PostgreSQL を指定してプロジェクトを作成します。

# プロジェクト作成
% rails new myapp -d postgresql
# ディレクトリ移動
% cd myapp

rails プロジェクト内に env ファイルを作成する

ディレクトリ構造

myapp
└── .env
    ├── development
    │   ├── database
    │   └── web
    # その他のプロジェクトファイル

.env/development/database の内容
DB の接続情報を記述します。

POSTGRES_DB=myapp_development
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password

.env/development/web の内容
後述する Dockerfile には RAILS_ENV で本番環境が指定されているため、開発環境で起動するようにオーバーライドします。

RAILS_ENV=development

rails プロジェクト内に compose.yaml を作成する

今回、 Dockerfilerails new でプロジェクトを作成した際に自動で作成されるものをそのまま使用します。
また、よく rails 固有のサーバーファイル(server.pid)の削除のために docker-entrypoint.sh などのスクリプトを作成しますが、こちらも /rails/bin/docker-entrypoint に含まれているため作成しません。
ここでは docker-compose 用に compose.yaml のみを作成します。

services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"
    env_file:
      - .env/development/database
    volumes:
      - "db_data:/var/lib/postgresql/data"
  web:
    build: .
    env_file:
      - .env/development/web
      - .env/development/database
    command: bash -c "bundle e rails s -b '0.0.0.0'"
    volumes:
      - .:/usr/src/app
      - gem_cache:/gems
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  db_data:
  gem_cache:

ちなみに自動で作成される Dockerfile の内容はこのようになっています。作成するプロジェクトのバージョンによって変わると思います。
内容については後述します。

# syntax = docker/dockerfile:1

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile

ARG RUBY_VERSION=3.2.2
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base

# Rails app lives here

WORKDIR /rails

# 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 libpq-dev libvips pkg-config

# 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

# Install packages needed for deployment

RUN apt-get update -qq && \
 apt-get install --no-install-recommends -y curl libvips postgresql-client && \
 rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Copy built artifacts: gems, application

COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security

RUN useradd rails --create-home --shell /bin/bash && \
 chown -R rails:rails db log storage tmp
USER rails:rails

# 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/bin/docker-entrypoint の内容はこのようになっています。

#!/bin/bash -e

# If running the rails server then create or migrate existing database
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
  ./bin/rails db:prepare
fi

exec "${@}"

rails プロジェクト内の database.yml を編集する

DB と接続するために config/database.yml を修正します。
hostusernamepassword を追記します
必要に応じて database も修正します。

default: &default
  adapter: postgresql
  encoding: unicode
  host: db # compose.yaml の DB コンテナ名を指定する
  username: <%= ENV['POSTGRES_USER'] %> # 追加
  password: <%= ENV['POSTGRES_PASSWORD'] %> # 追加
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: myapp_development

Docker でビルドする


% docker-compose build

DB を作成

# コンテナ起動前(手順通り)ならこちら
% docker-compose run web rake db:create
% docker-compose run web rake db:migrate
# コンテナを先に起動させた場合はこちら
% docker-compose exec web rake db:create
% docker-compose exec web rake db:migrate

起動確認

docker-compose でコンテナを起動します。

% docker-compose up

接続確認

http://localhost:3000 に接続して確認する

これで rails の初期画面が表示されればひとまず成功です。
最初に述べたようにあくまでもこれは素体なのでここから必要に応じてカスタマイズしてください。
例えば自動で作成される Dockerfile は本番環境用となっているため、開発用では不要な項目も多いと思います。

今回はひとまず標準の状態で起動が確認できるまでですが、ここまでで気になった点を ChatGPT に聞いてみたのでその内容を残しておきます。これからカスタマイズして使用するのに参考になればと思います。

これより先は AI による出力内容です。
私は初学者ですのでこれらのことに関してまだ詳しくないため正誤の判断はできません。


rails new で自動で作成される Dockerfile の内容について

この Dockerfile は、Rails アプリケーションをコンテナ化するためのマルチステージビルドの一例です。マルチステージビルドを用いることで、最終的なイメージサイズを小さく抑えつつ、ビルドやアセットのプリコンパイルなどを効率的に行うことができます。それでは、各セクションごとに解説していきます。

ベースステージ

# syntax = docker/dockerfile:1

ARG RUBY_VERSION=3.2.2
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base

WORKDIR /rails

ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"

解説

  • 1 行目 では、Dockerfile の構文バージョンを指定しています。新しいビルド機能や構文を利用するためです。
  • ARG で指定されている RUBY_VERSION は、ビルド時にパラメータとして設定可能な変数です。ここでは Ruby のバージョンを指定しています。
  • FROM はベースとなるイメージを指定します。ここでは、slim プロファイル(軽量版)の Ruby イメージを使用します。
  • WORKDIR で、作業ディレクトリ(アプリケーションのルートディレクトリ)を指定します。
  • ENV で、環境変数を設定します。ここでは、Rails の実行環境、Bundler の設定などを行っています。

ビルドステージ

FROM base as build

RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libpq-dev libvips pkg-config

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 . .

RUN bundle exec bootsnap precompile app/ lib/

RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile

解説

  • ビルドステージ は、ベースステージを元にビルド用の環境を作ります。
  • apt-get で、Gems のビルドやアプリケーションの実行に必要なパッケージをインストールします。
  • COPY で Gemfile をコピーし、 bundle install で Gems をインストールします。その後、不要なファイルを削除してイメージのサイズを抑えています。
  • アプリケーションのコードをコピーし、bootsnap をプリコンパイルし、アセットをプリコンパイルします。

ファイナルステージ

FROM base

RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libvips postgresql-client && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /rails /rails

RUN useradd rails --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp
USER rails:rails

ENTRYPOINT ["/rails/bin/docker-entrypoint"]

EXPOSE 3000
CMD ["./bin/rails", "server"]

解説

  • ファイナルステージ は、最終的なアプリケーションイメージを作成します。ベースステージを元に、実行に必要な最低限のパッケージをインストールします。
  • COPY --from=build でビルドステージで用意した Gems やアプリケーションコードをコピーします。
  • ユーザーrailsを作成し、一部のディレクトリのオーナーシップを設定後、そのユーザーでコマンドを実行します(セキュリティ強化のため)。
  • ENTRYPOINT でコンテナ起動時に実行されるコマンドを指定します。
  • EXPOSE で公開するポートを指定します。
  • CMD でコンテナが起動する際に実行されるデフォルトのコマンドを指定します。ここでは Rails サーバーを起動しています。

Dockerfile について

  • 開発用 Dockerfile:開発用のDockerfileはビルド時間を短縮するため、ある程度シンプルに保つのが良いです。また、開発ではデバッグやテストのための追加ツールが必要になるかもしれません。

以下は、シンプルな開発用Dockerfileの一例です。

FROM ruby:3.2.2

# 必要なパッケージをインストール
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs

# Yarnのインストール
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update && apt-get install -y yarn

WORKDIR /myapp

# Gemファイルを追加し、インストール
COPY Gemfile* /myapp/
RUN bundle install

# その他のアプリケーションソースをコピー
COPY . /myapp

このDockerfileでは、アセットのプリコンパイルなど本番特有のステップを省略しています。開発中はコードの変更が頻繁にあるため、ビルドを高速に保つことが重要です。

補足

  • 最低限の権限を持つユーザー:本番環境ではセキュリティを強化するため、アプリケーションは root ユーザーとして実行せず、限定された権限を持つユーザーとして実行すべきです。

具体的には、Dockerfileに以下のコマンドを追加して、アプリケーションを実行するユーザーを作成し、そのユーザーでコマンドを実行します。

RUN useradd -m myuser
USER myuser

上記をDockerfileの適切な位置に追加します。

  • 本番環境でのボリュームマウントの避け方:本番環境では、コンテナにコードをコピーし、変更のないイメージをデプロイすることをお勧めします。

Dockerfileで以下のようにアプリケーションコードをコピーします。

COPY . /myapp

開発環境でのボリュームのマウントは、ローカルのコード変更をすぐに反映するために使用しますが、本番環境ではこのような動作は望ましくありません。そのため、docker-compose.ymlにおいて、本番用のセクション(サービスやdocker-compose.override.ymlなど)を作成し、そこではボリュームのマウントを行わないようにします。

開発と本番で設定が異なる場合、docker-composeを使ってそれぞれの用途に適したコンテナの起動方法を定義すると、一貫した操作性を保ちつつ、環境ごとの違いを適切にハンドリングできます。


開発用と本番用のDockerfileを作成例です。

開発用の Dockerfile

# 開発用の Dockerfile

FROM ruby:3.2.2

# 必要なパッケージをインストール
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs

# Yarnのインストール
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update && apt-get install -y yarn

# 作業ディレクトリのセットアップ
WORKDIR /myapp

# Gemファイルを追加し、インストール
COPY Gemfile* /myapp/
RUN bundle install

# その他のアプリケーションソースをコピー
COPY . /myapp

本番用の Dockerfile

# 本番用の Dockerfile

FROM ruby:3.2.2 AS builder

# 必要なパッケージをインストール
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs

# Yarnのインストール
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update && apt-get install -y yarn

# 作業ディレクトリのセットアップ
WORKDIR /myapp

# Gemファイルを追加し、インストール
COPY Gemfile* /myapp/
RUN bundle install --without development test \
    && rm -rf /usr/local/bundle/cache/*.gem \
    && find /usr/local/bundle/gems/ -name "*.c" -delete \
    && find /usr/local/bundle/gems/ -name "*.o" -delete

# アプリケーションソースをコピー
COPY . /myapp

# アセットプリコンパイル
RUN bundle exec rake assets:precompile

# 実行ユーザーを作成
RUN useradd -m myuser
USER myuser

# 実行環境のセットアップ
FROM ruby:3.2.2

# 必要なパッケージをインストール
RUN apt-get update -qq && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*

# 作業ディレクトリのセットアップ
WORKDIR /myapp

# ユーザーを追加
RUN useradd -m myuser
USER myuser

# ビルダーから必要なファイル/ディレクトリをコピー
COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
COPY --from=builder /myapp /myapp

開発用のDockerfile はすべての依存関係をインストールし、コードをコンテナ内にコピーします。基本的にはデバッグやテストのツールを追加したり、ボリュームをマウントしてローカルのコード変更を即時に反映させる形になります。

本番用のDockerfile はビルドステージ(builder)とランステージを分け、ビルドステージで依存関係のインストールやアセットのプリコンパイルを行い、ランステージではアプリケーションコードとビルドで生成されたものをコピーして軽量なコンテナを作成します。また、セキュリティを強化するため非 root ユーザーでアプリケーションを実行します。

各環境によってdocker-compose.ymlDockerfileを調整し、ローカル開発の効率と本番運用のセキュリティ・効率を両立させていくことが大切です。


本番環境と開発環境で異なるDockerfileを使用する場合、それに伴いdocker-compose.ymlも環境ごとに異なる設定を持たせるのが一般的です。開発環境と本番環境では、使うコンテナの設定や、セキュリティ設定、ネットワークの設定などが異なる可能性があるためです。

開発用 docker-compose.yml

開発用のdocker-compose.ymlでは、コードの変更を即座に反映させるためにボリュームをマウントし、開発ツールを含めた Docker イメージを使用します。

version: "3"
services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: myapp_development
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password

  web:
    build:
      context: .
      dockerfile: Dockerfile.dev # 開発用のDockerfileを指定
    ports:
      - "3000:3000"
    volumes:
      - ".:/myapp"
    depends_on:
      - db

本番用 docker-compose.yml

本番環境用のdocker-compose.ymlでは、セキュリティを強化し、パフォーマンスを最適化します。コードはイメージにコピーされ、ボリュームは使用されません。

version: "3"
services:
  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: myapp_production
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: productionpassword

  web:
    build:
      context: .
      dockerfile: Dockerfile.prod # 本番用のDockerfileを指定
    ports:
      - "3000:3000"
    depends_on:
      - db

留意点として、本番環境のdocker-compose.ymlでは、シークレットな情報(データベースのユーザー名やパスワードなど)は環境変数や.envファイル、もしくは秘密情報を安全に管理するための専用ツールを使用して、コンテナ外から渡すようにしましょう。

両環境において、サービス(データベースやキャッシュストアなど)の設定が異なる可能性もあるため、それぞれの環境に最適なdocker-compose.ymlを作成することで、よりシームレスな開発・デプロイフローを実現することができます。


docker-composeコマンドは-fまたは--fileオプションを使って、使用するdocker-compose.ymlファイルを指定することができます。複数のdocker-compose.ymlファイルを持っている場合(例えば、docker-compose.dev.ymldocker-compose.prod.yml)、このオプションを使用してどちらの設定でコンテナを起動するかを選べます。

開発環境での起動例:

docker-compose -f docker-compose.dev.yml up

本番環境での起動例:

docker-compose -f docker-compose.prod.yml up

docker-compose upコマンドを実行するディレクトリは、指定したdocker-compose.ymlファイルが存在するディレクトリである必要があります。また、複数のdocker-compose.ymlファイルを指定して、設定をオーバーライド(上書き)することも可能です。

複数のファイルを指定した起動例:

docker-compose -f docker-compose.yml -f docker-compose.override.yml up

このコマンドは、最初にdocker-compose.ymlの設定を読み込み、次にdocker-compose.override.ymlの設定を読み込んで、最初の設定を上書きします。これにより、開発環境や本番環境で異なる設定を適用することが可能になります。


なぜローカルにプロジェクトを作成してから Docker コンテナにコピーするのか(Docker コンテナ内でプロジェクトを作成しないのはなぜなのか)

Rails プロジェクトをローカルで作成してから Docker コンテナにコピーする方法と、Docker コンテナ内で直接プロジェクトを作成する方法はいずれも有効ですが、以下の視点から前者のアプローチが一般的にとられることが多いです。

1. 開発効率

  • エディタの利便性: ローカルでプロジェクトを生成することで、開発者は自身のお気に入りのエディタや IDE を用いてコードを容易に編集できます。特に IDE の強力なコード補完やリファクタリング機能は、ローカルで直接コードを触る場合に有効です。
  • ハードウェアリソース: ローカルで生成・実行した方が、Docker コンテナを経由するよりもハードウェアリソース(CPU, メモリ, I/O)を効率的に利用できる場合があります。

2. ビルド時間の短縮

  • プロジェクトを最初にローカルで生成しておくことで、docker buildを実行する際にbundle installなどの時間を短縮できる場合があります。Gemfile が変わらない限り Docker キャッシュを利用でき、ビルド時間を節約できます。

3. 組み込みのデータベースセットアップ

  • ローカルで Rails プロジェクトを作成することで、rails newコマンドで生成される初期マイグレーションファイルを用いて、開発者は即座にデータベースをセットアップできます。

4. コンテナのイメージサイズ

  • プロジェクトをローカルで作成し、必要なファイルだけをコンテナにコピーすることで、コンテナのイメージサイズを小さく保つことが可能です。特に Node モジュールや一部の Gem など、大量のファイルを生成するパッケージが多いため、これらをコンテナイメージに含めると大きくなりがちです。

5. ローカルでのデバッグ

  • 開発中、ローカルで手早く動作確認やデバッグを行いたい場合があります。ローカルでプロジェクトがセットアップされていると、全てのコンテナを起動することなく、素早く動作確認が可能です。

6. CI/CD パイプラインとの整合性

  • CI/CD パイプラインを構築する場合、テストやビルドを行うステージではコードがコンテナにコピーされることが一般的です。ローカルでの開発フローと CI/CD のフローを統一することで、本番環境に近いテスト環境を容易に構築でき、デプロイプロセスをスムーズに進めることができます。

結論

いずれの方法も一長一短がありますが、上記の理由から、ローカルでプロジェクトを作成してからコンテナにコピーする方法が採用されることが多いです。プロジェクトのニーズや開発チームの作業スタイルによって、最適な方法は異なるため、特定のコンテキストに最も適したアプローチを選択することが重要です。

GitHubで編集を提案

Discussion