知識０から始めるRails on Docker

#はじめに

Railsの勉強を始めようと思ったが、どうせならDockerを使って仮想環境で開発したい。
しかしどちらも実務で使ったことがないためよくわからない（というかRubyすら触ったことがない）。

ということで、一から調べてみた。RailsとDockerの欲張りセット。
筆者はMacBookProを使っているため、MacOS向けの記事となります。

# tl;td

- Rubyにまつわる各用語の確認、ローカル環境でRailsの導入・起動方法の確認
- 手動でDocker上でのRailsの導入・起動方法の確認
- `dockerfile`にRailsの導入・起動情報を文書化して格納し、`docker-compose`から呼び出して起動

# 対象読者

- わたし
- これからRailsを勉強しようという方
- これからDockerを勉強しようという方

# ロードマップ（超簡易版）

最終目標：Dockerによる仮想環境上でRails serverを起動し、開発できるようにする
※RailsのデフォルトサーバーはPumaです。この記事ではデフォルトのままなのでPumaを使いますが、便宜上「Rails server」と呼称します。

- Railsをローカル環境で起動する
  - ローカル環境にRubyを導入する
  - ローカル環境にRailsを導入する
  - Railsプロジェクトを作成し、Rails serverを起動
- Docker環境を構築して仮想環境でRails serverを起動する
  - ローカル環境にDockerを導入する
  - DockerでRubyを使える仮想環境を構築する
  - 仮想環境でRails serverを起動する
- `dockerfile`と`docker-compose`を使ってコマンド一つでRails serverを起動する
  - `dockerfile`を作成する
  - `docker-compose.yml`を作成し、起動する

# Railsをローカル環境で起動する

これについては各所でがっつり解説されているので、ここでは流れ・用語の説明と参考資料の紹介にとどめます。
なお、この章の目的はRubyにまつわる各用語とRails serverの起動までの流れについて確認することが目的なので、既にバッチリの方は飛ばしてください。
参考資料：[Ruby初学者のRuby On Rails 環境構築【Mac】](https://qiita.com/TAByasu/items/47c6cfbeeafad39eda07)

## ローカル環境にRubyを導入する

流れ
Homebrewの導入（更新）-> rbenvの導入 -> Rubyの導入

### ざっくりHomebrew解説
- MacOSにおけるパッケージ管理ツール
- Rubyのためだけのものではない
- 参考資料：[homebrewとは何者か。仕組みについて調べてみた](https://qiita.com/omega999/items/6f65217b81ad3fffe7e6)

### ざっくりrbenv解説
- Rubyのバージョンを管理するためのツール
- Rubyのバージョン管理をrbenvで、rbenvのバージョン管理をHomebrewで行う（Homebrewは自分でやる）
- 参考資料：[rbenvとは？（rbenvを利用したRubyのインストール）](https://qiita.com/yunzeroin/items/33a51c805e60ed5eca0e)

## ローカル環境にRailsを導入する

流れ
Bundlerの導入 -> BundlerでGemfileの作成 -> Gemfileの編集 -> Gemの取得（Railsの導入）


### ざっくりGem解説
- RubyGemsが公開しているライブラリ
- 要はRubyの開発で使える便利なツール群のこと
- RailsもGemの一つ
- 参考資料：[Ruby on Rails 初心者必見！パッケージ管理ツール『gem』を徹底解説](https://blog.codecamp.jp/rails-gem)

### ざっくりBundler解説
- Gemの管理ツール
- `Gemfile`から利用するGemの情報を読み取り、ライブラリを自動で構築してくれるすごいヤツ
- この記事では`$ bundle init`と`$ bundle install`さえ覚えておけばOK
- 参考資料：[Bundlerの使い方](https://qiita.com/oshou/items/6283c2315dc7dd244aef)
- 参考資料：[bundle install と bundle updateの違いについて](https://qiita.com/lasershow/items/1a048d03ddaaba98171e)


### Railsプロジェクトを作成し、Rails serverを起動

流れ
Railsからプロジェクトを作成 -> Rails serverの起動 -> 接続確認

# Docker環境を構築して仮想環境でRails serverを起動する

## ローカル環境にDockerを導入する

[Docker公式](https://docs.docker.com/docker-for-mac/)から最新版を取得しましょう。
アカウントの登録が必要です。

なお、Dockerの基礎的な仕組みやコマンドなどについては、以下の参考資料がおすすめです。

参考資料：[Dockerでプログラマが最低限知るべきことが、最速でわかるチュートリアル
docker](https://qiita.com/woz/items/43462398765f8036f3a8)
参考資料：[【図解】Dockerの全体像を理解する -前編-](https://qiita.com/kotaro-dr/items/b1024c7d200a75b992fc)

### ざっくりDocker解説
- 仮想環境の構築・管理を行うツール
- 擬似的に様々な環境を構築することができ、ローカル環境と本番環境との差異を減らすことができる
- 「本番環境で急に動かなくなった問題」や「環境構築面倒すぎる問題」の救世主……らしい

## DockerでRubyを使える仮想環境を構築する

参考資料：[RailsアプリをDockerで開発するための手順](https://qiita.com/togana/items/30b22fc39fe6f7a188ec)

ここからが本題です。
まずは利用するイメージを決めます。Docker Hubに`ruby`という便利なイメージが用意されているので、ありがたく使わせていただきましょう。

```terminal:rubyイメージの取得
$ docker pull ruby
```

イメージが取得できたら早速run……する前に、このイメージについて調べましょう。

```terminal:イメージの情報を出力
$ docker inspect ruby
```

コマンドを実行すると大量の文字が吐き出されます。
目眩がするかもしれませんが、頑張って大事な記述を確認しましょう。注目するのは以下の場所です。

```terminal
"ContainerConfig": {
……
  "Env": [
                "PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "RUBY_MAJOR=2.6",
                "RUBY_VERSION=2.6.0",
                "RUBY_DOWNLOAD_SHA256=acb00f04374899ba8ee74bbbcb9b35c5c6b1fd229f1876554ee76f0f1710ff5f",
                "GEM_HOME=/usr/local/bundle",
                "BUNDLE_PATH=/usr/local/bundle",
                "BUNDLE_SILENCE_ROOT_WARNING=1",
                "BUNDLE_APP_CONFIG=/usr/local/bundle"
            ],
……
```

```
RUBY_VERSION=2.6.0
```

Rubyのバージョンを示しています。Docker Hubでも確認できますが、イメージ本体にも情報がしっかり記されていますね。
このバージョンは重要なので覚えておきましょう。あるいはイメージを`pull`する段階で指定してあげてもいいでしょう。

```
BUNDLE_PATH=/usr/local/bundle
```

Bundlerのパスが指定されています。Bundlerによる`Gemfile`を使ったGem管理を行えることがわかりますね。

ではこのイメージからコンテナを起動しましょう。
適当な名前のディレクトリを作成し、ターミナルの作業フォルダとします。
以降はプロジェクト名を`project_name`とします。
※`project_name`以外の名前でも構いません。本記事ではこの名前で統一するということです。

```terminal:rubyイメージからコンテナを起動
$ docker run -i -t --name TEST -p 3000:3000 -v "$PWD":/usr/src/project_name ruby /bin/bash
```

まずは各オプションについて。

```
-i       ：コンテナのSTDIN（標準入力）にアタッチ。標準入力に入力できる状態にするということ。
-t       ：疑似ターミナル (pseudo-TTY) を割り当て。ターミナル画面で操作できるようにするということ。
--name   ：コンテナの名前。今回は'TEST'を指定。
-p       ：ポートの指定。今回は3000ポートを解放しています。
-v       ：ボリュームの指定。[ホストPCのディレクトリ指定]：[コンテナ内のディレクトリ指定]。
/bin/bash：コンテナ起動後に実行するコマンド。今回はシェルを指定。
```

上記の説明は簡略したもので語弊を含むので、気になった項目は別途調べることをお勧めします。
`-i`と`-t`の二つはセットで使う（`-it`）ことが多いので是非覚えておきましょう。
ボリュームについては概念が難しいですが、とりあえず[ホストPCで指定したディレクトリ]を[コンテナ内で指定したディレクトリ]として扱えるようにする、程度の理解でいいと思います。

参考資料：[Dockerリファレンス](http://docs.docker.jp/engine/reference/commandline/run.html)

うまく起動できたら下のような状態になるはずです。

```terminal
root@03eb04059b1f:/#
```

コンテナの中のターミナルを操作しているようなイメージですね。
コンテナの中から一旦抜ける時は`control + pq`、再接続するときは`$ docker attach [コンテナの指定]`です。
※`exit`でも抜けられますが、コンテナが停止してしまうため注意。停止した場合、ポートを閉じてしまうようです。
※`$ docker exec`でもコンテナに接続できます。ただし、プロセス接続するごとにプロセスが増えます。

## 仮想環境でRails serverを起動する

それでは今起動したコンテナでRails serverの準備をしましょう。
先ほどコンテナを起動した際に、コンテナ内の`project_name`ディレクトリをボリュームとして指定しました。
指定したディレクトリが作成されているはずなので、そこまで移動しましょう。

```terminal:Railsプロジェクトの確認
$ cd /usr/src/project_name
```

このディレクトリにRailsプロジェクトを作成していきます。
まずは何をするにも`Gemfile`が必要なので、Bundlerを使って作成しましょう。

```terminal:Gemfileの作成
$ bundle init
```

次にRailsを導入していきます。
`Gemfile`を編集する必要がありますが、これはコンテナ内で行う必要はありません。`Gemifile`を作成したディレクトリはコンテナ起動時にホストPC側のディレクトリとセットでボリューム指定したため、ホストPC側の指定したディレクトリにも`Gemfile`が作成されています。この`Gemfile`を編集するだけでOKです。

```ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

- # gem "rails"
+ gem "rails"
```

```terminal:コンテナ内のGemfile確認に反映されているか確認
$ cat Gemfile
```

これで`Gemfile`の準備ができたので、GemのインストールとRailsプロジェクトの作成を行いましょう。

```terminal:Gemのインストール
$ bundle install
$ bundle exec rails new .
```

`rails exec .`は作業ディレクトリに対してRailsプロジェクトを作成します。
実行すると`Gemfile`の上書き確認をされるので許可しましょう。

ローカル環境ではこの時点でRails serverを起動できましたが、`ruby`イメージから作成したコンテナではまだRails serverは起動できません。
ですが、なぜ起動できないか確認するためにも一度試してみましょう。


```起動確認(失敗する).
$ bundle exec rails server
```

```terminal:エラーメッセージ（最後のみ抜粋）
/usr/src/project_name/vendor/bundle/ruby/2.6.0/gems/execjs-2.7.0/lib/execjs/runtimes.rb:58:in `autodetect': Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)
```

`ExecJS::RuntimeUnavailable`というエラーについて調べてみると、どうやらJavaScriptランタイムというものが必要のようです。
今回は`node.js`を導入することにしましょう。
参考資料：[rails sコマンド実行時に「Could not find a JavaScript runtime.」とエラーが出る場合の対処法](https://qiita.com/azusanakano/items/771dc9919f347de061d7)

`ruby`イメージでは`yum`コマンドは対応していないようなので、`apt-get`を使ってインストールしましょう。
参考資料：[apt-get - パッケージの操作・管理 - Linuxコマンド](https://webkaru.net/linux/apt-get-command/)

```terminal:node.jsのインストール
$ apt-get update
$ apt-get install nodejs
```

完了したら今度こそRails serverが起動できるはずです。

```terminal:Rails_server起動
$ bundle exec rails server
```

[http://localhost:3000/](http://localhost:3000/)に接続して起動できているか確認しましょう。


# dockerfile と docker-compose を使ってコマンド一つでRails serverを起動する

ここからは今まで行ってきたことを設定として文書化していきます。
先ほどまで使用していたコンテナはもう使わないので、`$ exit`で停止しておきましょう。
ただし、作成したRailsプロジェクトはそのままにしておいてください。

参考資料：[Dockerfile リファレンス](http://docs.docker.jp/engine/reference/builder.html)
参考資料：[Compose ファイル・リファレンス](http://docs.docker.jp/compose/compose-file.html)
参考資料：[RailsアプリをDockerで開発するための手順](https://qiita.com/togana/items/30b22fc39fe6f7a188ec)

## dockerfile を作成する

まず、前の章で行ったことを整理しておきましょう。

- `ruby`イメージからコンテナを作成（本記事ではバージョン2.6.0）
- ポート3000:3000を開放し、ボリュームを指定してコンテナを起動
- コンテナ内でRailsプロジェクトを作成
- コンテナに`node.js`をインストール
- Rails serverを実行

`dockerfile`を書くということは、これらの作業を文書化していくということです。

それでは`dockerfile`を書いていきます。Railsプロジェクトを作成したディレクトリに`dockerfile`という名前でファイルを作成し、編集していきます。
まずは使用したイメージの情報です。

```dockerfile:dockerfile
FROM ruby:2.6.0
```

`:2.6.0`はバージョン情報です。指定しなければ最新版が自動で選択されますが、固定させた方がいいでしょう。
これはRubyのバージョンと同じなので、もし違うバージョンからRailsプロジェクトを作成したならそのバージョンに合わせてください。

次にポートの開放とボリューム指定ですが、`dockerfile`ではホストPCのディレクトリを指定することはできません（後で`docker-compose.yml`で指定します）。よって、ここではポートの開放だけを行います。

```dockerfile:dockerfile
FROM ruby:2.6.0

EXPOSE 3000
```

ここでポートの開放を行なっても、`run`コマンドや`docker-compose.yml`で指定しなければ直接接続できないので注意してください。

次はコンテナ内でRailsプロジェクト作成ですが、これは後でボリューム指定により共有する予定なので必要ありません。
ですが、`Gemfile`が無くてはGemのインストールができず、`rails`コマンドが実行できません。
そこで、`Gemfile`だけをコンテナ内にコピーしてインストールすることにします。

```dockerfile:dockerfile
FROM ruby:2.6.0

ENV APP_ROOT /usr/src/project_name 

WORKDIR ${APP_ROOT}

COPY Gemfile ${APP_ROOT}
COPY Gemfile.lock ${APP_ROOT}

RUN bundle install

EXPOSE 3000
```

各コマンドは以下の通りです。

```dockerfile
ENV [key] [value] …… 環境変数の定義。
WORKDIR [ディレクトリ指定] …… 作業ディレクトリの指定
COPY [ソース指定] [保存先指定] …… ファイルのコピー。
RUN [コマンド] …… コマンドの実行。
```

これでコンテナ内でも`rails`コマンドが利用できるようになります。

最後に`node.js`のインストールとRails serverの起動です。

```dockerfile:dockerfile
FROM ruby:2.6.0

ENV APP_ROOT /usr/src/project_name 

WORKDIR ${APP_ROOT}

RUN apt-get update && \
    apt-get install -y nodejs

COPY Gemfile ${APP_ROOT}
COPY Gemfile.lock ${APP_ROOT}

RUN bundle install

EXPOSE 3000

CMD bundle exec rails server
```

Railse sever の起動が`RUN`ではなく`CMD`なのは、コンテナの立ち上げが完了してから実行してほしいからです。
もし`RUN`で書いてしまうとそこで動作が止まってしまい、いつまでもコンテナが立ち上がらないということになってしまいます。

では、これでうまく動作するかどうか確認してみましょう。

```terminal:dockerfileからイメージの作成と起動
$ docker build .
```

エラーが出ずにRails serverの起動までできればOKです。

## docker-compose.yml を作成し、起動する

では、先ほど作成した`dockerfile`を使って`docker-compose.yml`を作っていきましょう。
先ほどと同じく、Railsプロジェクトのディレクトリに作成し、以下の通りに編集してください。


```docker-compose.yml
app:
  build: .
  ports: 
    - '3000:3000'
  volumes: 
    - .:/usr/src/project_name
```

```yml
app: …… アプリケーションの名前。今回はappとした。
build: …… docker-composeのパス指定。今回は同一ディレクトリにある。
ports: …… 開放するポートの指定。
volumes: …… ボリュームの指定。
```

これで完了です。では起動してみましょう。

```terminal:docker-composeからの起動
$ docker-compose up -d
```

[http://localhost:3000/](http://localhost:3000/)に接続して、いつものアレが表示されたらOKです！
ちなみに、ホストPCのディレクトリを参照しているため、ファイルを編集すると即座に反映されます。`Gemfile`と同じですね。

# おわりに
というわけで、起動できました。とりあえず動いたので満足。