🐈

Docker + Rails7.1 の環境構築で詰まったポイント

2023/12/06に公開

Docker + Rails + PostgreSQLというよくある構成で環境構築をしたところ、Rails7.1でリリースされた新機能によって詰まった点があったのでメモします。

実行環境

  • M2 Macbook Air
  • Rails 7.1.2
  • Ruby 3.2.2
  • PostgreSQL 16.1
  • Docker 20.10.21

問題の再現

Docker docsに掲載されていたこちらのサンプルをもとに環境構築を進めていきます。

https://github.com/docker/awesome-compose/tree/master/official-documentation-samples/rails/

プロジェクトの定義

Dockerfileを作成。Rubyのバージョンは現時点での最新安定版3.2.2にします。

Dockerfile
# syntax=docker/dockerfile:1
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Configure the main process to run when running the image
CMD ["rails", "server", "-b", "0.0.0.0"]

Gemfileの作成。Railsのバージョンは指定せず最新安定版を取得してもらうことにします。※あとでGemfileを確認すると7.1.2がインストールされていました。

Gemfile
source 'https://rubygems.org'
gem 'rails'

空のGemfile.lockを作成。

$ touch Gemfile.lock

entrypoint.shも作成します。

entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

compose.yamlの作成。サンプルのファイル名はdocker-compose.ymlですが、Dockerのドキュメントではcompose.yamlが推奨されているのでそれにならいます。

また、DBのデータをDockerの管理下におきたかったので、マウントの方法をボリュームマウントに変更しました。

compose.yaml
services:
  db:
    image: postgres
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  db-data:

プロジェクトのビルド

docker compose runコマンドでRailsアプリの雛形を作成します。RailsはAPIモードで使いたかったので、末尾に--apiオプションを追加しました。

$ docker compose run --no-deps web rails new . --force --database=postgresql --api

Gemfileが更新されたので、Dockerイメージを再びビルドします。

$ docker compose build

ここまでは問題なく進んだように見えたのですが……。

DBとの接続

config/database.ymlにPostgreSQLとの接続情報を追加します。

config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
+   host: db
+   username: postgres
+   password: password
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

# 以下略

docker compose upでPostgreSQLとRailsを起動します。

$ docker compose up

別のシェルに移ってDBを作成しようとしたところ……エラーが発生しました!

$ docker compose run web bundle exec rake db:create

[+] Running 1/0
 ⠿ Container db-1  Running                                                                                        0.0s
connection to server at "192.168.160.2", port 5432 failed: fe_sendauth: no password supplied
Couldn't create 'myapp_production' database. Please check your configuration.
rake aborted!
ActiveRecord::ConnectionNotEstablished: connection to server at "192.168.160.2", port 5432 failed: fe_sendauth: no password supplied (ActiveRecord::ConnectionNotEstablished)

エラーの原因

エラー文をよく見ると、なぜかproduction用のDBを作成しようとしています。config/database.ymlにはまだproduction用の設定を書いていないので接続エラーになるのは当然です。

また、docker compose upを実行中のシェルに戻ってRailsのログをよく見ると、こちらもproduction環境で起動していました。

web-1  | => Booting Puma
web-1  | => Rails 7.1.2 application starting in production
web-1  | => Run `bin/rails server --help` for more startup options

環境変数でRAILS_ENVを指定していない場合のデフォルトの環境はdevelopment環境になるはずなので、なぜこうなっているのか謎が深まります……。

...

そんなこんなで色々調べた結果、原因はRails7.1の新機能「Dockerfileの自動生成」にあることがわかりました!

https://railsguides.jp/7_1_release_notes.html

新規Railsアプリケーションでは、デフォルトでDockerがサポートされるようになりました(#46762)。 新しいアプリケーションを生成すると、そのアプリケーションにDocker関連ファイルも含まれます。
これらのファイルは、RailsアプリケーションをDockerでproduction環境にデプロイするための基本的なセットアップとして提供されます。重要なのは、これらのファイルは開発用ではないことです。

Dockerfileを確認すると、全く覚えのない内容が! rails newしたときにバッチリ上書きされていたようです。RAILS_ENV="production"という記述があるのでこれではproduction環境で実行されてしまいます。

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/


# 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"]

対応

Dockerfileを元の内容で上書きすれば直りそうですが、せっかくproduction用のDockerfileが手に入ったので、下記リンク先を参考に、Dockerfilecompose.yamlをdevelopment環境用とproduction環境用に分けて作り直すことにしました。なおここではdevelopment環境の構築のみをゴールとします。

https://abillyz.com/watanabe/studies/467

まずはRailsが生成したproduction環境用のDockerfileDockerfile.prodとして退避しておきます。そして、元のDockerfileの内容をDockerfile.devとして作成し直します。

Dockerfile.dev
# syntax=docker/dockerfile:1
FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Configure the main process to run when running the image
CMD ["rails", "server", "-b", "0.0.0.0"]
Dockerfile.prod
# 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

# 以下略

compose.yamlcompose-dev.yamlに改名し、webサービスのビルドに使うDockerfileDockerfile.devに指定します。

compose-dev.yaml
services:
  db:
    image: postgres
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
  web:
    build:
+       context: .
+       dockerfile: Dockerfile.dev
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  db-data:

compose-dev.yamlをもとにビルドします。-fオプションでファイルを指定できます。

$ docker compose -f compose-dev.yaml build

コンテナを起動します。

$ docker compose -f compose-dev.yaml up

PostgreSQLとRailsが起動し、Railsはdevelopment環境で起動しました!

web-1  | => Booting Puma
web-1  | => Rails 7.1.2 application starting in development
web-1  | => Run `bin/rails server --help` for more startup options
web-1  | Puma starting in single mode...
web-1  | * Puma version: 6.4.0 (ruby 3.2.2-p53) ("The Eagle of Durango")
web-1  | *  Min threads: 5
web-1  | *  Max threads: 5
web-1  | *  Environment: development
web-1  | *          PID: 1
web-1  | * Listening on http://0.0.0.0:3000
web-1  | Use Ctrl-C to stop

DB作成コマンドを打ちます。

$ docker compose -f compose-dev.yaml run web rake db:create

[+] Running 1/0
 ⠿ Container db-1  Running                                                                                                                       0.0s
Created database 'myapp_development'
Created database 'myapp_test'

無事developmentとtestの2つのDBが作成されました!

まさかDockerfileが上書きされているとは思わないので、予想外の落とし穴でした。

GitHubで編集を提案

Discussion