Rails6(Unicorn)+MariaDB+Nginx on Docker(M1 preview版)の環境構築

#0. はじめに
ここでは掲題の環境を構築するためのファイル構成と作業手順について、要点絞って記録しています。詳細を理解したい方は、参考文献を以下に記載しますのでそちらをご参照ください。

【参考文献】
(1) そもそものDocker・コンテナについて
[【連載】世界一わかりみが深いコンテナ & Docker入門 ](https://tech-lab.sios.jp/archives/18811)

(2) 本記事に示したDockerfileやdocker-compose.ymlの中身について
[Docker + Rails + Puma + Nginx + MySQL](https://qiita.com/eighty8/items/0288ab9c127ddb683315)
[【Rails】Rails 6.0 x Docker x MySQLで環境構築](https://qiita.com/nsy_13/items/9fbc929f173984c30b5d)
[DockerでNginx+unicorn+rails+Mysqlの開発環境を作ってみた](https://qiita.com/E6YOteYPzmFGfOD/items/509dbabeb20bf2487283)

(3) nginxとunicornの設定ファイルの中身について
[(デプロイ編①)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで](https://qiita.com/naoki_mochizuki/items/814e0979217b1a25aa3e)


#1. ファイル構成
testフォルダの中で作業をしていくことにして、最初に準備するファイルの構成は以下の通り。

```
test ─── docker-compose.yml
      ├─ Dockerfile
      ├─ Gemfile
      ├─ Gemfile.lock
      └─ Nginx ─── Dockerfile
                └─  nginx.conf
```
続いて、一つずつファイルの中身を見ていきます。

###1. docker-compose.yml
db, unicorn, nginxの順にコンテナを作成・起動し、連携するよう記述。（※起動順序を制御するdepends_onは起動の完了までは保証しないので、事前に起動が完了して欲しいサービスが立ち上がっているか確認するには別のスクリプトを書く必要あり。）

```docker-compose.yml
version: '3.5'
services:
  db:
    image: mariadb:10.3
    ports: 
      - '3306:3306'
    environment:
      MYSQL_DATABASE: docker_development
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: root
      MYSQL_PASSWORD: password
    volumes: #dbコンテナの/var/lib/mysqlディレクトリをmysql-dataボリュームにマウント
      - mysql-data:/var/lib/mysql

  unicorn:
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec unicorn -c /myproject/config/unicorn.conf.rb 
    tty: true #pry-byebugを使えるようにする
    stdin_open: true
    depends_on:
      - db 
    ports:
      - "3000:3000"
    volumes:
      - .:/myproject:cached 
      #unicornコンテナの/myproject/tmp/socketsディレクトリをtmp-dataボリュームにマウント
      #ソケット通信用ファイルをnginxコンテナと共有するため
      - tmp-data:/myproject/tmp/sockets
      #unicornコンテナの/myproject/publicディレクトリをpublic-dataボリュームにマウント
      #画像データなどをnginxと共有するため
      - public-data:/myproject/public

  nginx:
    build:
      context: ./nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
    restart: always #restartポリシーをalwaysにすると、ホストOSやDockerデーモン起動時にコンテナが自動的に起動する。
    volumes:
      #nginxコンテナの/myproject/tmp/socketsディレクトリをtmp-dataボリュームにマウント
      #ソケット通信用ファイルをunicornコンテナと共有するため
      - tmp-data:/myproject/tmp/sockets
      #nginxコンテナの/myproject/publicディレクトリをpublic-dataボリュームにマウント
      #画像データなどをunicornと共有するため
      - public-data:/myproject/public
    depends_on:
      - unicorn

# ホストOS上にpublic-data, tmp-data, mysql-dataという名前のデータ保存領域を作成
volumes:
  public-data:
  tmp-data:
  mysql-data:
    driver: local
```

### 2. Dockerfile
Rails6を動作させるコンテナイメージのレシピ。Rails6.0からはWebpackerを使ったJSの管理がデフォルトとなっており、Webpackerを動作させるにはNode.jsとyarnが必要。なので、rubyイメージを取ってきた後でNode.jsとyarnをインストールする。その後、Host側にあるGemfileとGemfile.lockファイルをコンテナ内にコピーし、bundler gemをインストールし、`bundle install`コマンドで必要なgemを全てインストール。

```:Dockerfile
#既存のプロジェクトのrubyのバージョンを指定
FROM ruby:2.7

#パッケージの取得
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
    && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
    && apt-get update \
    && apt-get install -y --no-install-recommends\
    nodejs  \
    yarn \
    mariadb-client  \
    build-essential

WORKDIR /myproject

COPY Gemfile /myproject/Gemfile
COPY Gemfile.lock /myproject/Gemfile.lock
RUN gem install bundler
RUN bundle install

COPY . /myproject
```

###3. Gemfile
Ralsコンテナにインストールするgemの一覧ファイル。`rails new`で必要なgemが自動追記されるので、最初はrubyとrailsのgemのみ記載。`bundle install`でエラー(参照：[RailsのGPL混入問題についてまとめ（mimemagic）](https://qiita.com/Esfahan/items/45c0cdeb29509b7ec1c5))が出た(2021年3月29日時点)ので、mimemagicに依存しないrails 6.0.3.6を指定してインストール。

```:Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.7.2'
gem 'rails', "6.0.3.6"
```

###4. Gemfile.lock
自動追記されるので何も記述しない。

```Gemfile.lock
#空ファイル
```

###5. nginx/Dockerfile
nginxイメージの作成レシピ。ホスト側で用意したnginx設定ファイルをコンテナ内にコピーし、コンテナ起動と同時にnginxサーバが起動するように記述。

```:nginx/Dockerfile
FROM nginx:stable
#デフォルトのnginxファイルを削除して作成したものコンテナ内にコピー
RUN rm -f /etc/nginx/conf.d/*

COPY nginx.conf /etc/nginx/conf.d/myproject.conf

#-c以降の設定ファイルを指定して起動 daemon offでフォアグラウンドで起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

###6. nginx/nginx.conf
/etc/nginx/conf.d/myproject.confにコピーされるnginxの設定ファイル。

```nginx/nginx.conf
upstream unicorn {
  #ユニコーンソケットの設定
  server unix:/myproject/tmp/sockets/.unicorn.sock fail_timeout=0;
}

server {
  #IPとポートの指定
  listen 80 default;
  #サーバーネームの指定
  server_name localhost;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;
  #ドキュメントルートの指定
  root /myproject/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 @unicorn;
  keepalive_timeout 5;

  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;
  }
}
```

#2. 作業手順
(1) railsアプリを作成。

```bash:terminal
$ docker-compose run unicorn rails new . --force --database=mysql --webpacker
```

(2) Gemfileにunicorn gemを追加して、`docker-compose build`を実行。ここで、必要なgemが全てインストールされる。

```:Gemfile
gem 'unicorn'
```

```bash:terminal
$ docker-compose build
```

(3) unicorn.conf.rbを下記のように記述。

```unicorn.conf.rb
$worker  = 2
  $timeout = 30
  $app_dir = "/myproject" #自分のアプリケーションまでのpath
  $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
  # set config
  worker_processes  $worker
  working_directory $app_dir
  stderr_path $std_log
  stdout_path $std_log
  timeout $timeout
  listen  $listen
  pid $pid
  # loading booster
  preload_app true
  # before starting processes
  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 finishing processes
  after_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
  end
```

(4) docker-compose.ymlの設定と整合するようにdatabase.ymlを修正し、`docker-compose run unicorn bundle exec rails db:create`コマンドでDataBaseを作成。その後、`docker-compose build`コマンドにてdbコンテナに反映させておく。

```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

development:
  <<: *default
  database: docker_development
```

```bash:terminal
$ docker-compose run unicorn bundle exec rails db:create
$ docker-compose run unicorn bundle exec rails db:migrate
```

(5) `docker-compose up -d`コマンドでコンテナを起動。localhost:80にアクセスできることを確認。

```bash:terminal
$ docker-compose up -d
```

<img width="100%" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/803572/39c931a0-6664-5254-df81-879ff2f6b648.jpeg>