コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.2 -Hello, Rails on Docker-

# はじめに
第二回目の今回は、Ruby on RailsをDockerコンテナで起動させるHello worldをやっていきます。

# 今日のゴール
- Ruby on Rails on Docker で Hello world する

では早速、Ruby on Rails on Docker で Hello world していきましょう！

# Hello, Ruby on Rails on Docker
今回は↓の図のように、Docker 上に Rails アプリケーション用のコンテナと PostgreSQL (database) 用のコンテナを作っていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/aed10a68-89ce-488d-6dd3-221c2255e2fd.png)
まず、作業用のディレクトリを作っておきましょう。

```
$ mkdir Handson
$ cd Handson
```
今後、この`Handson`ディレクトリをホームディレクトリとして話を進めますので、特に指定がない場合、`Handson`ディレクトリでコマンドを叩いたり、`Handson`ディレクトリから見た相対パスでファイルを編集していると思ってください。

では早速、Ruby on Rails on Docker な環境を構築するために以下の4つのファイルを作成していきます。

- `Dockerfile`: Rails アプリ用の Docker image の元となる設計図
- `Gemfile`: Rails アプリに必要な`gem`を記載するファイル
- `Gemfile.lock`: `Gemfile`によってインストールされた`gem`のバージョン情報などを管理するファイル
- `docker-compose.yml`: 今回のアプリをコンテナ起動させるための Dcoker Compose ファイル

## Dockerfile
```dockerfile:Dockerfile
FROM ruby:2.6.5-alpine3.11

ENV HOME="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR $HOME

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
      gcc \
      g++ \
      less \
      libc-dev \
      libxml2-dev \
      linux-headers \
      make \
      nodejs \
      postgresql \
      postgresql-dev \
      tzdata \
      yarn && \
    apk add --virtual build-packs --no-cache \
      build-base \
      curl-dev

COPY Gemfile $HOME
COPY Gemfile.lock $HOME

RUN bundle install && \
    apk del build-packs

COPY . $HOME
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
```

Dockerfile は初めて見る人にとっては「なんだこれ？」なものな気がしますが、読んでみると意外とシンプルです。
頭に大文字で書かれているのが`命令`と呼ばれるものでコマンドみたいなものです。
今回の Dockerfile では以下の命令を利用しています。

- `FROM`: ベースイメージを定義する命令です。[ruby](https://hub.docker.com/_/ruby) イメージを指定しているので、元々 Ruby を使用できるイメージの上に Rails を動かす環境を作っていきます。
- `ENV`: 環境変数を定義する命令です。
- `WORKDIR`: 作業ディレクトリを定義する命令です。ベースイメージ内に該当のディレクトリがない場合は、そのディレクトリを作成することもしてくれます。
- `COPY`: ホストのファイルやディレクトリをイメージ内にコピーする命令です。
- `RUN`: コマンドを実行する命令です。
- `EXPOSE`: コンテナがリッスンするポートを宣言する命令です。 Rails ではデフォルトで 3000 番ポートを使用するので 3000 を指定してあげています。
- `CMD`: ソフトウェアを実行するためのコマンドを定義する命令です。コンテナが起動する時に実行されるコマンドといった方がイメージ湧きやすいかもしれません。少し特別な書き方（`["xxx", "xxx"]`みたいな）をしますが、Rails アプリケーションを起動させるコマンドは`rails server -b 0.0.0.0`でして、それを`CMD`の記法で書いています。

あらかた命令と１行１行の内容について述べてしまいましたが、取りこぼしているところをキャッチアップ。

`RUN`宣言で`apk ~`と色々書いている9行目からの部分がありますが、`apk add`は Alpine linux でパッケージをインストールするコマンドです。
`apk update`でパッケージリポジトリの最新のインデックス（インストールできる最新バージョンは何か）を取得してきて、`apk upgrade`ですでにインストールしているパッケージで最新版にアップデートできるものをアップデートします。その後、`apk add`で Rails を起動するのに必要なパッケージをインストールしていきます。`--no-cache`オプションはキャッシュを残さないようにするためのオプションです。不要なキャッシュを残さないことでコンテナ自体を軽量に保つことができます。（コンテナは軽量に保っておいた方がダウンロードに時間がかからなかったり、ホストのボリュームを圧迫しないのでよいとされています。）`--virtual`オプションはそのインストールしたパッケージ達を一つのグループとして名前づけしています。今回の例だと`build-packs`という名前をつけています。ここでインストールしたパッケージは Rails をビルドする上では必要なのですが起動させるためには不要なのであとで`apk del`で削除するために名前づけしています。

その後、`COPY`命令で`Gemfile`、`Gemfile.lock`をイメージ内にコピーして、`bundle install`を実行してます。`bundle install`は`Gemfile`の内容に沿って`gem`をインストールするコマンドです。Rails 自体も`gem`でインストールできるので`Gemfile`に`rails`を記入しておけばこの`bundle install`の際にインストールされます。`Gemfile.lock`はすでにインストール済の`gem`のバージョンなどを管理して無闇にバージョンアップさせないようにしてくれます。

最後に`COPY . $HOME`でローカルホストのファイルを一式イメージ内にコピーすることで Rails アプリケーションを起動させられる Docker image を作ることができます。

## Gemfile
```ruby:Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'
```
Gemfileはかなりシンプルで、インストールする`gem`のソースと`rails`gem をインストールすることを定義しています。Rails は2020.02.02時点で最新バージョンが 6.0.2 なのでまぁメジャーバージョンとして 6 のものをインストールしてくださいというような指定の仕方をしています。
Rails アプリケーションでは最初`rails new`コマンドでアプリに必要な`gem`やファイルをインストール・生成するため、初期ではこれほどシンプルな`Gemfile`があるだけで構わないのです。

`Gemfile`でバージョンを指定する表現方法はいくつかあります。`gem`はGitHubなどで公開されていることが多くて大体 README でこう Gemfile に記載してくれと書かれていることが多いのであんまり気にすることはないかもしれませんが一応紹介。

- `gem 'rails', '6.0.0'`: 絶対 6.0.0 をインストール（バージョンを定義）
- `gem 'rails', >= 6.0.0'`: 6.0.0 より最新のものをインストール（最低バージョンを定義）
- `gem 'rails', >= 6.0.0', < 6.0.2`: 6.0.0 以上 6.0.2 未満のバージョンをインストール（バージョンの範囲を定義）
- `gem 'rails', '~> 6.0.0'`: 6.0.X のバージョンをインストール（マイナーバージョンを定義）

## Gemfile.lock
`Gemfile.lock`は最初に`bundle install`されるときに書き込まれるので、最初は空ファイルで問題ありません。

```
$ touch Gemfile.lock
```
`touch`コマンドはファイルの更新日時を現在時刻に更新するためのコマンドですが、ファイルが存在しない場合は空ファイルを生成してくれるので`Gemfile.lock`を生成するために使いました。

## docker-compose.yml
```yaml:docker-compose.yml
version: '3'

services:
  db:
    image: postgres:12.1-alpine
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./tmp/db:/var/lib/postgresql/data

  web:
    build: .
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
```
まず、サービスとして`db`と`web`の二つがあることがわかるかと思います。`db`は文字通りデータベース用のコンテナ（サービス）、`web`は Rails アプリケーションを動作させるコンテナ（サービス）です。`db`コンテナでは`image`に`postgres:12.1-alpine`を指定しています。
`docker-compose.yml`については前回もお話したので、前回お話していない項目を中心にお話します。

- `environment`: コンテナを起動する時に環境変数としてセットする。この場合、TZ（タイムゾーン）を東京にしてる。
- `volumes`: コンテナからホストのディレクトリをマウントしている。左がホストのパス、右がコンテナ内のパス。ホストのパスはこの`docker-compose.yml`の場所からの相対パスで書いてます。こう書くことで簡単にいえば、ホストのパスとコンテナのパスを同期しているイメージになり、ホストでファイルを編集すればコンテナ内にも反映され、コンテナ内でファイルが編集されればホストのファイルにも反映されるという関係を気づけます。コンテナはステートレスなので一度コンテナを削除して新しくコンテナを起動させた場合、最初のコンテナ（削除したコンテナ）内で変更されたデータは全てなかったことになってしまうのですが、ホストのディレクトリに同期しておくことで次に起動するコンテナもそのディレクトリをマウントするのでデータが永続化されるようになります。
- `build`: `build: .`で`docker-compose.yml`と同じディレクトリの`Dockerfile`をbuildしたイメージを使ってコンテナを起動するようになります。
- `depends_on`: コンテナの依存関係を定義します。今回の場合『`web`は`db`に依存している』と定義していることになりますが、これは次の2つが実現されます。
    - `docker-compose up`した時に、`db`コンテナが起動してから`web`コンテナを起動する
    - `docker-compose up web`で`web`コンテナを起動させようとした場合、`db`コンテナも起動させる

# Rails アプリケーションを新規作成
まずは`rails new`コマンドで新規に Rails アプリを作成します。

```
$ docker-compose run --rm --no-deps web rails new . -fGTd postgresql
...
Webpacker successfully installed 🎉🍰
```
いろいろな要素のあるコマンドですね。ちょっと順を追って説明します。
まずこのコマンドの大枠は`docker-compose run [options] <service name> <command>`です。今回の例では、

- [options]: `--rm --no-deps`
- \<service name>: `web`
- \<command>: `rails new . -fGTd postgresql`

となっています。`docker-compose run`は`docker-compose.yml`の定義にそって対象のコンテナを立ち上げてその中でコマンドを実行し、実行後にコンテナを停止するコマンドです。つまり、`web`コンテナを立ち上げて`rails new . -fGTd postgresql`を`web`コンテナ内で実行してくれます。
`--rm`オプションはコマンド実行後にコンテナを停止した後に削除までしてくれるオプションです。基本停止したコンテナは不要だと思うのでこのオプションをつけるのがよきかと思います。
`--no-deps`オプションは`docker-compose.yml`で`depends_on`が定義されていたとしてもそれを無視して`docker-compose run`を実行することができます。

次に`web`コンテナで実行される`rails new . -fGTd postgresql`を見ていきましょう。
まず`rails new`は Rails アプリケーションを新規作成するためのコマンドです。`.`はアプリケーションを作成する場所を指していてカレントディレクトリ（コマンドが実行されたディレクトリ）を示しています。`-fGTd`はオプションなので一つずつ紐解きます。

- `-f`: ファイルの上書きを強制する。`Gemfile`などに上書きが走りますがいちいち Yes or No を聞かれないようにするためにつけています。
- `-G`: Gitの初期設定をスキップします。Rails 6 からなんかこのオプションをつけないとまともに`rails new`できなかったのでつけてます。
- `-T`: `minitest`という Rails でデフォルトでインストールされるテストフレームワークのインストールをスキップします。僕は`RSpec`というテストフレームワークをよく使っているのでこのオプションをつけて無駄に`minitest`がインストールされないようにしています。
- `-d`: `-d <database name>`で利用するデータベースを指定します。今回は`postgresql`を指定。

Rails アプリケーションの新規作成ができたら一度イメージをビルドしておきましょう。

```
$ docker-compose build
...
Successfully tagged handson_web:latest
```
`docker-compose build`コマンドは`docker-compose.yml`で`Dockerfile`からのビルドが必要なサービスのイメージビルドをすべて実行してくれます。今回は`db`は DockerHub のイメージを使っているので`web`のみがビルドが必要なサービスとしてビルドされます。

# DBの接続設定
ビルドが終わったら、Rails アプリケーションの DB 接続設定をコーディングしていきます。 DB の接続設定は`config/database.yml`に記載します。 Rails では設定系のファイルは`config`ディレクトリに格納されています。

```diff:config/database.yml
  ...
  default: &default
    adapter: postgresql
    encoding: unicode
+   host: db
+   username: postgres
+   password:
    pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  ...
```
`host`、`username`、`password`がデフォルトから新たに追加した項目です。
`host`はDBのホスト名です。今 Rails アプリが稼働している`web`コンテナは`docker-compose.yml`が作る Docker ネットワークの中にいます。この中ではサービス名で名前解決してコンテナが相互に接続することができます。つまり`host`として`db`を設定することで`docker-compose.yml`で`db`のサービス名で定義されたコンテナに接続できるようになるのです。
`username`と`password`は何の値なのでしょうか？これは`db`コンテナのイメージで指定した[`postgres`](https://hub.docker.com/_/postgres)のデフォルト値です。`password`は同じホストからのアクセスであれば省略が可能になっています。`postgres`イメージの`username`と`password`はそれぞれ`POSTGRES_USER`と`POSTGRES_PASSWORD`の環境変数で定義することもできます。

データベースの設定はここまでです。このデフォルトの設定値が開発環境（`development`）、テスト環境（`test`）の設定値として反映されるようになっています。

# データベースを作成する
データベースの接続設定を定義したので、データベースを実際に作成していきます。
Rails ではデフォルトで本番環境（`production`）、開発環境（`development`）、テスト環境（`test`）の3つの環境（environment）が用意されています。特別に環境を指定しない場合、開発環境で挙動するようになっています。
データベースの作成は`rails db:create`コマンドを使いますが、このコマンドは開発環境とテスト環境用にデータベースを作成してくれます。本番環境用のデータベースを作成する場合は、`RAILS_ENV=production`をオプションとしてつけます。
今回はまず開発環境向けにデータベースの作成を行いたいので、以下のコマンドを実行します。

```
$ docker-compose run --rm web rails db:create
```

# Hello Ruby on Rails on Docker!!
ここまでで Hello world に必要な作業は全て完了しました。
コンテナを立ち上げて Hello world ページが表示されることを確認しましょう。

```
$ docker-compose up -d
```

Rails アプリは少し起動に時間がかかります。すぐに`http://localhost:3000`にアクセスしてもまだアプリケーションが起動していないこともありますので、その場合は`docker-compose logs`コマンドを使ってアプリケーションの起動状態を確認してみましょう。

```
$ docker-compose logs -f
```
`-f`オプションはログの変化をリアルタイムでコンソールに表示するためのオプションです。Rails アプリが起動した場合

```
web_1  | => Booting Puma
web_1  | => Rails 6.0.2.1 application starting in development
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 4.3.3 (ruby 2.7.0-p0), codename: Mysterious Traveller
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop
```
というログが表示されます。この表示を確認したら`Ctrl+C`で`docker-compose logs`から抜け出しましょう。

では、`http://localhost:3000`にアクセスしてみましょう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/85942f2b-525b-c61f-9933-e2d8f4752ff6.png)
このようなページが表示されたでしょうか？このページが Rails アプリケーションの第一歩、つまり Hello world です。おめでとうございます！これでもう『Rails は Hello world までならやったことあります』と自慢することができます！

# 後片付け
ここまでできたら後片付けをしておきましょう。
このままではコンテナが起動しっぱなしになってしまうので、最後にコンテナを停止させておきます。

```
$ docker-compose down
```
これでコンテナを停止させたので、`http://localhost:3000`にアクセスしても先ほどのページは表示されなくなっていることでしょう。

# まとめ
今回は、`Dockerfile`や`docker-compose.yml`などのファイルを作成し、Rails アプリケーションを稼働させられる Docker イメージ、Docker コンテナを作成してみました。
さらに、Rails アプリを新規作成して Hello world に成功しました！
まだまだアプリケーション開発のほんの入り口ですが、Docker を使って Web アプリを起動させることができただけでもかなり感動があると思いますし、ここまでさほど大変ではないことも感じてもらえたかなと思います。これこそが Rails や Docker の偉大なところですね。

次回は、`scaffold`という Rails の便利機能を使って、サンプル Web アプリケーションを作ってみようと思います。この`scaffold`で作成できるアプリケーションが Rails の基本的なアプリケーションの形になりまして、その中には Rails アプリケーションを語る上では避けられない`RESTful`や`MVC`の要素が詰まっていますので、その辺りも合わせて学んでいけるようにしようと思います。

では、次回も乞うご期待！ここまでお読みいただきありがとうございました！

Next: [コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.3 - Scaffold, RESTful, MVC - - Qiita](https://qiita.com/at-946/items/33f096baf143602003da)

# 本日のソースコード
- [at946/rails_on_docker_handson at vol.02](https://github.com/at946/rails_on_docker_handson/tree/vol.02)

# Reference
- [Dockerfile を書くベスト・プラクティス — Docker-docs-ja 17.06.Beta ドキュメント](http://docs.docker.jp/engine/userguide/eng-image/dockerfile_best-practice.html)
- [Ruby on Rails チュートリアル：実例を使って Rails を学ぼう](https://railstutorial.jp/)

# Other Hands-on Links
- https://github.com/at946/rails_on_docker_handson/tree/master