"Docker-compose" で "Rails6" の安定的な開発環境構築マニュアル

## 本記事の目的
Docker・Rails初心者である私が`Docker-compose`を使用し`Rails6`の環境構築を行った際
御見識ある方に情報を提供頂き、無事に環境構築する事ができました。
しかしながら、構築内容で実行している内容が初心者の私には非常に理解しづらく、
`Rails5`での環境構築時と違い処理事項を間違えると環境構築できませんでした。
環境構築にあたり、ポイントが複数あると思います。

本記事では自身が相談し、回答頂いた事項を元に

- 環境構築時のコードを分解解説し自身の学習と知識定着を行う
- 自身の忘備録と今後の環境構築時のマニュアルとして使う
- 他の方に少しでも参考資料として役立ててもらえればとの意図

上記を目的にまとめました。

初心者ゆえに間違いがあるかもしれませんが、お気づきの点がありましたら、
ご指摘頂けますと幸いです。
少しでも参考になりましたら **<font color="LimeGreen">LGTM</font>** 頂けると感激です。

## この記事で作成する環境

Docker-compose

- Web
  - Ruby version:2.7〜
  - Rails version:6.0.3.4〜
- db
  - PostgreSQL version: 12.4〜

## とりあえず使いたい方
早々に使ってみて構築したい方は下記GitHubリポジトリに公開しております。
READMEに手順記載しておりますが、下記のコマンドを順に実行して下さい。

1. 下記サイトのREADMEより`git clone <Project name>` の実行
2. `cd　<Project name>` でプロジェクトディレクトリ移動
3. `./qs setup` コマンド実行

＊下記サイトのREADMEを参照して実行をお願いします。
[Rails6-QS](https://github.com/Tomoaki-Takeuchi-bot/Rails6-QS)

コマンド完了まで２０分ぐらいかかります。
辛抱強くお待ち願います。（笑）
終わるとブラウザでlocalhost:3000を叩けばお馴染みのRails初期画面が現れます。
DockerのDashboardにもプロジェクト名の中にweb,dbのコンテナがあるはずです。

## この記事で作成するもの
- Dockerfile
- Docker-compose.yml
- シェルスクリプト(sample.bash)<br>
  ＊公開リポジトリはqs(クイックスタートの略）と名前をしております。
- .gitignore
- Gemfile
- database.yml

## ①Dockerfileの作成
まずはDockerfileから作成を行なっていきます。
Dockerfileの作成にあたっては

- 公式のサイト<br>
[Docker Quickstart](https://docs.docker.com/compose/rails/)
- Docker Docs<br>
[Docker Docs](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)

上記のサイトの説明がわかりやすく整備されています。

>`Dockerfile` で定義されたイメージを使って作成されるコンテナは、可能ならばエフェメラル（短命；ephemeral）にすべきです。私たちの「エフェメラル」とは、停止・破棄可能であり、明らかに最小のセットアップで構築して使えることを意味します。

上記サイト引用の通り`Dockerfile`は最小のセットアップで構築する事を目指しております。
以降で今回作成する`Dockerfile`を項目毎に説明していきます。

### ファイルの構成

#### Rubyバージョンの指定 Version：2.7

```
FROM ruby:2.7
```

#### rails consoleで日本語を使う為に必要

```
ENV LANG C.UTF-8
```

#### 警告非表示・リッスンポート設定
1. DEBCONFの警告をなくすための設定<br>
[Ubuntu manual]
http://manpages.ubuntu.com/manpages/bionic/man7/debconf.7.html
- apt-keyコマンドによって出る警告防止の為
- Dockerのマウント領域に生成されるファイルが、<br>
キャッシュディレクトリの書き込み権限が無くてエラーを吐くケースがあリます。<br>
XDG_CACHE_HOMEに書き込み権限のあるディレクトリを指定してますが、<br>
どうもRailsのみの使用では発生しなさそうです。<br>
問題なければ外してもいいと思います。
- コンテナが接続用にリッスンするポート

```
ENV DEBCONF_NOWARNINGS yes
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE yes
ENV XDG_CACHE_HOME /tmp
EXPOSE 3000
```

#### コンテナパッケージインストール
1. apt-get とapt-get install-y でキャッシュ利用されず最新バージョンのパッケージを使用できる。
- ビルドツールのインストール
- PostgreSQL対応に必要です。
- vimのインストール*
- lessコマンド(linux)を追加する為に指定しています。*
- オープンソースのツールパッケージgraphvizを使う為*

*印部分は使わない場合は削除してもいいと思います。

```
RUN apt-get update -qq && apt-get install -y \
    build-essential \
    libpq-dev \
    vim \
    less \
    graphviz
```

#### yarnのインストール<br>
Rails6よりWebpackが標準となったので、yanrインストールがないとエラーが発生します。

1. https対処の為 `apt-transport-https` でパッケージをインストールしています。
- その後のコマンドはyarnのインストールです。

```
RUN apt-get install apt-transport-https
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y yarn
```

#### ワークディレクトリの作成

```
# setting work directory
RUN mkdir /app
WORKDIR /app
```

#### 環境変数設定

- スクリプトやコンテナに入ったときに使う為

```
ENV HOME /app
```

#### Gemfile作成

```
COPY Gemfile /app/Gemfile
```

---

上記全てを記載すると下記の内容になると思います。
（＊部分的にコメント追記しております。）

```
# use ruby version 2.7
FROM ruby:2.7

# using japanese on rails console
ENV LANG C.UTF-8

# remove warn
ENV DEBCONF_NOWARNINGS yes
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE yes
ENV XDG_CACHE_HOME /tmp
EXPOSE 3000

# install package to docker container
RUN apt-get update -qq && apt-get install -y \
    build-essential \
    libpq-dev \
    vim \
    less \
    graphviz

# install yarn
RUN apt-get install apt-transport-https
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y yarn

# setting work directory
RUN mkdir /app
WORKDIR /app

# setting environment value
ENV HOME /app

# executing bundle install
COPY Gemfile /app/Gemfile
```

## ②Docker-compose.yml ファイルの作成

#### docker-composeのversion指定
現状は３です。

```
version: '3'
```

#### service作成
- image<br> 
データベース・webなどを指定します。<br>
今回使われそうな `db` `redis` `chrome` `web: &app` `spring` `solagraph`を指定しました。<br>
必要に応じて削除等して修正頂ければと思います。
- port<br>
ポートの設定（一般的に設定されるポート内容で作成しております。）
- volume<br>
マウント設定。db等Dockerコンテナ削除後に抹消しては困るデータの格納先の指定です。
- build<br>
`Dockerfile`等があるパスを示しますが、基本は`.`でカレントディレクトリとなります。

---
補足的事項

- `shm_size`<br>
selenium-chromeを使用した場合メモリ不足なる事あるので少し増やしています。
- `.:`<br>
docker-composeの機能でボリュームの読み込みを高速化するために使用しています。
- `<<: *app`<br>
webサービスの設定をspringに継承しています。
- `tty: true`<br>
コンテナを起動し続ける設定
- ポート設定について<br>
左側がホスト側のポートで、右側がコンテナ側のポートとなります。<br>

---

これも上記全て記載してまとめると下記の通りとなります。

```
version: '3'
services:
  db:
    image: postgres
    ports:
      - "5432:5432"
    volumes:
      - data:/var/lib/postgresql/data:cached
    environment:
      POSTGRES_PASSWORD: postgres
  redis:
    image: redis
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
       - redis-data:/data:cached
  chrome:
    image: selenium/standalone-chrome
    ports:
      - "4444:4444"
  web: &app
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app:cached
      - bundle:/usr/local/bundle:cached
      - /app/.git
    environment:
      HOME: /app
      RAILS_ENV: development
    ports:
      - "3000:3000"
    tty: true
    links:
      - db
  spring:
    <<: *app
    command: bundle exec spring server
    ports: []
  solargraph:
    <<: *app
    command: bundle exec solargraph socket --host=0.0.0.0 --port=7658
    ports:
      - "8091:7658"
    links: []

volumes:
  bundle:
    driver: local
  data:
    driver: local
  redis-data:
    driver: local
```


## ③スクリプト作成
シェルスクリプトで実行内容をまとめておきます。こうする事により`Dockerfile`を「停止・破棄可能であり、明らかに最小のセットアップで構築して使えること」が実現できます。シェルスクリプトとは、Unixコマンドを実行するために使用されるものです。

#### シェルスクリプトの基本抜粋
シャルスクリプトの解説記事ではありませんが、自身が対象シェルスクリプトを理解する為に基本事項を抜粋し記載します。
記事作成とシェル動作の理解にあたり下記サイトを参考にさせて頂きました。
[初心者向けシェルスクリプトの基本コマンドの紹介]
https://qiita.com/zayarwinttun/items/0dae4cb66d8f4bd2a337
[シェルスクリプトリファレンス]
https://shellscript.sunone.me/

- 宣言
`#!/bin/bash`
シェルスクリプトの宣言を行うために必要。
`#!/bin/sh`の書き方もありますが、`bash`の独自機能を使う場合は`sh`ではNGとなります。
参考にしたサイト：https://sechiro.hatenablog.com/entry/20120806/1344267619
特にこだわりがなければ、#!/bin/bash とするといいと思われます。

- コメントアウト
先頭に#をつけるだけです。

- 出力
`echo`で出力

- if条件
  - `if`の基本の書き方は `if [ 条件 ] then コマンド fi` です。
  - 条件が真の場合 `then` の次のコマンドを実行します。
  - 違う条件の場合 `elif [ 条件2 ]` を確認します。
  - 真の条件がない場合 `else` の次のコマンドを実行して終了します。
  - `else` がない場合は、そのまま終了します。

- 引数の取り方
変数をアクセスする時変数名の前に`$`を入れます。あるいは`$`入れて変数を`{}`で囲みます。

- `-e`
特殊テキストをエスケープ

- `$*`
全部の引数をまとめて1つとして処理。同じような処理に`$@`があります。
ダブルコーテーションで囲なければ$@と同じであるが、囲むと引数毎に出力するか、
まとめて一つの引数で出力するかの違いがあります。

- `$1`
1番目の引数

- `$0`
スクリプト名

- 関数
シェルスクリプトでは、関数を書いて引用することができます
`Function () {
echo
}`

- 終了処理
スクリプトを途中で終了するには`exit`コマンドに数字を与えます。

- Swith
switchの基本の書き方は `case 変数 in 条件・値) コマンド ;; esac` です。
条件・値が変数と合う場合次のコマンドを実行します。

### シェルスクリプト構成の概要

1. 最初にdocker-composeのコマンドをフルパスでdcに指定します。
2. dockerがインストールしていない場合はインストールをするようにメッセージが出ます。
3. それぞれの変数に値を格納します(rm, app, db, app_name)
4. echoで$1のコマンド値を実行
5. $1は関数で与えられた値（関数によってはswitch構文で展開）される与えられたコマンドを処理する形になります。
6. $0でスクリプト名を記載しhelpとして使用できるようにしています。


上記内容で、各種コマンドや設定事項を入れ込んだスクリプトファイルを作成すると下記の通りとなります。

```qs.bash
dc=$(which docker-compose) # docker-compose command with full path

if [[ -x "$dc" ]]; then
  :
else
  echo "Please install Docker before run this command."
  exit 2
fi

rm="--rm" # To destroy a container

app="web" # describe $application service name from docker-compose.yml

db="db" # describe database service name from docker-compose.yml

app_name=$(pwd | awk -F "/" '{ print $NF }') # get project dir name

# define container name
app_container="${app_name}_${app}_1"
db_container="${app_name}_${db}_1"

echoing() {
  echo "========================================================"
  echo "$1"
  echo "========================================================"
}

rm_pids() {
  if [ -f "tmp/pids/server.pid" ]; then
    rm -f tmp/pids/server.pid
  fi
}

create_project() {
  echoing "Exec Bundle Install for executing rails new command"
  compose_build $app
  bundle_cmd install

  echoing "Exec rails new with postgresql and webpack"
  bundle_exec rails new . -f -d=postgresql $*

  echoing "Update config/database.yml"
  mv database.yml config/database.yml

  echoing "Exec db create"
  bundle_exec rails db:create

  echoing "docker-compose up"
  compose_up $app

  echo "You can access to localhost:3000"
}

init_services() {
  echoing "Building containers"
  $dc down -v
  $dc build --no-cache $app

  bundle_cmd install

  if [ "--webpack" == "$1" ]; then
    run_yarn install
  fi

  rails_cmd db:migrate:reset
  rails_cmd db:seed

  rm_pids

  $dc up $app
}

compose_up() {
  echoing "Create and start containers $*"
  rm_pids
  $dc up -d "$1"
}

compose_down() {
  echoing "Stop and remove containers $*"
  $dc down $*
}

compose_build() {
  echoing "Build containers $*"
  $dc build $*
}

compose_start() {
  echoing "Start services $*"
  rm_pids
  $dc start $*
}

compose_stop() {
  echoing "Stop services $*"
  $dc stop $*
}

compose_restart() {
  echoing "Restart services $*"
  $dc restart $*
}

compose_ps() {
  echoing "Showing running containers"
  $dc ps
}

logs() {
  echoing "Logs $*"
  $dc logs -f $1
}

invoke_bash() {
  $dc run $rm -u root $1 bash
}

invoke_run() {
  renv=""
  if [ -n "$RAILS_ENV" ]; then
    renv="-e RAILS_ENV=$RAILS_ENV "
  fi

  if [ -n "$TRUNCATE_LOGS" ]; then
    renv="$renv -e TRUNCATE_LOGS=$TRUNCATE_LOGS "
  fi

  dbenv=""
  if [ -n "$DISABLE_DATABASE_ENVIRONMENT_CHECK" ]; then
    dbenv="-e DISABLE_DATABASE_ENVIRONMENT_CHECK=$DISABLE_DATABASE_ENVIRONMENT_CHECK "
  fi

  $dc run $rm ${renv}${dbenv}$*
}

run_app() {
  invoke_run $app $*
}

run_db() {
  invoke_run $db $*
}

run_spring() {
  $dc exec spring $*
}

run_solargraph() {
  invoke_run solargraph $*
}

rails_server() {
  rm_pids
  # $dc run $rm ${renv}--service-ports $app rails s -p 3000 -b 0.0.0.0
  $dc run $rm --service-ports $app bundle exec foreman start -f Procfile.dev
}

rails_db() {
  case "$1" in
  set)
    rails_cmd db:migrate
    ;;
  up)
    rails_cmd db:migrate:up VERSION="$2"
    ;;
  down)
    rails_cmd db:migrate:down VERSION="$2"
    ;;
  reset)
    rails_cmd db:reset
    ;;
  *)
    rails_cmd db:migrate:status
    ;;
  esac
}

spring_db() {
  case "$1" in
  set)
    spring_cmd rake db:migrate
    ;;
  up)
    spring_cmd rake db:migrate:up VERSION="$2"
    ;;
  down)
    spring_cmd rake db:migrate:down VERSION="$2"
    ;;
  reset)
    spring_cmd rake db:reset
    ;;
  *)
    spring_cmd rake db:migrate:status
    ;;
  esac
}

spring_dive() {
  $dc exec spring bash
}

rails_cmd() {
  bundle_exec rails $*
}

rake_cmd() {
  bundle_exec rake $*
}

rspec_cmd() {
  $dc start chrome
  bundle_exec rspec $*
}

test_cmd() {
  bundle_exec test $*
}

bundle_cmd() {
  run_app bundle $*
}

bundle_exec() {
  run_app bundle exec $*
}

rubocop_cmd() {
  bundle_exec rubocop $*
}

rails_console() {
  bundle_exec rails c $*
}

spring_cmd() {
  run_spring spring $*
}

solargraph_cmd() {
  run_solargraph solargraph $*
}

rake_reset_db() {
  echoing "Running reset db"
  compose_stop $app
  DISABLE_DATABASE_ENVIRONMENT_CHECK=1 rake_cmd "db:reset"
  rake_cmd "db:fdw:setup"
  RAILS_ENV=test rake_cmd "db:fdw:setup"
  compose_up $app
}

db_console() {
  # from config/database.yml
  database="development"
  username="postgres"
  port="5432"

  run_db psql -h $db_container -p $port -U $username $database
}

db_dump() {
  # from config/database.yml
  database="development"
  username="postgres"
  port="5432"

  tm=$(date +\%Y\%m\%d-\%H\%M)
  dump_file=tmp/dbdump-${dbname}-${tm}.dump

  echoing "Dump database $dbname data to $dump_file"

  run_db pg_dump -h $db_container -p $port -U $username --disable-triggers $database >$dump_file
  echo "done"
}

run_yarn() {
  run_app bin/yarn $*
}

run_npm() {
  run_app npm $*
}

run_webpack() {
  run_app webpack $*
}

cmd=$1
shift
case "$cmd" in
setup)
  create_project $* && exit 0
  ;;
init)
  init_services $* && exit 0
  ;;
ps)
  compose_ps && exit 0
  ;;
up)
  compose_up $* && compose_ps && exit 0
  ;;
build)
  compose_build $* && exit 0
  ;;
start)
  compose_start $* && exit 0
  ;;
stop)
  compose_stop $* && exit 0
  ;;
restart)
  compose_restart $* && exit 0
  ;;
down)
  compose_down $* && exit 0
  ;;
logs)
  logs $*
  ;;
bash)
  invoke_bash $*
  ;;
run)
  invoke_run $*
  ;;
server)
  rails_server $*
  ;;
rails)
  rails_cmd $*
  ;;
db)
  rails_db $*
  ;;
cons)
  rails_console $*
  ;;
rake)
  rake_cmd $*
  ;;
rspec)
  rspec_cmd $*
  ;;
test)
  test_cmd $*
  ;;
bundle)
  bundle_cmd $*
  ;;
rubocop)
  rubocop_cmd $*
  ;;
reset-db)
  rake_reset_db
  ;;
psql)
  db_console $*
  ;;
db-dump)
  db_dump $*
  ;;
yarn)
  run_yarn $*
  ;;
npm)
  run_npm $*
  ;;
webpack)
  run_webpack $*
  ;;
spring)
  spring_cmd $*
  ;;
sdb)
  spring_db $*
  ;;
sdive)
  spring_dive $*
  ;;
solargraph)
  solargraph_cmd $*
  ;;
*)
  read -d '' help <<-EOF
Usage: $0 command

Service:
  setup    Create new rails application
  init     Initialize backend services then run
  ps       Show status of services
  up       Create service containers and start backend services
  down     Stop backend services and remove service containers
  start    Start services
  stop     Stop services
  logs     [options] default: none. View output from containers
  bash     [service] invoke bash
  run      [service] [command] run command in given container

App:
  server   Run rails server
  rails    [args] Run rails command in application container
  rake     [args] Run rake command in application container
  db       [args] Run rails db command you can use set(migrate), up, down, reset, other is status
           ex: ./qs db set #running rails db:migrate
               ./qs db up 2019010101 #running rails db:migrate:up VERSION=2019010101
  rspec    [args] Run rspec command in application container
  test     [args] Run Minitest command in application container
  bundle   [args] Run bundle command in application container
  cons     Run rails console
  rubocop  [args] Run rubocop
  yarn      Run yarn command in application container
  npm       Run npm  command in application container
  webpack   Run webpack  command in application container

Spring
  spring    Exec spring command in Spring container
  sdive     Into spring container
  sdb       [args] Run rails db command you can use set(migrate), up, down, reset, other is status
             ex: ./qs db set #running rails db:migrate
                 ./qs db up 2019010101 #running rails db:migrate:up VERSION=2019010101

Solargraph
  solargraph Run solargraph command in Spring container

DB:
  reset-db  reset database in DB container
  psql      launch psql console in DB container
  pg-dump   dump database data as sql file in DB container
EOF
  echo "$help"
  exit 2
  ;;
esac
```

## その他作成ファイル
その他の設定ファイルですが、一般的な構築で作成しておりますので説明は割愛致します。
補足的な事項として、DBとしてPostgreSQLを選んでおります。
理由としてMySQLではRails6.0よりバリデータのエラーが発生する事があり設定を追加する必要がある為です。
この点に関してはMySQLを使用する場合は設定を行えばいいのですが、
PostgreSQLでは設定不要の為、こちらをDBとして採用しています。
詳細は下記サイトを参照ください。
[Rails 6.0で"Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1."という警告が出たときの対処法](https://qiita.com/jnchito/items/e23b1facc72bd86234b6)

```database.yml
default: &default
  adapter: postgresql
  encoding: utf8
  min_messages: WARNING
  host: db
  port: 5432
  username: postgres
  password: postgres
  pool: 5
  timeout: 5000
  stats_execution_limit: 10

development:
  <<: *default
  database: development

test:
  <<: *default
  database: test

production:
  <<: *default
  database: production
```

Gemfile

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

gem 'rails', '~> 6.0'
```

.gitignore

```
.bash_history
.bundle
.solargraph
.atom
.vscode
.viminfo
```

## あとがき
ローカルでのRails6の開発環境構築はhomebrewでrbenvなどを使いながら
それほど大きな工数がかからなかった印象ですが、安定的なコンテナでの開発環境の構築は、
Docker公式サイトでもRails5での記載で、Rails6での構築は難しい印象でした。

また公式サイトのQuickStartでは実行コマンドがDockerfileに記載されており、
出来る限り最小のセットアップで記載する場合、シェルスクリプトへ分けた方が
開発環境としてメンテナンス性も含めいいと考えられます。
*本番環境はDockerfileが変わる可能性も含め

この記事を読了頂きありがとうございました。何らか開発の助けになれば幸いです。

最後に、この記事をまとめるにあたり多くの協力とご助言を賜りました。
ありがとうございました。

## 参照サイト・資料元
[Quickstart Compose and Rails](https://docs.docker.com/compose/rails/)
[Dockerfileのベストプラクティス](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)
[Bashスクリプト実行サンプル](https://github.com/belion-freee/rails_web_api/blob/master/qs)
新しいLinuxの教科書 SB Creative