Rails6の環境構築を自動でしてくれるシェルスクリプトを作ってみた (1) ~ローカル起動編~

#はじめに

Rails6 を Docker のコンテナ上で動作させて
自動で開発環境を構築してくれるシェルスクリプトを作ってみました。

フォルダやDockerファイルもスクリプト内で生成するので、
Dockerとdocker-composeさえ導入されていればどんな環境でもコマンド一つで構築できると思います
(多分。。。)

私自身の環境は以下の通りです。

| OS                         | 環境  
| ---------------------------|------
| Windows 10 Home 64bit      | Ubuntu 18.04(WSL2)

# 構築する環境

以下構成をdocker上で動作させて、ローカルで初期画面が見れるところをゴールとします。

| 項目　　　   | 使用したもの  | バージョン  
|-------------| ------------ |----------
|言語         | ruby         | 2.6.6    
|フレームワーク| rails        | 6.0.2   
|データベース　| MySQL        | 8.0      
|Webサーバ    | Nginx        | 1.17.10  
|APサーバ     | puma         | 4.3.5

コンテナ構成は以下の通りです。

| appコンテナ                 
| ---------------------------
| ruby
| rails
| puma

| dbコンテナ                 
| ---------------------------
| MySQL

| webコンテナ                 
| ---------------------------
| Nginx

# できること

まず実際にできることを提示してから、その後中身について記載していきたいと思います。

### シェルの内容

今回作成したシェルスクリプトが以下の通りです。
結構長いのでGitHubにあげた方がいいかもと思ったのですが、ここにべた書きしてしまいます(^^;

```docker_rails_template.sh
#!/bin/bash

####################################################
# 以下環境をdockerで構築するためのシェルスクリプト     #
# プログラミング言語：Ruby / Ruby on rails           #
# データベース      ：mysql                          #
# webサーバ         ：Nginx                         #
####################################################

RUBY_VERSION="2.6.6"
RAILS_VERSION="6.0.2"
MYSQL_VERSION="8.0"
NGINX_VERSION="1.17.10"
APP_ADDLESS="app"

if [ $# != 1 ]; then
  echo "引数の指定が間違っています。"
  echo "ex: $0 sample"
  exit 1
fi

APP_NAME=$1
APP_DIR=`pwd`/${APP_NAME}

# 作業フォルダを作成
echo "----- Make working directories -----"
echo "mkdir -p ${APP_DIR}"
mkdir -p ${APP_DIR}
cd ${APP_DIR}
echo ""

# Dockerfile 配置用のフォルダを作成
echo "----- Make docker directories -----"
echo "mkdir -p docker/app"
mkdir -p docker/app
echo "mkdir -p docker/web"
mkdir -p docker/web
echo ""

# ソースコード配置用のフォルダを作成
echo "----- Make source directories -----"
echo "mkdir -p src/tmp/sockets"
mkdir -p src/tmp/sockets
echo ""

# 各種ファイル作成
echo "----- Make source directories -----"
# app用のDockerfile 作成
echo "make app Dockerfile"
cat << EOF > docker/app/Dockerfile
FROM ruby:${RUBY_VERSION}
RUN apt-get update -y && apt-get install -y default-mysql-client nodejs npm && \
    npm install -g -y yarn
RUN mkdir /myapp
WORKDIR /myapp
COPY ./src/Gemfile Gemfile
COPY ./src/Gemfile.lock Gemfile.lock
RUN bundle install
COPY ./src /myapp
EOF

# web用のDockerfile 作成
echo "make web Dockerfile"
cat << EOF > docker/web/Dockerfile
FROM nginx:${NGINX_VERSION}
RUN rm -f /etc/nginx/conf.d/*
COPY ./docker/web/${APP_NAME}.conf /etc/nginx/conf.d/${APP_NAME}.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
EOF

# Gemfile 作成
echo "make Gemfile"
cat << EOF > src/Gemfile
source 'https://rubygems.org'
gem 'rails', '${RAILS_VERSION}'
EOF

# Gemfile.lock 作成
echo "make Gemfile"
touch ${APP_DIR}/src/Gemfile.lock

# nginx.conf 作成
echo "make ${APP_NAME}.conf"
cat << EOF > docker/web/${APP_NAME}.conf
upstream ${APP_NAME} {
  server unix:///myapp/tmp/sockets/puma.sock;
}

server {
  listen 80;
  
  server_name ${APP_ADDLESS};

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

  root /myapp/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 @${APP_NAME};
  keepalive_timeout 5;

  location @${APP_NAME} {
    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://${APP_NAME};
  }
}
EOF

# docker-compose.yml 作成
echo "make docker-compose.yml"
cat << EOF > docker-compose.yml
version: '3'

volumes:
  db_data:

services:
  db:
    image: mysql:${MYSQL_VERSION}
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - 3306:3306

  app:
    build:
      context: .
      dockerfile: ./docker/app/Dockerfile
    command: bundle exec puma -C config/puma.rb
    volumes:
      - ./src:/myapp
    depends_on:
      - db

  web:
    build:
      context: .
      dockerfile: ./docker/web/Dockerfile
    volumes:
      - ./src/public:/myapp/public
      - ./src/tmp:/myapp/tmp
    ports:
      - 80:80
    depends_on:
      - app

EOF

echo ""

# rails new 実行
echo "----- Set application -----"
echo "docker-compose run app rails new . --force --database=mysql"
docker-compose run app rails new . --force --database=mysql

# database.yml を編集
echo "edit database.yml"
cat src/config/database.yml | sed 's/password:$/password: password/' | sed 's/host: localhost/host: db/' > __tmpfile__
cat __tmpfile__ > src/config/database.yml
rm __tmpfile__

# puba.rb を編集
cat << EOF > src/config/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
EOF

echo ""

echo "----- container start -----"
# イメージをビルド
echo "docker-compose build"
docker-compose build

# 初回マイグレーション
echo "docker-compose run app rails db:create"
docker-compose run app rails db:create

# 一度コンテナをリセット
echo "docker-compose down"
docker-compose down

# コンテナ起動
 echo "docker-compose up"
 docker-compose up
```

### 実行結果

こちらのスクリプトを適当な場所において、以下のコマンドを実行します。
引数で渡している `sample` は作成したいアプリの名前です。

```bash
sudo ./docker_rails_template_ver1.0.sh sample
```

完了まで 10分ほどかかります。
以下状態になったら実行完了です。

```
app_1  | * Listening on tcp://0.0.0.0:3000
app_1  | * Listening on unix:///myapp/tmp/sockets/puma.sock
app_1  | Use Ctrl-C to stop
```

ブラウザでlocalhostにアクセスしてみます。
railsのみだったら`ポート3000`を指定してアクセスすることが多いと思いますが、
今回はWebサーバ(Nginx)経由であり、`ポート80`で受け入れをしているので、ポート指定せずにアクセスできます。

<img width="482" alt="2020-05-25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626975/50824bde-634b-14ec-362f-4d5ef2cbae14.png">

無事に表示出来たら完了です。

ただ、バージョンなどを少しでもいじるとどこかでつまずく可能性があるので注意してください。
その場合は大人しくデバックに勤しむしかないです。。。(^^;

# シェルの内容について

シェルスクリプト内にコメントを記載していますが、内部で行っていることについて補足していきたいと思います。
補足できていない部分や、詳細な処理内容について気になる方は、記事最下部に挙げている参考記事をご参照ください。

### フォルダ階層

ホスト側のフォルダ階層は以下の通りです。
ルートディレクトリに配置している`docker-compose.yml`から`dockerフォルダ`配下のDockerfileを参照しにいき、コンテナ生成などを行っています。
また、railsのソースコードは`src`に格納しています。
この`src`フォルダがappコンテナとの共有ボリュームとして機能します。

``` ホスト側
sample
├── docker
│   ├── app
│   │   └── Dockerfile
│   └── web
│       └── Dockerfile
├── docker-compose.yml
└── src
```

### src フォルダ生成について

```
# ソースコード配置用のフォルダを作成
echo "----- Make source directories -----"
echo "mkdir -p src/tmp/sockets"
mkdir -p src/tmp/sockets
echo ""
```
srcフォルダ作成で、`src/tmp/sockets`までフォルダを作成している理由について補足します。
アプリケーションサーバは`puma`を利用していますが、puma起動時に`src/tmp/sockets/puma.sock`というファイルが自動生成されます。
しかし、自動生成時にフォルダ階層までは生成してくれないので、事前に作成しています。

### app用Dockerファイルについて
`ruby + rails6 + puma` が格納されるコンテナ用のDockerファイルです。
`rails6` は `webpacker` のインストールが必須となり、それに伴い`yarn`が必要となるので注意です。

### nginx.conf 作成について
Nginx用の設定ファイルになります。
upstrem内で`///myapp/tmp/sockets/puma.sock`を参照しておりますが、こちらがappコンテナ内のpumaと連携する部分になります。
webコンテナにて`ポート80`でリクエストを受け入れ、appコンテナへつないでいます。

### docker-compose.yml作成について
DBコンテナ、appコンテナ、webコンテナをdocker-composeで操作するためのファイルです。
#####dbコンテナ
`/var/lib/mysql`でdockerのlocalボリュームである`db_data`を永続ボリュームとしてマウントしています。
パスワードは面倒なので固定で「password」としています。
#####appコンテナ
`/myapp`でホスト側の`src`をマウントしています。
起動時には毎回以下コマンドでwebコンテナからの受け入れを開始します。

```
bundle exec puma -C config/puma.rb
```

また、dbコンテナと`depends_on`にて関連付けを行っています。

#####webコンテナ
`src`内のフォルダをマウントして、各デフォルト表示用のhtmlやソケット通信用のpuma.sockをappコンテナと共有します。
ポートは80を受け入れています。

### その後の処理について

その後は`rails new`を実行し、生成されたファイルを編集したのち、イメージビルド⇒コンテナ実行をしています。(雑)

アプリケーションのソースを`src`にてホスト側で編集できるため、都度イメージビルドをする必要がなく、開発環境に使えるのではないでしょうか。

以上になりますが、何かの参考になれば幸いです。

# 参考記事

以下記事を参考にさせて頂きました。ありがとうございましたm(_ _)m

[丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)](https://qiita.com/azul915/items/5b7063cbc80192343fc0)

[Rails+PostgreSQL+Docker+AWSで作成したポートフォリオの概要](https://ryo10leo.hatenablog.com/entry/2020/02/02/204150)

[docker-compose コマンドまとめ](https://qiita.com/wasanx25/items/d47caf37b79e855af95f)