ローカルでRails開発環境を構築する
仕事でRailsが必要になったけど触ったのが昔過ぎて覚えていないので、一から環境作りについて学んでいく。
せっかくなのでメモに残す。
要件
- Dockerを使う
- ローカル環境をあんまり汚したくない
- データが揮発しないよう、Railsアプリ+DBそれぞれでボリュームを設定する
- direnvを使って環境変数を外に出す
- 漏洩して問題ないとはいえ、GitHubに機密情報が保存されるのは避けたい
- Railsは最新の7系、DBは慣れているMySQLを使う
Dockerコンテナの準備
外部の記事 + ChatGPTに頼りながらファイルを整備。
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"]
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ファイルを使って設定。
direnv
MYSQL_ROOT_PASSWORD=xxxxxx
MYSQL_DATABASE=xxxxxx
MYSQL_USER=xxxxxx
MYSQL_PASSWORD=xxxxxx
Gemfileも用意。
とりあえずRailsだけ。
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も生成されるようになったみたい。
# 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"]
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で指定した環境変数を利用。
勉強したいだけで本番運用する気も無いので development
と production
は全く同じにしてとりあえず動くようにした。
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を読み込むよう指定。
version: '3'
services:
db:
+ env_file:
+ - .env
...
web:
+ env_file:
+ - .env
...
「.envは自動で読み込むのでは?」と思ったけど、仕様を勘違いしていただけだった。
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を修正。
+ 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が返ってきた。
コンテナのホストの設定の問題かな?と思って調べたらそれっぽいのが引っかかる。
要約すると「Railsは自動で localhost
(= 127.0.0.1
)でlistenするからコンテナ内部では 0.0.0.0
でlistenするように修正しろ」という話。
確かに過ぎる。
Dockerfile内のRails起動コマンドにオプションを付け加えて 0.0.0.0
で立ち上がるように修正。
+ 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
で自動生成されたファイルにその答えがあった。
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
./bin/rails db:prepare
fi
db:prepare
がDBのセットアップを済ませてくれるみたい。
公式にもその記述があった。
データベースが未作成の場合は、bin/rails db:setupと同様に動作します。
データベースが存在しているがテーブルが未作成の場合は、「スキーマの読み込み」「pending中のマイグレーションの実行」「更新されたスキーマのダンプ」「seedデータの読み込み」を行います。
データベースとテーブルが存在しているが、seedデータがまだ読み込まれていない場合は、seedデータの読み込みだけを行います。
データベースの作成、テーブルの作成、seedデータの読み込みがすべて完了している場合は、何も行いません。
なるほどねー。