Docker + Rails + Puma + Nginx + MySQL

Dockerに興味があったものの、コマンドを叩く以外はほとんど手を出す機会がなかったので、
夏季休暇の間にしっかりと基本を身につけるべく、検証用にRails環境を構築してみました。
思ってた以上に悪戦苦闘したので、記憶からすっ飛ばないようにちゃんと記事として残しておこうと思います。

## __前提__

- 作業はすべて [ConoHaVPS](https://www.conoha.jp) の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 として作成しました。

```console
$ sudo mkdir -p /var/webapp
$ sudo chown -R $USER:$USER /var/webapp
```

### Nginxコンテナディレクトリ

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

### 環境変数用ディレクトリ

```console
$ mkdir /var/webapp/environments
```

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

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

### Rails用Dockerfile

```console
$ 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

```console
$ vim Gemfile
```

``` ruby:Gemfile
source 'https://rubygems.org'
gem 'rails', '5.2.0'
```

### Gemfile.lock

```console
$ touch Gemfile.lock
```

中身は空でOK

### Nginx用Dockerfile

```console
$ 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設定ファイル

```console
$ vim containers/nginx/nginx.conf
```

```nginx: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接続用の情報ファイル

```console
$ vim environments/db.env
```

```ini:db.env
MYSQL_ROOT_PASSWORD=db_root_password
MYSQL_USER=user_name
MYSQL_PASSWORD=user_password
```

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

### docker-compose.yml

Dockerに触れて一番頭を悩ませたのがデータの永続化でしたが、
自分はほとんどトップレベルの名前付き Volume で対応するようにしました。
ちなみに、バージョン3からは volumes_from が削除されたため使えません。（[詳しくはここ](https://docs.docker.com/compose/compose-file/compose-versioning/#upgrading)）

```console
$ 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が生成されます。

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

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

```yaml
volumes:
  .:/webapp
```

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

### 権限の変更

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

```console
$ sudo chown -R $USER:$USER .
```

### puma.rbの編集

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

```console
$ cp /dev/null config/puma.rb
```

```console
$ 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の編集

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

```console
$ cp /dev/null config/database.yml
```

```console
$ 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接続用の情報ファイル](#db%E6%8E%A5%E7%B6%9A%E7%94%A8%E3%81%AE%E6%83%85%E5%A0%B1%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB) で定義した環境変数名を設定してくだい。

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

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

__イメージのビルド__
docker-compose.yml の各サービス（appとかdbとかweb）の build に指定されている Dockerfile を元にイメージを作成します。
build ではなく image が定義されている場合は DockerHub からイメージを pull してきます。

```console
$ docker-compose build
```

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

```console
$ 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
```

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

```console
$ docker-compose up -d
```

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

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

```console
$ 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接続用の情報ファイル](#db%E6%8E%A5%E7%B6%9A%E7%94%A8%E3%81%AE%E6%83%85%E5%A0%B1%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB) に設定した MYSQL_USER の値に置き換えてください。

```console
$ vim db/grant_user.sql
```

``` sql:grant_user.sql
GRANT ALL PRIVILEGES ON *.* TO 'user_name'@'%';
FLUSH PRIVILEGES;
```

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

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

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

```console
$ 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 で下記のような環境変数が設定されていると、
ビルド時に指定されたユーザーが作成されます。

```yaml
environment:
  MYSQL_USER: user_name
  MYSQL_PASSWORD: user_password
```

また、MYSQL_USER と MYSQL_PASSWORD は必ずセットで記述しないとダメです。
（[詳しくはEnvironment Variablesを参照](https://hub.docker.com/r/library/mysql/)）
自分はこれらの設定をハードコーディングしたくなかったので、別ファイル（db.env）に切り出して読み込むようにしました。
あと .gitignore で除外できるので！

最初、extends キーワードでこれらのシークレット情報を記述した yml を読み込もうとしたのですが、
version3では extends が削除されていて使用できませんでした。

### DBの作成

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

```console
$ docker-compose exec app rails db:create
```

ここまでで一旦環境構築は完了です。
ブラウザにアクセスしてRailsのWelcomeページが表示できるか確認しておきましょう。

http://localhost
![スクリーンショット 2017-08-25 14.40.21.png](https://qiita-image-store.s3.amazonaws.com/0/35509/c501d2d1-2074-22f7-ad61-19fef46ec2a7.png)


##__6. scaffoldでアプリのベースを構築__

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

### scaffoldの実行

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

### マイグレートの実行

```console
$ docker-compose exec app rails db:migrate
```

### 確認

ブラウザを開いて http://localhost/users にアクセスしてみてください。
下記の通りに表示されればOKです。
![スクリーンショット 2017-08-25 14.47.32.png](https://qiita-image-store.s3.amazonaws.com/0/35509/9a4a7214-c075-e599-a329-e6a23ff3b66e.png)

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

![crud.png](https://qiita-image-store.s3.amazonaws.com/0/35509/9f647db8-a415-864a-074f-1481e0ddd517.png)


## __7. おまけ__

### docker-composeコマンド

コンテナの停止

```console
$ docker-compose stop
```

コンテナの停止と削除

```console
$ docker-compose down
```

コンテナの起動

```console
$ docker-compose start
```

MySQLへの接続

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

### dockerコマンド

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

```console
$ docker rm `docker ps -a -q`
```

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

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

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

```console
$ docker volume prune
```

## __8. まとめ__

そもそもこの構築手順が Docker のお作法的に良いのか悪いのかはわかりませんが、
ひとまず１コンテナ１プロセスに則って実現できたのは良かった。
なにか間違っている説明や不明点などがあったらご指摘をお願いします！

## __参考にしたサイト__

- [Quickstart: Compose and Rails](https://docs.docker.com/compose/rails/)
- [Nginx + Rails (Puma) on Docker のいくつかの実用パターン](http://qiita.com/na-o-ys/items/1a863419e1f6c3063ace)
- [Setup Docker for Rails Application using Postgres (Mysql) with nginx](https://bestcoder.info/programming/2017/07/16/setup-docker-for-rails-application-using-postgres-mysql.html)
- [Compose file version 3 reference](https://docs.docker.com/compose/compose-file/)
- [開発環境をDockerに乗せる方法とメリットを3ステップで学ぶチュートリアル](http://qiita.com/KeitaMoromizato/items/ae1a57fc62b41b942d71)