または、scaffoldでCRUDが一通りできた!!ドン!!!!
で満足していませんか?
Webアプリを作ったら最終的にはproductionで動かすと思うので、(たとえ練習であっても)developmentで動かして終わるのではなくproductionでも動かしてみるのが良いです。
RAILS_ENV=developmentで動いてもproductionにしたら動かないなんてことはざらにあります。
そこで、この記事ではRailsをproductionで動かすところまでやってみたいと思います。
この記事でやること
RailsをRAILS_ENV=productionで動かします。
空のRailsでやっても面白くないので、Webアプリを作るのであればやると思われるアセット管理やデータベース操作なども実施したいと思います。
具体的には下記を実施します。
- rails new
- ソース管理(GitHub)
- 実行環境(Docker)
- データベース操作(MySQL)
- アセット管理(webpacker)
構成
versions
- Ruby 2.6.3
- Rails 6.0.0
- Docker desktop 2.1.0.2
- docker-compose 3.5
- MySQL 5.7.27
- nginx 1.17.3
インフラ
動作環境はdevelop, production共にローカル環境のDocker上で動かします。
環境ごとの構成は次の通り。
development
webとdbという名前の2つのコンテナを立てます。
webではRailsを実行します。アプリケーションサーバーとwebサーバーの役割を持っており、クライアントがRailsアプリへアクセスするときはこのサーバーへ直接リクエストを送信します。
dbではMySQLを実行します。
production
productionでは負荷などの観点からアプリケーションサーバーとWebサーバーを分けることが一般的です。それを実現するためにWebサーバー(nginx)を別コンテナで立てています。
クライアントはnginxにアクセスし、jsなどの静的ファイルはそのまま返却し、動的処理はアプリケーションサーバーへリクエストを流します。
開発
ここからは実際に開発していきます。
リポジトリ作成
ソース管理は必須だと思うのでまずはリポジトリを作りましょう。
GitHubを使います。
リポジトリをローカルにクローンします。
$ git clone git@github.com:ham0215/rails_in_production.git
Cloning into 'rails_in_production'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
$ cd rails_in_production/
$ ls
LICENSE
Docker(develop)
最終目標はproductionで動かすことですが、まずはdevelopmentで動くようにします。
実行環境のDockerfile、docker-composeを作成します。
Dockerfile
動かすだけなら必須ではない(むしろproductionには入れない方がいい)ですが、開発しやすいようにvimやlocalesなどを入れています。
FROM ruby:2.6.3
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y \
build-essential \
nodejs \
yarn \
vim \
locales \
locales-all \
mysql-client \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENV LANG ja_JP.UTF-8
RUN mkdir -p /app
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN gem update bundler
RUN bundle install
COPY . .
RUN yarn install --check-files
CMD ["rails", "server", "-b", "0.0.0.0"]
docker-compose.yml
Railsを動かす'web'とMySQLを動かす'db'を定義します。
version: '3.7'
services:
web:
build: .
command: bundle exec rails s -b 0.0.0.0
volumes:
- .:/app:cached
ports:
- 3000:3000
depends_on:
- db
tty: true
stdin_open: true
db:
image: mysql:5.7.27
volumes:
- db-volume:/var/lib/mysql
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
db-volume:
rails new
それでは新規Railsを作成しましょう。
rails new
コマンドもDocker内で実行したいのでまずはDockerを起動します。
Dockerfileも記述しているCOPY Gemfile Gemfile.lock ./
を成功させるためにファイルを作成します。
source 'https://rubygems.org'
gem 'rails', '6.0.0'
Gemfile.lockは空ファイルを作成
$ touch Gemfile.lock
準備ができたのでDockerを起動してrails new
を実行、、、の前にコマンドのヘルプを見ておきましょう。
rails new
の時点でいらない機能は抜いておかないと後から消すのは面倒です。
オプションはバージョンごとに結構異なるのできちんと確認しましょう。
$ docker-compose run web bundle exec rails new --help
Usage:
rails new APP_PATH [options]
Options:
[--skip-namespace], [--no-skip-namespace] # Skip namespace (affects only isolated applications)
-r, [--ruby=PATH] # Path to the Ruby binary of your choice
# Default: /usr/local/bin/ruby
-m, [--template=TEMPLATE] # Path to some application template (can be a filesystem path or URL)
-d, [--database=DATABASE] # Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/frontbase/ibm_db/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc)
# Default: sqlite3
[--skip-gemfile], [--no-skip-gemfile] # Don't create a Gemfile
-G, [--skip-git], [--no-skip-git] # Skip .gitignore file
[--skip-keeps], [--no-skip-keeps] # Skip source control .keep files
-M, [--skip-action-mailer], [--no-skip-action-mailer] # Skip Action Mailer files
[--skip-action-mailbox], [--no-skip-action-mailbox] # Skip Action Mailbox gem
[--skip-action-text], [--no-skip-action-text] # Skip Action Text gem
-O, [--skip-active-record], [--no-skip-active-record] # Skip Active Record files
[--skip-active-storage], [--no-skip-active-storage] # Skip Active Storage files
-P, [--skip-puma], [--no-skip-puma] # Skip Puma related files
-C, [--skip-action-cable], [--no-skip-action-cable] # Skip Action Cable files
-S, [--skip-sprockets], [--no-skip-sprockets] # Skip Sprockets files
[--skip-spring], [--no-skip-spring] # Don't install Spring application preloader
[--skip-listen], [--no-skip-listen] # Don't generate configuration that depends on the listen gem
-J, [--skip-javascript], [--no-skip-javascript] # Skip JavaScript files
[--skip-turbolinks], [--no-skip-turbolinks] # Skip turbolinks gem
-T, [--skip-test], [--no-skip-test] # Skip test files
[--skip-system-test], [--no-skip-system-test] # Skip system test files
[--skip-bootsnap], [--no-skip-bootsnap] # Skip bootsnap gem
[--dev], [--no-dev] # Setup the application with Gemfile pointing to your Rails checkout
[--edge], [--no-edge] # Setup the application with Gemfile pointing to Rails repository
[--rc=RC] # Path to file containing extra configuration options for rails command
[--no-rc], [--no-no-rc] # Skip loading of extra configuration options from .railsrc file
[--api], [--no-api] # Preconfigure smaller stack for API only apps
-B, [--skip-bundle], [--no-skip-bundle] # Don't run bundle install
--webpacker, [--webpack=WEBPACK] # Preconfigure Webpack with a particular framework (options: react, vue, angular, elm, stimulus)
[--skip-webpack-install], [--no-skip-webpack-install] # Don't run Webpack install
Runtime options:
-f, [--force] # Overwrite files that already exist
-p, [--pretend], [--no-pretend] # Run but do not make any changes
-q, [--quiet], [--no-quiet] # Suppress status output
-s, [--skip], [--no-skip] # Skip files that already exist
Rails options:
-h, [--help], [--no-help] # Show this help message and quit
-v, [--version], [--no-version] # Show Rails version number and quit
Description:
The 'rails new' command creates a new Rails application with a default
directory structure and configuration at the path you specify.
You can specify extra command-line arguments to be used every time
'rails new' runs in the .railsrc configuration file in your home directory.
Note that the arguments specified in the .railsrc file don't affect the
defaults values shown above in this help message.
Example:
rails new ~/Code/Ruby/weblog
This generates a skeletal Rails installation in ~/Code/Ruby/weblog.
databaseはmysqlを指定。必要ないものは--skip-xxxでスキップします。
今回のコマンドは下記の通り。
$ docker-compose run web bundle exec rails new . --database=mysql --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-storage --skip-spring --skip-turbolinks --skip-bootsnap --skip-test --force
rails new
が成功したらファイルが一通り生成されていると思うのでコミットしておきましょう。
README.mdや.gitignoreもRails用に更新されているので必要に応じて更新しておきましょう。
ちなみに私はvimを使っているので、.gitignoreに*.swp
とtags
は必ず追加します。
一通り揃ったところで一度dockerをビルドしてサーバーを起動してみます。-dでバックグランドで起動しています。
成功したらdocker container ls
でwebとdbのコンテナが表示されるはずです。
$ docker-compose up --build -d
・・・
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3dbc395255eb rails_in_production_web "bundle exec rails s…" 6 seconds ago Up 5 seconds 0.0.0.0:3000->3000/tcp rails_in_production_web_1
443d1337b008 mysql:5.7.27 "docker-entrypoint.s…" 28 hours ago Up 16 minutes 3306/tcp, 33060/tcp rails_in_production_db_1
起動できたらhttp://localhost:3000 にアクセスしてみましょう。
エラーが発生しました。MySQLに接続できていないようですね。
ということでMySQLの設定に進みます。
MySQL
まずは接続情報を修正しましょう。
hostにdocker-composeでつけたMySQLコンテナの名前db
がhost名として使えるので修正します。
usernameやpasswordはroot/(なし)にしているのでそのままでOKです。
--- a/config/database.yml
+++ b/config/database.yml
@@ -15,7 +15,7 @@ default: &default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:
- host: localhost
+ host: db
続いてDBを作成します。
$ docker-compose exec web bundle exec rake db:create
Created database 'app_development'
Created database 'app_test'
ここまでできたら再度 http://localhost:3000/ にアクセスしてみましょう。
無事、Yay! You’re on Rails!が表示されたら成功です。
何もテーブルがないと面白くないのでユーザーを管理するテーブルを作ります。
サンプルなのでscaffoldを使ってサクッと一式作ります。
$ docker-compose exec web bundle exec rails g scaffold user name:string
invoke active_record
create db/migrate/20190914140349_create_users.rb
create app/models/user.rb
invoke resource_route
route resources :users
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke erb
create app/views/users
create app/views/users/index.html.erb
create app/views/users/edit.html.erb
create app/views/users/show.html.erb
create app/views/users/new.html.erb
create app/views/users/_form.html.erb
invoke helper
create app/helpers/users_helper.rb
invoke jbuilder
create app/views/users/index.json.jbuilder
create app/views/users/show.json.jbuilder
create app/views/users/_user.json.jbuilder
invoke assets
invoke scss
create app/assets/stylesheets/users.scss
invoke scss
create app/assets/stylesheets/scaffolds.scss
$ docker-compose exec web bundle exec rake db:migrate
== 20190914140349 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0384s
== 20190914140349 CreateUsers: migrated (0.0389s) =============================
usersテーブルが作成できました。一旦コミットしておきましょう。
これで http://localhost:3000/users にアクセスするとusersのCRUDが一通りできるようになりました。
Webpaker
Rails6からはWebpackerが標準で入るようになったのでjsとcssの管理をそちらに寄せます。
config/webpacker.yml
を見るとソースパスがapp/javascript
となっていますが、scaffoldではscssがapp/assets
に作られているのでapp/javascript
配下に移動します。
$ mv app/assets/stylesheets app/javascript/
cssもwebpackerの対象となるようにapp/javascript/packs/application.js
でcssをimportして、cssを読み込んでいる箇所をWebpackerで生成したcssを読み込むように修正します。
layouts/applicationでusers.scssなど個別のcssまで読み込んでいて微妙ですが、現状ではusersしかないのでとりあえずこれでいきます。
こういう必須じゃないけど微妙だなーと感じるところはissueとして残しておいて、後々対応していきましょう(この記事では対応しません。。。)
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 239b600..459fc07 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -13,3 +13,6 @@ require("channels")
//
// const images = require.context('../images', true)
// const imagePath = (name) => images(name, true)
+import '../stylesheets/application.css'
+import '../stylesheets/scaffolds.scss'
+import '../stylesheets/users.scss'
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index b0772fa..c3f192a 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -5,7 +5,7 @@
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
- <%= stylesheet_link_tag 'application', media: 'all' %>
+ <%= stylesheet_pack_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
</head>
再度/usersを開いてデザインが効いていたらcssの設定は成功です。コミットしておきましょう。
productionで起動
developmentで動くことが確認できたので次はproductionで動かします。
まずはnginxのDockerfile, nginx.confとdocker-composeを作成しました。
server {
listen 80;
server_name web;
# 静的ファイルを格納するディレクトリ。ホストの./publicと同期します。
root /usr/share/nginx/public;
access_log /var/log/default.access.log;
error_log /var/log/default.error.log;
location / {
# 静的ファイルが見つかればそれを返却。なければappへ接続
try_files $uri @app;
}
location @app {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://app:3000;
}
}
FROM nginx:1.17.3
RUN mkdir -p /usr/share/nginx/public
# 設定ファイルを上書き
COPY nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
version: '3.7'
services:
web:
build:
context: .
dockerfile: Dockerfile_nginx
volumes:
- ./public:/usr/share/nginx/public:cached
ports:
- 80:80
depends_on:
- app
app:
build: .
command: bundle exec rails s -b 0.0.0.0
volumes:
- .:/app:cached
depends_on:
- db
tty: true
stdin_open: true
environment:
RAILS_ENV: production
db:
image: mysql:5.7.27
volumes:
- db-volume:/var/lib/mysql
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
volumes:
db-volume:
次にデータベースの設定をします。
productionのデータベース接続情報を修正します。今回はdevelopmentと同じくroot/(なし)にしています。
production:
<<: *default
database: app_production
- username: app
- password: <%= ENV['APP_DATABASE_PASSWORD'] %>
productionのデータベース作成&マイグレーション
rails_in_production:$ docker-compose -f docker-compose.prd.yml exec app bundle exec rake db:create
Created database 'app_production'
rails_in_production:$ docker-compose -f docker-compose.prd.yml exec app bundle exec rake db:migrate
== 20190914140349 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0087s
== 20190914140349 CreateUsers: migrated (0.0089s) =============================
それでは起動してみましょう。3つのコンテナが起動したら成功です。
$ docker-compose -f docker-compose.prd.yml up --build -d
・・・
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f0771d9c7e2 rails_in_production_web "nginx -g 'daemon of…" 28 seconds ago Up 26 seconds 0.0.0.0:80->80/tcp rails_in_production_web_1
844d4e71e549 rails_in_production_app "bundle exec rails s…" 31 seconds ago Up 29 seconds rails_in_production_app_1
443d1337b008 mysql:5.7.27 "docker-entrypoint.s…" 5 days ago Up 3 hours 3306/tcp, 33060/tcp rails_in_production_db_1
今回は80ポートなので http://localhost/users でアクセスします。
うーん、エラーになりました。ログを確認しましょう。
# nginx: access.log
$ docker-compose -f docker-compose.prd.yml exec web tail -f /var/log/default.access.log
172.23.0.1 - - [17/Sep/2019:07:55:27 +0000] "GET /users HTTP/1.1" 500 1635 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
# nginx: error.log
$ docker-compose -f docker-compose.prd.yml exec web tail -f /var/log/default.error.log
# rails: production.log
$ docker-compose -f docker-compose.prd.yml exec app tail -f log/production.log
I, [2019-09-17T07:55:27.766706 #1] INFO -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] Started GET "/users" for 172.23.0.4 at 2019-09-17 07:55:27 +0000
I, [2019-09-17T07:55:27.768054 #1] INFO -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] Processing by UsersController#index as HTML
I, [2019-09-17T07:55:27.769031 #1] INFO -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] Rendering users/index.html.erb within layouts/application
D, [2019-09-17T07:55:27.772991 #1] DEBUG -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] (0.3ms) SET NAMES utf8mb4, @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'), @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
D, [2019-09-17T07:55:27.774112 #1] DEBUG -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] User Load (0.4ms) SELECT `users`.* FROM `users`
I, [2019-09-17T07:55:27.774743 #1] INFO -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] Rendered users/index.html.erb within layouts/application (Duration: 5.3ms | Allocations: 1132)
I, [2019-09-17T07:55:27.775551 #1] INFO -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5] Completed 500 Internal Server Error in 7ms (ActiveRecord: 0.7ms | Allocations: 1660)
F, [2019-09-17T07:55:27.777753 #1] FATAL -- : [9d8bb416-82f3-4e28-9d38-8339c90ba0c5]
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] ActionView::Template::Error (Webpacker can't find application in /app/public/packs/manifest.json. Possible causes:
1. You want to set webpacker.yml value of compile to true for your environment
unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.
Your manifest contains:
{
}
):
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 5: <%= csrf_meta_tags %>
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 6: <%= csp_meta_tag %>
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 7:
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 8: <%= stylesheet_pack_tag 'application', media: 'all' %>
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 9: <%= javascript_pack_tag 'application' %>
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 10: </head>
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] 11:
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5]
[9d8bb416-82f3-4e28-9d38-8339c90ba0c5] app/views/layouts/application.html.erb:8
Railsのログに色々出ていますが、manifest.jsonが見つからないのが問題みたいです。
webpacker.ymlを見るとproductionではcompile: false
になっています。ここがtrueだとアクセス時にアセットをコンパイルしてくれるのですが、都度コンパイルしているとパフォーマンスが悪くなってしまうのでproductionではfalseになっています。
# Production depends on precompilation of packs prior to booting for performance.
compile: false
このエラーを回避するために事前にアセットをコンパイルしておく必要があります。原因がわかったのでアセットをコンパイルしましょう。
コマンドをコンテナに入って直接実行してもよいのですが次回以降再現できなくなってしまう可能性があるのでDockerfileにコマンドを追加してbuildからやり直します。
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,5 +25,6 @@ RUN gem update bundler
RUN bundle install
COPY . .
RUN yarn install --check-files
+RUN bundle exec rails assets:precompile
CMD ["rails", "server", "-b", "0.0.0.0"]
再ビルド
$ docker-compose -f docker-compose.prd.yml up --build -d
それでは改めてアクセスしてみます。
無事、productionで動かすことができました。登録・更新・削除もできているようです。
おまけ
productionでwebサーバーを使わない場合
今回はnginxを使いましたが、使わないことも可能です。
ただ、productionの設定に下記が記載されているため、アセットを配信するためには環境変数にRAILS_SERVE_STATIC_FILES=true
を設定しておく必要があります。
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
リポジトリ
この記事で作成したリポジトリです。
https://github.com/ham0215/rails_in_production