【Rails】Rails6系とMySQL8系の組み合わせの開発環境をDockerだけでつくる

# 概要

- :point_right: この記事は弊社ｧ内で行った準備作業の備忘録メモでございますわっ
- 次の新規案件に備え、RubyとRails最新版のスケルトン(?)なプロジェクトを準備する
- 各バージョンは以下の予定
    - Ruby2.7系
    - Rails6.0系
    - MySQL8.0系
    - Nginx1.17系
    - redis(いちおう)
- Docker前提です
    - 新規ならDockerでしょ？って、僕も過激派になっている感。
    - AWS ECSでの運用が前提のため。
- Rubyのイメージは**alpine3.11**を使用。**カーネル5.4系でセキュリティも向上してる**らしい(?)し、**軽い**し[^1]、

    ``` shell
    $ docker images
    REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
    ruby                      2.7-slim-buster     b913dc62d63c        10 days ago         149MB
    ruby                      2.7-buster          0c1ee6efe061        10 days ago         842MB
    ruby                      2.7-alpine3.10      78005ca97a7f        2 weeks ago         52.9MB
    ruby                      2.7-alpine3.11      1f7033feacdb        3 weeks ago         53.5MB
    ```
- docker-compose化する。最終的にはね。
    - そこだけ読みたいなら[ここ](#docker-compose化)までスクロール

# やりたいこと

- 作業PC的な観点で
    - ~~PCローカル内のモダンな過去プロジェクトを汚さずに環境構築したい。~~
    - ~~PCローカル内のレガシーな過去プロジェクトを汚さずに環境構築したい。~~
    - PCローカル内のヴィンテージな過去プロジェクトを汚さずに環境構築したい。
    - PC内のパッケージに悪影響が出ないようにしたい。
- `rails new` すらもDockerの中でやりたい。
- dockerコンテナ落としても**DBの内容や**bundleの**gemが消えないように**したい。
- **他の開発者(後輩くん)が使える程度のもの**を残しておきたい。
- 運用想定的な観点で(~~何様~~)
    - 環境作成
        - AP,DBコンテナの構成で十分
        - `rails new` してソースコード準備したい
        - できれば動作確認したい
    - 開発環境(弊社ｧ内エンジニア用)
        - WEB,AP,DBコンテナに加えてRedisも建てたい
            - ↑↑環境差異による不具合を減らすため、本番環境にできるだけ合わせたい・・・！！（切実）
        - docker-compose化しておいて、少しでも普段遣いのコマンドを減らしたい
    - 本番環境
        - **AWS ECS**で動かす想定
        - dockerイメージはできるだけ**開発環境そのままECRにプッシュする**
            - **環境変数だけで環境変えたい**よね
        - ECSタスク定義とかRDS設定は**docker-compose見ればほぼ設定できる**ようにしておきたい

# Railsプロジェクトの新規作成
## 目的

- docker内で`rails new`してWebアプリケーションのソースを用意する
- ここの見出しでやるのは`rails new`だけ。開発環境整える方法は[次の見出し](#docker-compose化)までスクロールしてね

## やることを決める

- dockerボリュームの有効活用
    - 環境構築途中で失敗しても**dbとgemを消さない**ようにして時短。
    - dockerのマウントタイプは `bind` でわなく `volume` を使う[^2]。Macのファイルシステムへのマウントは遅い(?)ので
- 動作確認のためDBコンテナも建てる

## 事前準備
### docker network（一時） を作成

``` shell
$ # host OS (local PC)
$ # コンテナ間通信用のネットワークを用意しておく
$ 
$ APP_NETWORK=tmp_network # 名前をきめてね
$ 
$ docker network create ${APP_NETWORK}
$ 
$ # 確認しよう
$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
c31a38da01e0        tmp_network         bridge              local
```

### docker volume（一時） を作成

``` shell
$ # host OS (local PC)
$ # DBデータ保持用と、gem保存用の２つのdockerボリュームを作っておく。
$ 
$ DB_VOLUME=tmp_dbdata_vol # 名前をきめてね
$ BUNDLE_VOLUME=tmp_bundle_vol # 名前をきめてね
$ 
$ docker volume create ${DB_VOLUME}
$ docker volume create ${BUNDLE_VOLUME}
$ 
$ # 確認しよう
$ docker volume ls
DRIVER              VOLUME NAME
local               tmp_bundle_vol
local               tmp_dbdata_vol
```

- これ以降のコンテナ作成作業中で、エラーが出まくったら、volumeを一度作り直すなどして問題解決していこうかと考えていた。

## コンテナの作成
### DBコンテナ（一時）を建てる

- `mysqld` コマンドのオプションで色々宣言している。 `**.cnf`を用意するのでも良かったけど、本番はRDS使うだろうし、

``` shell
$ # host OS (local PC)
$ 
$ # パスワードはあくまで一例。ちゃんと毎回違うランダム文字を使う癖をつけるように教えるの（）
$ docker run \
    --detach \
    --env MYSQL_ROOT_PASSWORD=4h%zpW8wrb+G \
    --env TZ="Asia/Tokyo" \
    --mount type=volume,source=${DB_VOLUME},target=/var/lib/mysql \
    --name tmp_db_mysql \
    --network ${APP_NETWORK} \
    --publish 127.0.0.1:13306:3306 \
    --tty \
    mysql:8.0 \
    mysqld \
      --block_encryption_mode aes-256-cbc \
      --character_set_server utf8mb4 \
      --collation-server utf8mb4_bin \
      --default-authentication-plugin mysql_native_password \
      --explicit_defaults_for_timestamp \
      --init-connect 'SET NAMES utf8mb4' \
      --skip-character-set-client-handshake
```

### APコンテナ（一時）を建てる

``` shell
$ # host OS (local PC)
$ 
$ # DBパスワードはDBコンテナと同じもの
$ docker run \
    --detach \
    --env APP_DATABASE_PASSWORD=4h%zpW8wrb+G \
    --mount type=volume,source=${BUNDLE_VOLUME},target=/usr/local/bundle \
    --name tmp_ap_rails \
    --network ${APP_NETWORK} \
    --publish 127.0.0.1:3000:3000 \
    --tty \
    --workdir /app \
    ruby:2.7-alpine3.11
```

## プロジェクトの新規作成
### Railsプロジェクトを新規作成

``` shell
$ # host OS (local PC)
$ 
$ docker exec -it tmp_ap_rails sh
/app # # guest OS (ruby:2.7-alpine3.11)
/app # 
/app # # 必要そうなパッケージをAlpineLinuxPackagesからインストール
/app # apk update
/app # apk upgrade
/app # apk add --update --no-cache \
          build-base \
          git \
          imagemagick \
          imagemagick-dev \
          libxml2-dev \
          libxslt-dev \
          mysql-client \
          mysql-dev \
          nodejs \
          ruby-dev \
          tzdata \
          yarn
/app # 
/app # # yarnのインストールコマンドを叩いておく
/app # yarn install
```

- ローカルPC内でGemfileを作って、それをAPコンテナ(一時)にコピーする
- ここでrailsのバージョンを決めている

``` Ruby:Gemfile
source 'https://rubygems.org'

git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem 'rails', '~> 6.0', '>= 6.0.2.1'
```
``` shell
$ # host OS (local PC)
$ 
$ # cpコマンドでdocker内にファイルをコピーする
$ docker cp Gemfile tmp_ap_rails:/app/Gemfile
```
``` shell
$ # host OS (local PC)
$ 
$ docker exec -it tmp_ap_rails sh
/app # # guest OS (ruby:2.7-alpine3.11)
/app #
/app # # railsをインストールする
/app # bundle install
/app #
/app # rails --version
Rails 6.0.2.1
/app #
/app # # Railsプロジェクトを新規作成する（各オプションはプロジェクトに応じて変える）
/app # rails new . \
      --database=mysql \
      --force \
      --skip-coffee \
      --skip-sprockets \
      --skip-turbolinks \
      --skip-test \
      --webpack=vue
```

### 設定値の書き換え

``` shell
/app # # guest OS (ruby:2.7-alpine3.11)
/app #
/app # # development と test の設定を変更
/app # vi config/database.yml
```
``` yaml:config/database.yml
# --- L20..L24
development:
  <<: *default
  database: app_development
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: tmp_db_mysql
# --- L29..L33
test:
  <<: *default
  database: app_test
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: tmp_db_mysql
```

### 動作確認

``` shell
/app # # guest OS (ruby:2.7-alpine3.11)
/app #
/app # rails db:create
/app #
/app # rails server -b 0.0.0.0
```

- ローカルPCで http://localhost:3000 にアクセス

<img width="862" alt="スクリーンショット 2020-02-21 18.37.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166229/3f086d41-c291-2882-04ea-c120afde37a4.png">

- うまく、いった、みたい、

### ソースコードのコピー

- ディレクトリ内をコピーするときは `.` をつけるらしい[^3]。
- この作業で新規プロジェクトのソースコードがローカルPCにコピーされるので、これをgithubで管理するなりしておく

``` shell
$ # host OS (local PC)
$ 
$ # Docker内のソースコードをホスト側にコピーする
$ docker cp tmp_ap_rails:/app/. ./
```

## ゴミ掃除
### APコンテナ（一時）の削除

- APコンテナ（一時）は `rails new` と動作確認のためだけのコンテナだったので、消す。

``` shell
$ # host OS (local PC)
$ 
$ docker stop tmp_ap_rails
$ docker rm tmp_ap_rails
$ 
$ # 消えていることを確認
$ docker ps -a
```

### DBコンテナ（一時）の削除

- DBコンテナ（一時）も、消す。

``` shell
$ # host OS (local PC)
$ 
$ docker stop tmp_db_mysql
$ docker rm tmp_db_mysql
$ 
$ # 消えていることを確認
$ docker ps -a
```

### docker network（一時）の削除

- docker network（一時）も、消す。

``` shell
$ # host OS (local PC)
$ 
$ docker network rm ${APP_NETWORK}
$ 
$ # 消えていることを確認
$ docker network ls
```

### docker volume（一時）の削除

- docker volume（一時）も、消す。

``` shell
$ # host OS (local PC)
$ 
$ docker volume rm ${DB_VOLUME}
$ docker volume rm ${BUNDLE_VOLUME}
$ 
$ # 消えていることを確認
$ docker volume ls
```

## Railsプロジェクトの新規作成まとめ

- 長文になってしまって申し訳ナソス
- ローカルPCのrubyバージョンに影響されずにrailsプロジェクトが新規作成できた。
    - (rbenvとかを使用してもこの程度はできる気がするけど・・・)
- これを応用してエンジニア用の開発環境も整えていく形で良さそう。（何様）
- 上記に記してはいないが、volume設定でgemを別に保持できていたのを確認できているので、感触はOK
    - 直接volume内を見に行くのは少々面倒だが、bindよりは動作速そう(?)。

# docker-compose化
## 目的

- ここから先が本当にやりたかったことやで。。。
- 上記までの準備(新規作成したrailsプロジェクト)をもとに、開発環境(弊社ｧ内エンジニア用)を整えてゆく

## やることを決める

- `docker-compose.yml`を用意する
- 基本 `docker-compose up -d`, `docker-compose down` コマンドだけで動くようにしておく
- **ログローテート**はちゃんと設定する（もし無限ログ吐き出されてもPCをパンクさせない教訓）
- **Dockerfile**の用意をしてビルド時には必要パッケージがインストールされている状態にする
    - Dockerfileは**マルチステージビルド**[^4]を仕込んでおき、**本番リリースに使える**ようにしておく
- WEBコンテナ(**nginx**)を追加して、**静的ページ**を表示できるようにする
    - 静的ファイル、ソケットはAPコンテナとつなげておく
        - ソケットはrails6だからpuma(**unixソケット**にする予定)
    - 起動順の依存関係には一番最後
- CACHEコンテナ(**redis**)を追加しておく
    - 起動順の依存関係はAPより前
    - 今回は使わないけど。開発で使うので

## Dockerfileの作成

``` docker:containers/web/Dockerfile
FROM nginx:1.17-alpine

LABEL maintainer="なまえ <メアド>"

# 設定ファイルを上書き
RUN rm -f /etc/nginx/conf.d/*
ADD ./containers/web/default.conf /etc/nginx/conf.d/default.conf

# nginxの起動
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```
``` nginx:containers/web/default.conf
# @note ホストサーバーとのファイル共有は以下
#   - tmp/     # pumaソケット通信用
#   - public/  # rails静的ファイル共有(404ページとかjs,cssとか)

# nginxとpumaのソケット通信の設定
# @see{https://github.com/puma/puma/blob/master/docs/nginx.md}
upstream rails_app {
  server unix:///rails_app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name localhost;

  # HTTPの持続的な接続維持時間（軽量なので0秒(off)でもいいかも）
  keepalive_timeout 5;

  # アップロードサイズの上限を設定
  client_max_body_size 10m;

  # 静的ファイルのパス
  root /rails_app/public;

  # ログ出力(dockerイメージの設定より、このパスは標準出力となる)
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log info;

  # メンテナンスファイルが置かれている場合はメンテ画面を出す
  if (-f $document_root/maintenance.html) {
    rewrite  ^(.*)$  /maintenance.html last;
    break;
  }

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # 静的ファイル
    if (-f $request_filename) {
      break;
    }

    if (-f $request_filename/index.html) {
      rewrite (.*) $1/index.html break;
    }

    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://rails_app;
      break;
    }
  }

  # 後方一致で画像などの静的ファイルが指定された場合はpublic/配下のファイルを直接返す(Railsを介さない)
  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;

    # ファイルパスが存在しない場合はRailsで処理
    if (!-f $request_filename) {
      proxy_pass http://rails_app;
      break;
    }

    expires max;
    break;
  }
}
```

- ↓↓**マルチステージビルド**の使い方が完全に自己流なんだけどこれで良いのかしら・・・。

``` docker:containers/app/Dockerfile
FROM ruby:2.7-alpine3.11 as dev_mode

LABEL maintainer="なまえ <メアド>"

# ビルド時の作業ディレクトリ
WORKDIR /app

# AlpineLinuxPackagesで必要そうなコマンドをインストール
RUN apk update && \
    apk upgrade && \
    apk add --update --no-cache \
        build-base \
        git \
        imagemagick \
        imagemagick-dev \
        libxml2-dev \
        libxslt-dev \
        mysql-client \
        mysql-dev \
        nodejs \
        ruby-dev \
        tzdata \
        yarn

# yarnインストールの実行(先にlockファイルをコピーしてバージョンを固定)
COPY package.json yarn.lock ./
RUN yarn install

# マルチステージビルドで本番用イメージを作成
FROM dev_mode as prod_mode

# Railsの秘密情報のマスターキー(docker build時にオプションで宣言する)
ARG RAILS_MASTER_KEY
ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY}
# 本番モードで起動する
ENV RAILS_ENV production

# アプリのソースをイメージ内にコピー
COPY ./ ./

RUN bundle install

# フロント用のライブラリのインストールと静的ファイルの作成
RUN rails yarn:install
RUN rails webpacker:compile

# コンパイル後、マスターキー情報はイメージから消しておく
ENV RAILS_MASTER_KEY=
```

## docker-compose.ymlの作成

- 2020/02くらいに確認したときは、バージョン3.7が最新だった[^5]ので、それで。
- パスワードはあくまで一例。

``` yaml:docker-compose.yml
version: '3.7'

# @see {https://docs.docker.com/compose/compose-file/}
services:
  # DBコンテナの設定
  db:
    # ビルドに使うイメージ
    image: mysql:8.0
    # 環境変数
    environment:
      MYSQL_DATABASE: app_development
      MYSQL_ROOT_PASSWORD: F-O75%kSG6gz
      TZ: "Asia/Tokyo"
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # コンテナ間通信用
    networks:
      - app_net
    # ホストOSに開放するポートの指定
    ports:
      - 127.0.0.1:3306:3306
    # ファイルのマウント設定
    volumes:
      # データベースの内容をDocker領域に同期する(データ永続化のため)
      - type: volume
        source: mysql_data_vol
        target: /var/lib/mysql
    # コンテナを永続化
    tty: true
    # 認証を旧式(パスワード)に変更、デフォルトの文字コードとcollate設定を指定、暗号化のモード選択
    command: >
      mysqld
      --block_encryption_mode aes-256-cbc
      --character_set_server utf8mb4
      --collation-server utf8mb4_bin
      --default-authentication-plugin mysql_native_password
      --explicit_defaults_for_timestamp
      --init-connect 'SET NAMES utf8mb4'
      --skip-character-set-client-handshake

  # Redisコンテナの設定
  redis:
    # ビルドに使うイメージ
    image: redis
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # コンテナ間通信用
    networks:
      - app_net
    # コンテナを永続化
    tty: true

  # APコンテナの設定
  app:
    # ビルド時の設定
    build:
      # プロジェクトのディレクトリでビルドする
      context: .
      # マルチステージビルドのターゲットを指定
      target: dev_mode
      # Dockerfileの場所を指定
      dockerfile: containers/app/Dockerfile
    # コンテナ依存関係
    depends_on:
      - db
      - redis
    # 環境変数
    environment:
      APP_DATABASE_PASSWORD: F-O75%kSG6gz
      # RAILS_MASTER_KEY:
      # RAILS_MAX_THREADS:
      TZ: "Asia/Tokyo"
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # コンテナ間通信用
    networks:
      - app_net
    # ファイルのマウント設定
    volumes:
      # アプリのソースをホストOSと共有する
      - type: bind
        source: .
        target: /app
      # gemをDocker領域に同期する
      - type: volume
        source: bundle_vol
        target: /usr/local/bundle
    # コンテナを永続化
    tty: true
    # bundle install 、yarn install を行い、pumaサーバーを起動する
    command: >
      sh -c "
      bundle install &&
      rails yarn:install &&
      rails webpacker:compile &&
      pumactl start"

  # WEBコンテナの設定
  web:
    # ビルド時の設定
    build:
      # プロジェクトのディレクトリでビルドする
      context: .
      # Dockerfileの場所を指定
      dockerfile: containers/web/Dockerfile
    # コンテナ依存関係
    depends_on:
      - app
    # ログローテート設定
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    # ホストOSに開放するポートの指定
    ports:
      - 127.0.0.1:80:80
    # ファイルのマウント設定
    volumes:
      # 静的ファイルの共有
      - type: bind
        source: ./public
        target: /rails_app/public
      # pumaソケットファイルの共有
      - type: bind
        source: ./tmp
        target: /rails_app/tmp

# ネットワークの定義
networks:
  # コンテナ間通信用
  app_net:

# ボリュームの定義
volumes:
  mysql_data_vol:
  bundle_vol:
```

- rails側のDB設定も修正修正（ホストをdocker-composeで定義した名前に変えよう）

``` yaml:config/database.yml
# --- L20..L24
development:
  <<: *default
  database: app_development
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: db
# --- L29..L33
test:
  <<: *default
  database: app_test
  password: <%= ENV['APP_DATABASE_PASSWORD'] %>
  host: db
```

- あと、WEBコンテナとソケット通信させるためにconfigも少し修正が必要だった[^6]

``` ruby:config/puma.rb
# --- L13
# unixソケット通信はポートを使用しない
# port        ENV.fetch("PORT") { 3000 }

# --- 最後に追加
# ソケット通信の設定(WORK_DIRを/appにしているからもう直書きで良いかしら)
bind "unix:///app/tmp/sockets/puma.sock"
```

## 動作確認

``` shell
$ # host OS (local PC)
$ 
$ # いよいよdocker-compose起動するぜ！！
$ docker-compose up -d
$ 
$ # 確認
$ docker-compose ps
        Name                       Command               State                  Ports
----------------------------------------------------------------------------------------------------
skl_rails6021_app_1     sh -c  bundle install && r ...   Up
skl_rails6021_db_1      docker-entrypoint.sh mysql ...   Up      127.0.0.1:3306->3306/tcp, 33060/tcp
skl_rails6021_redis_1   docker-entrypoint.sh redis ...   Up      6379/tcp
skl_rails6021_web_1     /bin/sh -c /usr/sbin/nginx ...   Up      127.0.0.1:80->80/tcp
$ 
$ # 確認
$ docker-compose logs --tail 30 app
$ docker-compose logs --tail 30 web
$ 
$ # MySQLもローカルPCから接続できるよね
$ mysql -u root -p
```

- ローカルPCで http://localhost:80 にアクセス（初回は時間がかかるのでログを見て落ち着いたら）

<img width="1012" alt="スクリーンショット 2020-02-28 20.23.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166229/131ef78a-fa89-e02f-fd11-d16b90f35fc3.png">

- うまく、できた、みたい、

## READMEに説明を書く

- やる。
- 僕だけ使えても意味がない。
- もしかしたらこの作業が一番骨折れるかもしれない。

# 最後に

- 長文失礼。
- ポート開放不親切失礼。必要に応じて書き換え願う
- ruby2.7対応していないgemがいくつかあるみたいで、ログが汚い。`warning`の大量発生や。。。（2020/02ころ現在）
- WEBコンテナ追加してnginxとソケット通信する作業でに労力増えてる感ある。`rails sever`のありがたみを感じる。。。
- redisの設定は開発時にやるで良いよね。。。

[^1]: https://hub.docker.com/_/ruby ←←2020/02くらいにここを閲覧した、そして全部取得して比べてみた。
[^2]: https://docs.docker.com/storage/volumes/ ←Docker領域内に名前つけて保存されるから、万が一ソースコードごと紛失してもワンチャンある()
[^3]: https://medium.com/veltra-engineering/how-to-copy-a-directory-using-docker-cp-command-f2c73f9ccf75 ←←こちらのサイトがとても参考になりました :pray::pray::pray:
[^4]: https://docs.docker.com/develop/develop-images/multistage-build/#use-a-previous-stage-as-a-new-stage
[^5]: https://docs.docker.com/compose/compose-file/compose-versioning/
[^6]: https://github.com/puma/puma#binding-tcp--sockets unixソケットにするためにbindのオプションを`config/puma.rb`に書く必要があった。[ここ](https://github.com/puma/puma/blob/master/lib/puma/binder.rb#L121)とか[これ](https://github.com/puma/puma/blob/master/lib/puma/configuration.rb#L318)とか[このへん](https://github.com/puma/puma/blob/master/lib/puma/dsl.rb#L155)読んでも書き方迷いそう。。。