Open9

ローカルでRails開発環境を構築する

やましたやました

仕事でRailsが必要になったけど触ったのが昔過ぎて覚えていないので、一から環境作りについて学んでいく。
せっかくなのでメモに残す。

やましたやました

要件

  • Dockerを使う
    • ローカル環境をあんまり汚したくない
    • データが揮発しないよう、Railsアプリ+DBそれぞれでボリュームを設定する
  • direnvを使って環境変数を外に出す
    • 漏洩して問題ないとはいえ、GitHubに機密情報が保存されるのは避けたい
  • Railsは最新の7系、DBは慣れているMySQLを使う
やましたやました

Dockerコンテナの準備

外部の記事 + ChatGPTに頼りながらファイルを整備。

Dockerfile
FROM ruby:3.2.2

WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

RUN bundle install

CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:latest
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    ports:
      - "3306:3306"
  web:
    build: .
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db

volumes:
  db-data:

ChatGPTの出力に MYSQL_ROOT_PASSWORD の記述について書かれていなかったので追記。
MYSQL_ALLOW_EMPTY_PASSWORD 手もあったけど、rootにパスワード無しで入れるのは好きじゃないのでパスワードを設定する。

環境変数はdirenv + .envファイルを使って設定。

.envrc
direnv
.env
MYSQL_ROOT_PASSWORD=xxxxxx
MYSQL_DATABASE=xxxxxx
MYSQL_USER=xxxxxx
MYSQL_PASSWORD=xxxxxx

Gemfileも用意。
とりあえずRailsだけ。

Gemfile
source "https://rubygems.org"
ruby "3.2.2"

gem "rails", "~> 7.1.2"

Gemfile.lockは空で用意。

ここまできたらコンテナをビルドする。

docker-compose build
やましたやました

Railsアプリケーション作成

ビルドが無事に終わったらRailsアプリケーションを構築。

docker-compose run --no-deps --rm web rails new . --force --database=mysql

--no-deps オプションをつけることで、依存するサービス(ここだと db )を再作成することを防いでくれる。

Railsアプリケーションが構築されたら手元にRailsのディレクトリとファイル群が生成される。
またDockerfile、Gemfile、Gemfile.lockが書き換わった。
「何でDockerfileが?」と思ったけど、Railsの7.1からDockerfileも生成されるようになったみたい。

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 default-libmysqlclient-dev git 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 default-mysql-client libvips && \
    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"]
Gemfile
source "https://rubygems.org"

ruby "3.2.2"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.2"

# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
gem "sprockets-rails"

# Use mysql as the database for Active Record
gem "mysql2", "~> 0.5"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"

# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ]
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"

  # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
  # gem "rack-mini-profiler"

  # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
  # gem "spring"
end

group :test do
  # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
  gem "capybara"
  gem "selenium-webdriver"
end
やましたやました

Railsアプリケーション立ち上げ(失敗編 vol.1)

このままだとDBに繋がらないのでDB設定を修正する。
ここで.envで指定した環境変数を利用。

勉強したいだけで本番運用する気も無いので developmentproduction は全く同じにしてとりあえず動くようにした。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: <%= ENV['MYSQL_DATABASE'] %>
  username: <%= ENV['MYSQL_USER'] %>
  password: <%= ENV['MYSQL_PASSWORD'] %>
  host: db

development:
  <<: *default

production:
  <<: *default

rails new でDockerfileとGemfileが更新されたので、コンテナを再ビルドし立ち上げる。

docker-compose build
docker-compose up -d

が、どうやら web の方が起動していない。なぜ。

やましたやました

Railsアプリケーション立ち上げ(失敗編 vol.2)

docker-compose logs web でログを見たら何か出ていた。

web-1  | bin/rails aborted!
web-1  | ActiveRecord::DatabaseConnectionError: There is an issue connecting to your database with your username/password, username: . (ActiveRecord::DatabaseConnectionError)
web-1  |
web-1  | Please check your database configuration to ensure the username/password are valid.
web-1  |
web-1  |
web-1  | Caused by:
web-1  | Mysql2::Error::ConnectionError: Access denied for user 'rails'@'172.18.0.3' (using password: NO) (Mysql2::Error::ConnectionError)
web-1  |
web-1  | Tasks: TOP => db:prepare
web-1  | (See full trace by running task with --trace)

どうやら環境変数が読み込めていなかった模様。
docker-compose.ymlに.envを読み込むよう指定。

docker-compose.yml
version: '3'
services:
  db:
+  env_file:
+    - .env
    ...
  web:
+  env_file:
+    - .env
    ...

「.envは自動で読み込むのでは?」と思ったけど、仕様を勘違いしていただけだった。

https://matsuand.github.io/docs.docker.jp.onthefly/compose/environment-variables/
https://qiita.com/SolKul/items/989727aeeafcae28ecf7

env_file を追記した状態でコンテナ再ビルド&立ち上げ。
これでいける…!

と思ったのに今度は http://localhost:3000 に繋がらず。 https://localhost:3000 にアクセスされる。
ログにはPumaのエラーが出ていた。

web-1  | 2023-12-17 14:09:46 +0000 HTTP parse error, malformed request: #<Puma::HttpParserError: Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?>
やましたやました

Railsアプリケーション立ち上げ(失敗編 vol.3)

先のPumaのエラーは「Railsがproductionモードで動いていたから自動で https にリダイレクトされてしまっていた」という問題っぽい。
rails new で自動生成されたDockerfileを修正。

Dockerfile
+ ENV RAILS_ENV="development" \
- ENV RAILS_ENV="production" \

再度コンテナをビルドし立ち上げる。
今度こそいける…と思ったのにまだ http://localhost:3000 に繋がらない。なぜ。

やましたやました

Railsアプリケーション立ち上げ(成功編)

何が問題なのかを確認。
今回はhttpsにリダイレクトされていない。

curlを叩いてみると以下のエラーが。

curl: (56) Recv failure: Connection reset by peer

コンテナ内からアクセスできるかな?と思い、 docker-compose exec web bash でコンテナに入ってcurlを叩く。
するとRails初期画面とおぼしきHTMLが返ってきた。

コンテナのホストの設定の問題かな?と思って調べたらそれっぽいのが引っかかる。

https://stackoverflow.com/questions/27806631/docker-rails-app-fails-to-be-served-curl-56-recv-failure-connection-reset

要約すると「Railsは自動で localhost (= 127.0.0.1 )でlistenするからコンテナ内部では 0.0.0.0 でlistenするように修正しろ」という話。
確かに過ぎる。

Dockerfile内のRails起動コマンドにオプションを付け加えて 0.0.0.0 で立ち上がるように修正。

Dockerfile
+ CMD ["./bin/rails", "server", "-b", "0.0.0.0"]
- CMD ["./bin/rails", "server"]

再度ビルド&立ち上げ。

無事起動できていた。やったね!
というか初期Dockerfileには 0.0.0.0 でlistenするように指定したので完全に節穴だったというしかない。

変なところで引っかかりまくったなぁ。
とりあえずこれでようやく開発ができる。

やましたやました

余談

ChatGPTにも参考にした資料にも↓のコマンドを叩いてDBをセットアップするように言われていたけど、叩かなくても動いた。

docker-compose run --rm web bundle exec rake db:create

何でだろうとファイルを見ていったら rails new で自動生成されたファイルにその答えがあった。

docker-entrypoint
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
  ./bin/rails db:prepare
fi

db:prepare がDBのセットアップを済ませてくれるみたい。
公式にもその記述があった。

データベースが未作成の場合は、bin/rails db:setupと同様に動作します。
データベースが存在しているがテーブルが未作成の場合は、「スキーマの読み込み」「pending中のマイグレーションの実行」「更新されたスキーマのダンプ」「seedデータの読み込み」を行います。
データベースとテーブルが存在しているが、seedデータがまだ読み込まれていない場合は、seedデータの読み込みだけを行います。
データベースの作成、テーブルの作成、seedデータの読み込みがすべて完了している場合は、何も行いません。

https://railsguides.jp/active_record_migrations.html#データベースを準備する

なるほどねー。