Dockerを使ってRails5の初期環境をサクッと構築する

## はじめに

そうだ、Rails環境、作ろう。
(BGM: 「My Favorite Things / The Sound of Music」）

## この記事の目標

- `docker compose up` したらホスト側からrailsのウェルカムページが表示できるコンテナイメージを作成するDokerfileおよびdocker-compose.ymlを作成する。
- DBは個人的な好みでpostgresqlにする（それほど特にこだわりはないけど）
- DBはホスト側のSQLツールから接続できるようにする。
- ホスト側の環境は極力汚さない。

## 作業環境

- Windows10 Pro（MacOSでも手順は変わらないと思います）
- Docker Desktop version 4.1.1
- Visual Studio Code 1.59.1 
 - 以下の機能拡張をインストールしておく。
 - Remote Development
 - Docker

## 作成手順

### docker-compose.ymlの構成

コンテナを2つ作ることとします。

- web: railsのコンテナ
- db: postgresqlのコンテナ

### ベースになるrubyコンテナの作成

docker-compose.ymlを作る予定のフォルダに、`dockerfiles/web`フォルダを作成します。
コンテナのビルドに必要なファイルはここに入れることにします。

まずはDockerfileを[公式のdocker composeチュートリアル](https://docs.docker.jp/compose/rails.html)を参考に作成します。

```Dockerfile:dockerfiles/web/Dockerfile
FROM ruby:2.7
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
```

```Gemfile:dockerfiles/web/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 5'
```

dockerfiles/web/Gemfile.lock は空のファイル。

とりあえずrailsをインストールできるところまでの記述です。

次にcompose用のファイルを作成します。同様にチュートリアルからの参照です。

```yml:docker-compose.yml
version: '3'
services:
  db:
    image: postgres
    volumes:
      - dbdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp_development
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
    ports:
      - 5432:5432
  web:
    build: ./dockerfiles/web/
    tty: true
    volumes:
      - myapp:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  dbdata:
  myapp:
```

ポイントは、
- dbコンテナは環境変数でpostgresのユーザ名、パスワード、デフォルトデータベース名を記載しておく。
- dbコンテナのポートはホスト側からアクセスできるように空けておく
- dbのデータ格納場所とweb側のアプリデータ格納場所は`volumes`で永続化する。その際ホスト側にマウントはしない。
- web側のポートも同様にrails内部サーバデフォルトの3000を開けておく。
- web側は `tty: true`を指定しておく。後からコンテナのシェルにアタッチしたいため。

### rails側コンテナの整備

ここで一度`docker compose up -d`してコンテナをビルドし実行します。
無事エラーなくコンテナがupできたら、vscodeのdockerタブから今立ち上がったwebコンテナを右クリックしvscodeでアタッチします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/20689cfc-d8a4-9a2a-e636-44830d443de4.png)
新しくvscodeのウィンドウが開き、必要な機能拡張がコンテナ側にインストールされていきます。
インストールが完了したら、railsアプリを入れる予定の `/myapp`ディレクトリを「フォルダーを開く」ボタンから選択して開きましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/de7a3dec-8772-31b1-d91c-71148d9045e5.png)

エクスプローラータブにフォルダが開けばOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/2fa55fd9-e46b-3bd9-4059-40040f8b940a.png)

新しいターミナルを開き、クイックスタートの内容に沿ってrailsアプリを初期化します。

```sh:shell
$ rails new . --force --database=postgresql
```

すると、こんなメッセージが表示されます。

```
Bundler could not find compatible versions for gem "sprockets":
  In snapshot (Gemfile.lock):
    sprockets (= 4.0.2)

  In Gemfile:
    sass-rails (~> 5.0) was resolved to 5.1.0, which depends on
      sprockets (< 4.0, >= 2.8)

    rails (~> 5.2.6) was resolved to 5.2.6, which depends on
      sprockets-rails (>= 2.0.0) was resolved to 3.4.0, which depends on
        sprockets (>= 3.0.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
         run  bundle exec spring binstub --all
bundler: command not found: spring
Install missing gem executables with `bundle install`
```

使用するモジュール間で依存するモジュールのバージョンにコンクリフトがあるようです
（sass-railsとrailsで共に使用しているsproketsのバージョンコンクリフトの模様）
解決するには `bundle update`しなさい、と書かれているので実際に実行します。
なお、このコマンドの終了コードは"0"で、すなわちエラー終了ではありません（重要！）

```shell:shell
$ bundle update
```
ずらずらとログが流れますが特に問題なく終了します。
これでアプリフォルダの作成は終了したので、実際にサーバを起動してウェルカムページが表示されるかチェックします。

```shell:shell
$ rails server -b 0.0.0.0
```
`-b 0.0.0.0`は必ず指定します。そうしないとコンテナ内部からしか接続できません。ホスト側から接続しないなら`0.0.0.0`でListenしなければなりません。
無事サーバが起動したら、ホスト側から http://localhost:3000/ にアクセスして動作チェックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/18a5932b-2bfb-71ba-1f93-8befe58b7739.png)

エラーが出ました。postgresに接続できないエラーのようです。
それもそのはず、今回postgresはdbコンテナを別で用意していますが、railsのデフォルトのDB接続先はlocalhostです。
`config/database.yml`を修正してデータベースの情報を追記しなければなりません。

```yml:config/database.yml
（抜粋）
# Configure Using Gemfile
# gem 'pg'
#
default: &default
  adapter: postgresql
  host: db
  username: postgres
  password: postgres
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # http://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
```

`host` `username` `password` を追記しました。
この3つは`docker-compose.yml`でdbコンテナに指定した環境変数の値に合わせます。

この変更を保存してrailsサーバーを再起動することにより、無事にウェルカムページが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/766c63a6-5271-714d-dd1a-e5574020e330.png)

このdatabase.ymlを変更する作業は `rails new`した後、つまりconfig/database.ymlが作成された後であれば `bundle update`する前に行っても問題ありません。

### コンテナup後に実行した変更をDockerfileに反映させる

up後に実行した内容は以下の通りです。

- `rails new . --force --database=postgresql`
- config/database.yml の書き換え
- `bundle update`
- railsサーバ起動

この作業をDockerfileに反映させます。

### database.yml の取得

vscodeのエクスプローラーからdatabase.ymlを右クリックし、「ダウンロード」を選択するとホスト側にファイルを転送できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/fe130028-54da-3e32-fc9d-d7847d226f03.png)

`dockerfiles/web`フォルダ内に保存しましょう。

### Dockerfileの書き換え

Dockerfileを以下の様に書き換えます。

```Dockerfile
FROM ruby:2.7
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
# ここまでが既存作業分
# ここからが追加作業分
RUN rails new . --force --database=postgresql
# bundle update前にhost等を書き換えたdatabase.ymlに差し替える
COPY database.yml /myapp/config/database.yml  
RUN bundle update

# entrypointのヘルパースクリプトを追加する
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
# portのEXPOSEを忘れずに
EXPOSE 3000

# up時にサーバーが起動状態になるようCMDを設定
CMD ["rails", "server", "-b", "0.0.0.0"]
```

entrypoint.sh には不意にコンテナが正常終了しなかった場合、railsのserver.pidが残るので再起動前に削除するために配置します。

```sh:entrypoint.sh
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
```

RUNコマンドは終了コードが0以外だとコンテナのbuild時にそこでコンテナの構築が停止してしまいます。
先ほどの `rails new`コマンドはエラーのようなメッセージは出ましたが終了コードが0なのでそのまま次のコマンドを実行することができます。

Dockerfileを書き換えたらコンテナをdownし、再ビルドします。
念のため永続化ボリュームもいったん削除して再構築します。

```
docker compose down -v
docker compose build --no-cache
```

buildが無事終了したら、upして http://localhost:3000/ へアクセスして無事ウェルカムページが表示できるかどうか確かめてみましょう。
表示できたら環境構築成功です。お疲れさまでした。

## 環境構築後

`rails new`や`bundle update`するためにvscodeからwebコンテナにアタッチしました。以後はvscode内でrailsコマンドを実行したり、通常の開発と同じようにvscodeのエクスプローラからファイルの作成等がローカル側での開発と同様に可能となります。
ホスト側にあるファイルもvscodeのエクスプローラにドラッグ＆ドロップすれば簡単にコピーできます（逆にコンテナ側からホスト側へのファイルコピーは右クリック→ダウンロードになります）
vscodeを使い慣れていればローカルで開発するのと同じような感覚でコーディングが可能です。

## おわりに

今回は「一発でrailsサーバーが起動するDocker環境構築」を行いました。
今回のrails環境構築の様に、単にRUNコマンドでインストールしていくだけでは環境構築がうまくいかない場合、エラーが出ないところまで環境構築し、その後はコンテナ側で追加のインストール作業を行い、その作業結果をDockerfileにフィードバックする方法を取ると良いでしょう。
他のコンテナの環境構築にも十分応用できると思います。

### おまけ：今回作成したファイル一式

githubに公開しています。
https://github.com/haruyan-hopemucci/docker-rails5-postgres