Docker + Rails7.1 の環境構築で詰まったポイント
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に掲載されていたこちらのサンプルをもとに環境構築を進めていきます。
プロジェクトの定義
Dockerfile
を作成。Rubyのバージョンは現時点での最新安定版3.2.2にします。
# 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がインストールされていました。
source 'https://rubygems.org'
gem 'rails'
空のGemfile.lock
を作成。
$ touch Gemfile.lock
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の管理下におきたかったので、マウントの方法をボリュームマウントに変更しました。
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との接続情報を追加します。
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の自動生成」にあることがわかりました!
新規Railsアプリケーションでは、デフォルトでDockerがサポートされるようになりました(#46762)。 新しいアプリケーションを生成すると、そのアプリケーションにDocker関連ファイルも含まれます。
これらのファイルは、RailsアプリケーションをDockerでproduction環境にデプロイするための基本的なセットアップとして提供されます。重要なのは、これらのファイルは開発用ではないことです。
Dockerfile
を確認すると、全く覚えのない内容が! rails new
したときにバッチリ上書きされていたようです。RAILS_ENV="production"
という記述があるのでこれではproduction環境で実行されてしまいます。
# 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 /usr/local/bundle /usr/local/bundle
COPY /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
が手に入ったので、下記リンク先を参考に、Dockerfile
とcompose.yaml
をdevelopment環境用とproduction環境用に分けて作り直すことにしました。なおここではdevelopment環境の構築のみをゴールとします。
まずはRailsが生成したproduction環境用のDockerfile
はDockerfile.prod
として退避しておきます。そして、元のDockerfile
の内容を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"]
# 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.yaml
をcompose-dev.yaml
に改名し、web
サービスのビルドに使うDockerfile
をDockerfile.dev
に指定します。
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
が上書きされているとは思わないので、予想外の落とし穴でした。
Discussion