Rails
nginx
puma
Docker
mysql5.7

Docker + Rails + Puma + Nginx + MySQL

Dockerに興味があったものの、コマンドを叩く以外はほとんど手を出す機会がなかったので、

夏季休暇の間にしっかりと基本を身につけるべく、検証用にRails環境を構築してみました。

思ってた以上に悪戦苦闘したので、記憶からすっ飛ばないようにちゃんと記事として残しておこうと思います。


前提


  • 作業はすべて ConoHaVPS のUbuntu16.04上でやってます。

  • OS上にすでに Docker と docker-compose がインストール済みという前提で話を進めます。

  • この環境では フロント に Nginx を配置して、バックエンドの Puma にプロキシしています。


バージョン


  • ホストOS(Ubuntu16.04 Xenial Xerus)

  • Docker(17.06.1-ce)

  • docker-compose(1.11.2)

  • Nginx(1.15.8)

  • Ruby(2.5.1)

  • Ruby on Rails(5.2.0)

  • MySQL(5.7)


ディレクトリ構成

/var/webapp

├── containers
│ └── nginx
│ ├── Dockerfile
│ └── nginx.conf
├── docker-compose.yml
├── Dockerfile
├── environments
│ └── db.env
├── Gemfile
└── Gemfile.lock


1. 各ディレクトリの作成


アプリケーションルート

どこでもいいんですが、自分は /var/webapp として作成しました。

$ sudo mkdir -p /var/webapp

$ sudo chown -R $USER:$USER /var/webapp


Nginxコンテナディレクトリ

$ mkdir -p /var/webapp/containers/nginx


環境変数用ディレクトリ

$ mkdir /var/webapp/environments


2. コンテナ生成のための各ファイルを作成

※ 以降はアプリケーションルート内で行う


Rails用Dockerfile

$ vim Dockerfile


Dockerfile

FROM ruby:2.5.1

# リポジトリを更新し依存モジュールをインストール
RUN apt-get update -qq && \
apt-get install -y build-essential \
nodejs

# ルート直下にwebappという名前で作業ディレクトリを作成(コンテナ内のアプリケーションディレクトリ)
RUN mkdir /webapp
WORKDIR /webapp

# ホストのGemfileとGemfile.lockをコンテナにコピー
ADD Gemfile /webapp/Gemfile
ADD Gemfile.lock /webapp/Gemfile.lock

# bundle installの実行
RUN bundle install

# ホストのアプリケーションディレクトリ内をすべてコンテナにコピー
ADD . /webapp

# puma.sockを配置するディレクトリを作成
RUN mkdir -p tmp/sockets



Gemfile

$ vim Gemfile


Gemfile

source 'https://rubygems.org'

gem 'rails', '5.2.0'


Gemfile.lock

$ touch Gemfile.lock

中身は空でOK


Nginx用Dockerfile

$ vim containers/nginx/Dockerfile


Dockerfile

FROM nginx:1.15.8

# インクルード用のディレクトリ内を削除
RUN rm -f /etc/nginx/conf.d/*

# Nginxの設定ファイルをコンテナにコピー
ADD nginx.conf /etc/nginx/conf.d/webapp.conf

# ビルド完了後にNginxを起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf



Nginx設定ファイル

$ vim containers/nginx/nginx.conf


nginx.conf

# プロキシ先の指定

# Nginxが受け取ったリクエストをバックエンドのpumaに送信
upstream webapp {
# ソケット通信したいのでpuma.sockを指定
server unix:///webapp/tmp/sockets/puma.sock;
}

server {
listen 80;
# ドメインもしくはIPを指定
server_name example.com [or 192.168.xx.xx [or localhost]];

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

# ドキュメントルートの指定
root /webapp/public;

client_max_body_size 100m;
error_page 404 /404.html;
error_page 505 502 503 504 /500.html;
try_files $uri/index.html $uri @webapp;
keepalive_timeout 5;

# リバースプロキシ関連の設定
location @webapp {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://webapp;
}
}



DB接続用の情報ファイル

$ vim environments/db.env


db.env

MYSQL_ROOT_PASSWORD=db_root_password

MYSQL_USER=user_name
MYSQL_PASSWORD=user_password

パスワードとユーザー名は適宜変更してください。


docker-compose.yml

Dockerに触れて一番頭を悩ませたのがデータの永続化でしたが、

自分はほとんどトップレベルの名前付き Volume で対応するようにしました。

ちなみに、バージョン3からは volumes_from が削除されたため使えません。(詳しくはここ

$ vim docker-compose.yml


docker-compose.yml

version: '3'

services:
app:
build:
context: .
env_file:
- ./environments/db.env
command: bundle exec puma -C config/puma.rb
volumes:
- .:/webapp
- public-data:/webapp/public
- tmp-data:/webapp/tmp
- log-data:/webapp/log
depends_on:
- db
db:
image: mysql:5.7
env_file:
- ./environments/db.env
volumes:
- db-data:/var/lib/mysql
web:
build:
context: containers/nginx
volumes:
- public-data:/webapp/public
- tmp-data:/webapp/tmp
ports:
- 80:80
depends_on:
- app
volumes:
public-data:
tmp-data:
log-data:
db-data:


3. Railsプリケーションの生成と編集


Railsの生成

ここまでの準備ができたら rails new でアプリケーションを生成します。

具体的には下記のコマンドを打つとappコンテナ内で rails new が実行されRailsが生成されます。

$ docker-compose run --rm app rails new . --force --database=mysql --skip-bundle

また、docker-compose.yml の app に定義している volumes にて

volumes:

.:/webapp

ホストのカレントディレクトリ(この場合アプリケーションルート)とコンテナ内の /webapp ディレクトリを繋ぐことで、コンテナ側で生成されたRailsをホスト側で参照することができます。


権限の変更

生成されたRailsアプリの所有権が root:root となっているので(Dockerの操作は基本すべてroot権限で実行されるため)、現在のログインユーザーに変更しておきます。

$ sudo chown -R $USER:$USER .


puma.rbの編集

コメント行がたくさんあって編集がけっこう面倒なので、ファイルをゼロバイトにしたあとで、あらかじめ用意しておいたものをコピペすると楽です。

$ cp /dev/null config/puma.rb

$ vim config/puma.rb


puma.rb

threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i

threads threads_count, threads_count
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"

stdout_redirect "#{app_root}/log/puma.stdout.log", "#{app_root}/log/puma.stderr.log", true



database.ymlの編集

こちらもファイルをゼロバイトにしてからコピペ。

$ cp /dev/null config/database.yml

$ vim config/database.yml


database.yml

default: &default

adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV.fetch('MYSQL_USER') { 'root' } %>
password: <%= ENV.fetch('MYSQL_PASSWORD') { 'password' } %>
host: db

development:
<<: *default
database: webapp_development

test:
<<: *default
database: webapp_test


設定の中で MYSQL_USER と MYSQL_PASSWORD は DB接続用の情報ファイル で定義した環境変数名を設定してくだい。


4. イメージのビルドとコンテナの起動

いよいよコンテナを作ってそれらを起動してみます。

イメージのビルド

docker-compose.yml の各サービス(appとかdbとかweb)の build に指定されている Dockerfile を元にイメージを作成します。

build ではなく image が定義されている場合は DockerHub からイメージを pull してきます。

$ docker-compose build

ビルドが完了したら一旦確認してみます。

$ docker images

REPOSITORY          TAG                IMAGE ID            CREATED             SIZE

webapp_web latest 6c3ae1c68ff0 18 hours ago 107MB
webapp_app latest fe241efeb525 18 hours ago 820MB
<none> <none> 36b3d89bfec7 18 hours ago 781MB
ruby 2.4.1 15b96d1e91df 2 days ago 679MB
nginx 1.13.3 b8efb18f159b 4 weeks ago 107MB
mysql 5.7 c73c7527c03a 4 weeks ago 412MB

コンテナの起動

ビルドが完了しイメージができたら下記のコマンドで一気にコンテナ(サービス)を立ち上げます。

$ docker-compose up -d

オプションに -d を指定するとデーモンとしてバックグラウンドで起動します。

コンテナが起動しているか確認します。

$ docker-compose ps

   Name                  Command               State         Ports

-------------------------------------------------------------------------
webapp_app_1 bundle exec puma -C config ... Up
webapp_db_1 docker-entrypoint.sh mysqld Up 3306/tcp
webapp_web_1 /bin/sh -c /usr/sbin/nginx ... Up 0.0.0.0:80->80/tcp


5. DB設定


権限の付与

DBの操作は一般ユーザーで行いたいので、すでに作成されている一般ユーザーに対して実行権限を付与しておきます。

GRANT文を記述したSQLファイルを作成します。

user_name は DB接続用の情報ファイル に設定した MYSQL_USER の値に置き換えてください。

$ vim db/grant_user.sql


grant_user.sql

GRANT ALL PRIVILEGES ON *.* TO 'user_name'@'%';

FLUSH PRIVILEGES;

ホストからdbコンテナに向けてクエリを流し込みます。

$ docker-compose exec db mysql -u root -p -e"$(cat db/grant_user.sql)"

念のため権限が付与できたか確認します。

$ docker-compose exec db mysql -u user_name -p -e"show grants;"

+------------------------------------------------+

| Grants for user_name@% |
+------------------------------------------------+
| GRANT ALL PRIVILEGES ON *.* TO 'user_name'@'%' |
+------------------------------------------------+

ちなみに、 docker-compose.yml の db で下記のような環境変数が設定されていると、

ビルド時に指定されたユーザーが作成されます。

environment:

MYSQL_USER: user_name
MYSQL_PASSWORD: user_password

また、MYSQL_USER と MYSQL_PASSWORD は必ずセットで記述しないとダメです。

詳しくはEnvironment Variablesを参照

自分はこれらの設定をハードコーディングしたくなかったので、別ファイル(db.env)に切り出して読み込むようにしました。

あと .gitignore で除外できるので!

最初、extends キーワードでこれらのシークレット情報を記述した yml を読み込もうとしたのですが、

version3では extends が削除されていて使用できませんでした。


DBの作成

railsコマンドでDBを作成します。

$ docker-compose exec app rails db:create

ここまでで一旦環境構築は完了です。

ブラウザにアクセスしてRailsのWelcomeページが表示できるか確認しておきましょう。

http://localhost

スクリーンショット 2017-08-25 14.40.21.png


6. scaffoldでアプリのベースを構築

最後に名前とメールアドレスを持った簡単な CRUD を作成してみます。


scaffoldの実行

$ docker-compose exec app rails g scaffold User name:string email:string


マイグレートの実行

$ docker-compose exec app rails db:migrate


確認

ブラウザを開いて http://localhost/users にアクセスしてみてください。

下記の通りに表示されればOKです。

スクリーンショット 2017-08-25 14.47.32.png

CRUD操作が問題なく実行できるか試してみてください。

crud.png


7. おまけ


docker-composeコマンド

コンテナの停止

$ docker-compose stop

コンテナの停止と削除

$ docker-compose down

コンテナの起動

$ docker-compose start

MySQLへの接続

$ docker-compose exec service_name mysql -u user_name -p [-D DB名]


dockerコマンド

停止中のコンテナをまとめて削除

$ docker rm `docker ps -a -q`

参照されていないイメージをまとめて削除

docker rmi `docker images | sed -ne '2,$p' -e 's/  */ /g' | awk '{print $1":"$2}'`

使用されていないボリュームの削除

$ docker volume prune


8. まとめ

そもそもこの構築手順が Docker のお作法的に良いのか悪いのかはわかりませんが、

ひとまず1コンテナ1プロセスに則って実現できたのは良かった。

なにか間違っている説明や不明点などがあったらご指摘をお願いします!


参考にしたサイト