Rails×Unicorn×Nginx(Docker)

「Railsを本番環境で動かす際に、UnicornとNginxを使用している人が多いけど、一体なんでだろう？」

そう疑問に思ったので、Rails×Unicorn×Nginxについて調べてまとめました。

## Railsとアプリケーションサーバー
本題に入るまえに、Railsとアプリケーションサーバーについて解説します。

通常、$rails sでアプリケーションサーバーの「rack」が起動し、Railsが動き出します。

rackとは、rubyのコードで書かれたWebフレームワークとWebサーバーを繋ぐもので、シンプルに言えばRailsを動かすアプリケーションサーバです。

ただし、高負荷に弱いというデメリットがあるそうで、その問題を解決するための方法がRails×Unicorn×Nginxになります。

### Rails×Nginx×Unicornの仕組み
まずNginxとUnicornについて簡単に解説します。

- Nginxは、高負荷に強いWebサーバ。
- Unicornは、Rack専用のWebサーバ。

Webサーバーがダブっていますが、これで合っています。

なぜなら、NginxはRackに繋ぐことができないので、専用サーバーであるUnicornなどで中継させる必要があるからです。

また、Nginxのバッファリングにより、1つづつしか処理を捌けないUnicornに、通信状態のわるいクライアントからの処理が送られなくなっています。

バッファリングとは
通信速度の減速や中断などに備え、記憶領域にデータを保存する機能。

## Unicornについて
Unicornは、プロセス(工程)がmaster1つと複数のworkerに分かれていますが、ソースコードを読み込むのはmasterだけなので起動が早いです。

処理の流れをまとめると以下のようになります。

1. Nginxからリクエストが届く
2. unicornが起動する
3. masterプロセス生成
4. ソースコードの読込＆worker作成
5. Nginxから流れてきた処理を捌いていく

上記のように、Unicornは負荷分散をおこない、処理をRack(Rails)に送ります。

最終的にRack側から返答が返ってきたら、それをクライアントを返して終了です。

### Unicornの導入方法
まずはUnicornをインストールするために、gemfileに以下のコードを追加。

```
gem 'unicorn'
```

続いてUnicorn設定ファイルを作成するので、configディレクトリ下にunicorn.rbファイルを作成。

※参考にさせて頂いたサイトのコードをみてメモっているだけです。詳細は[こちら](URL "https://qiita.com/E6YOteYPzmFGfOD/items/509dbabeb20bf2487283#3%E5%90%84%E7%A8%AE%E8%A8%AD%E5%AE%9Adatabaseymlunicornconfrb")
のサイトが分かりやすかったです。
```
$worker  = 2
  $timeout = 30
  $app_dir = "/app" 
  $listen  = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir
  $pid     = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
  $std_log = File.expand_path 'log/unicorn.log', $app_dir

  worker_processes  $worker #ワーカーの最大同時起動数
  working_directory $app_dir #自分のアプリケーションまでのpath
  stderr_path $std_log #ログ出力ファイル
  stdout_path $std_log #エラーログ出力ファイル
  timeout $timeout #ワーカーの処理開始から終了までの最大時間
  listen  $listen #Nginxとの接続用ポート番号
  pid $pid #プロセスIDの保存ファイル
 
  preload_app true #Unicornの再起動をダウンタイムなしで行うかどうか

#フォーク開始前の処理
  before_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
    old_pid = "#{server.config[:pid]}.oldbin"
    if old_pid != server.pid
      begin
        Process.kill "QUIT", File.read(old_pid).to_i
      rescue Errno::ENOENT, Errno::ESRCH
      end
    end
  end
 
#フォーク終了後の処理
  after_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
  end
```

| 設定 | 説明 |
|:-|:-:|
| worker_processes  |  ワーカーの最大同時起動数 |
|  timeout | ワーカーの処理開始から終了までの最大時間  |
|  working_directory | 起動コマンドが実行されるディレクトリ  |
| pid  |  プロセスIDの保存ファイル |
| listen  |  Nginxとの接続用ポート番号 |
|  stdout_path | ログ出力ファイル  |
| stderr_path  |  エラーログ出力ファイル |
| preload_app | Unicornの再起動をダウンタイムなしで行うかどうか  |
変数を定義して、各項目に値をあてているだけです。設定内容に関しては、上記の表を参照してください。

フォーク前後の処理内容はすこし複雑なので、ザックリ解説します。

フォークとは
IT分野では、現在のプロセスを複製して、新しいプロセスとして動かすことを意味する。

今回の場合は、マスタープロセスがワーカープロセスを複製することを指します。

#### フォーク前の処理(before_fork)
フォーク前には、unicornとActiveRecordとの接続を切ります。
```
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
```
また、マスタープロセスを再起動する処理も必要。

理由は、preload_appをtrueにしてダウンタイムなしでUnicornを再起動するようにしているので、再起動を行い古いプロセスを止めたいからです。
```
old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      Process.kill "QUIT", File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
  end
end
```
#### フォーク後の処理(after_fork)
フォーク後には、再度unicornとActiveRecordを接続します。
```
after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
```

### Unicornの起動/停止/再起動コマンド
| 内容 | コマンド |
|:-:|:-:|
| Unicorn起動  | $ bundle exec unicorn -c config/unicorn.rb [-E $RAILS_ENV] [-D]  |
|  Unicorn停止 |  $ kill -QUIT "cat /path/to/unicorn.pid" |
|  Unicorn再起動(通常) | $ kill -HUP "cat /path/to/unicorn.pid"  |
|  Unicorn再起動(Capistranoなどの使用時) |  $ kill -USR2 "cat /path/to/unicorn.pid" |

Capistranoを使用している場合の再起動には、-USR2オプションを指定する必要があるようです。

もしアプリのデプロイ時などに、再起動をおこなっても内容が反映されない場合は、停止→起動コマンドを実行してみて下さい。

## Nginxについて
先述したように、Nginxは高負荷につよいWebサーバーで、以下のような特徴があります。

- 複数のアクセスに1つの対応。
- 急激なアクセス数UPに強い。
- 処理速度を維持しやすい。

今回、RailsとNginxを併用する理由は高負荷につよいサーバーが欲しいからなので、まさにピッタリのWebサーバーだと思います。

その他のWebサーバー
メジャーなWebサーバーにApacheなども存在しますが、アクセスの急上昇によわくダウンしやすいといった特徴があるようです。

### Nginxの導入方法
今回はRailsのディレクトリ内にNginxディレクトリを作成し、その中にDockerfileとNginx.confファイルを作成しました。

```
(Dockerfile)
FROM nginx:stable

RUN rm -f /etc/nginx/conf.d/*

COPY nginx.conf /etc/nginx/conf.d/myapp.conf

CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```
```
(Nginx.conf)
upstream unicorn {
  server unix:/app/tmp/sockets/.unicorn.sock;
}

server {
  listen 80;
  server_name localhost;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;
  
  root /app/public;

  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  try_files  $uri/index.html $uri @unicorn;

  location @unicorn {
    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://unicorn;
  }
}
```
それぞれのコードの意味。
#### upstream unicornについて
Unicornソケットの設定を行います。

簡単にいえば、NginxとUnicornで通信するために必要な設定です。

※Unicorn側でもNginxと通信するために、listen = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dirでソケットを指定しています。

#### serverについて
上から順に、listenではポート番号を設定しますが、Webサーバーには80を指定するのが基本です。

今回はローカルで起動させたので、server_nameをlocalhostに設定し、access_logとeroor_logでログ出力設定を行なっています。

rootにpublicディレクトリを指定することで、コンパイル済みのJavaScriptファイルやCSS、画像などをRailsへ送信することができるようになります。

try_filesとは？
try_filesを使えば、指定のファイルorディレクトリが存在しなかった場合に、unicornへリダイレクトするように設定可能です。

今回の場合は、$uri/index.htmlに対する静的コンテンツ(コンパイル済のJavascpirt等)が存在すれば、そのファイルを返すように設定しています。

## docker-composeの設定
Rails×Unicorn×Nginxのコンテナを一括管理するために、docker-compose.ymlを作成します。

```
#(一部省略しています)
version: '3'
services:
  db:
    image: mysql:5.7
    ports:
      - 3306:3306
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ✳︎✳︎✳︎✳︎✳︎✳︎✳︎✳︎
  app:
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec rails s -p 3000 -b 0.0.0.0  --environment=production #Railsを本番環境で開始する
    tty: true
    stdin_open: true
    ports:
      - 3000:3000
    volumes:
      - .:/app:cached
      - tmp-data:/app/tmp/sockets
      - ./public:/app/public
    environment:
      APP_DATABASE_HOST: db
      APP_DATABASE_USERNAME: ✳︎✳︎✳︎✳︎
      APP_DATABASE_PASSWORD: ✳︎✳︎✳︎✳︎✳︎✳︎✳︎✳︎
    depends_on:
      - db
    stdin_open: true
    tty: true
  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
    restart: always
    volumes:
      - tmp-data:/app/tmp/sockets
      - ./public:/app/public
    depends_on:
      - app 
volumes:
  db_data:
  tmp-data:
```

あくまで僕がproduction環境でRails動かす場合のコードなので、細かいところは変更して下さい。

### Rails×Unicorn×Nginxを動作させるための最終準備
Rails×Unicorn×Nginxをproduction環境で動作させるために、以下の流れでコマンドを実行します。

1. $docker-compose build
2. $docker-compose up
3. $docker-compose run app bin/rails db:create RAILS_ENV=production
4. $ docker-compose run app bin/rails db:migrate RAILS_ENV=production
5. $docker compose run app bundle exec unicorn -p 3000 -c /app/config/unicorn.rb

①②はDockerイメージをビルドして、コンテナを起動させるためのコマンドです。

③④で、production環境用のDBの作成を行なっています。

⑤は、Unicornを起動させるコマンドですが、docker-compose内のcommandで実行していいと思います。※自分もあとで書きかえる予定です。

### 【注意】webpackerを使用している場合
最後に、自分がRails×Unicorn×Nginxを設定するのに10時間ほどハマってしまった落とし穴があるので紹介します。

ちなみにwebpackerを使用してJavaScriptを扱っていない人には関係のない話です。

✳︎✳︎✳︎.jsファイルの取得が404エラーになる
ここまでの流れで、無事にNginxとUnicornを繋ぐことが出来たのですが、webpackerで使っていたjsファイルが取得できる、画面が真っ白になるエラーが発生しました。

いくつかの要素が重なって起きたエラーだったので、原因をまとめました。

- 静的ファイルを用意していなかった。
- webpack-dev-serverを起動していた。

通常、development環境では、webpackerがjsなどのコードをコンパイルしてくれています。

しかし、production環境では、コンパイルの時間をかけたくないので、jsファイルなどは事前にコンパイルしてpublicディレクトリに入れておかないといけません。

※Railsではproduction環境でwebpackerによるコンパイルは行われない設定になっています。

#### jsファイルなどをコンパイルする方法
```
docker-compose run app rails assets:precompile
```

上記のコードを実行すれば、publicディレクトリの配下にコンパイルされたJavaScriptのコードなどが出力されます。

同時にmanifest.jsonというファイルも生成されますが、そのなかで定義されたファイルが、production環境で出力されます。

```
{
  "entrypoints": {
    "hello_vue": {
      "js": [
        "/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.js"
      ],
      "js.map": [
        "/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.js.map"
      ]
    }
  },
  "hello_vue.js": "/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.js",
  "hello_vue.js.map": "/packs/js/✳︎✳︎✳︎-✳︎✳︎✳︎.map"
}
```

#### webpack-dev-server
エラーを繰り返しながら、原因を探っているうちに気がついたのですが、production環境でwebpack-dev-serverは起動してはいけません。

なぜならば、上記のmanifest.jsonのファイル名を勝手に変更されてしまうからです。

webpack-dev-serverが存在しないファイルを指定
自分の場合、webpack-dev-serverがpublic/packs/js配下に存在しないファイルを指定していたので、ファイルのGETに対して404エラーが出ていました。

```
"GET /packs/js/hello_vue-16116754674fd726dd84.js HTTP/1.1" 404
```

そもそもproduction環境でwebpack-dev-serverは不要なので、起動しないのが無難ですね。