Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #19 Docker編

# こんな人におすすめ
* プログラミング初心者でポートフォリオの作り方が分からない
* Rails Tutorialをやってみたが理解することが難しい

前回：[#18 EC2環境構築, Nginx+Puma+Capistrano編](https://qiita.com/aokyo17/items/281b5a1658ea58bc7b24)
番外：[#18.5 環境変数, Gmail送信設定編](https://qiita.com/aokyo17/items/98e8e48d309a506db4e8)
次回：準備中

# 今回の流れ

1. 完成のイメージを理解する
* Dockerを導入する理由を知る
* RailsアプリをGitHubからクローンする
* ローカルの開発環境を整える
* Dockerをインストールする
* Dockerの仕組みを理解する
* Dockerのファイルを作成する
* Dockerを起動する
* トラブルシューティング

この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回は、**Railsアプリの開発環境にDocker**を組み込みます。
**ローカル（ホスト）にはMac**を使います。

# 完成のイメージを理解する
はじめにDockerの必要性を知ります。
ローカル（Mac）にRailsアプリがない、動く環境にない方は先に環境を整えます。

次にDockerを構成します。
具体的には、プロセスをapp（RailsやPumaなど）db（MySQL）nginx（Nginx）の3つに分け、Docker Composeで定義します。
ここでDockerを構成しながら、Dockerの理解を深めます。

最後にDockerを起動します。
起動がスムーズに行くことは稀で、何らかのエラーが発生します。
トラブルシューティングを活用しながら、起動を成功させます。

以上です。

# Dockerを導入する理由を知る
Dockerは、軽くてポータビリティのある開発環境ツールです。
Dockerは、モダンな企業のほとんどが使うツールです。
Dockerを、ポートフォリオに組み込むことで、転職先の企業とのマッチング率を高めるという狙いがあります。

さてDockerの動作については、こちらの記事が分かりやすいので、一読します。
[いまさらだけどDockerに入門したので分かりやすくまとめてみた](https://qiita.com/gold-kou/items/44860fbda1a34a001fc1)

# RailsアプリをGitHubからクローンする
RailsアプリをローカルであるMacにクローンします。
**※ すでにローカルでRailsアプリを開発している方は飛ばしてください。**

```shell:local（自分のMacターミナル）
$ vi ~/.gitconfig
```

```config:.gitconfig
[user]
  name = Gitに登録している名前
  email = Gitに登録しているメールアドレス

[url "github:"]
  InsteadOf = https://github.com/
  InsteadOf = git@github.com:
```

```shell:local
$ cd ~/.ssh
$ ssh-keygen -t rsa
Enter file in which to save the key ():git_rsa
Enter passphrase (empty for no passphrase): 
# 何もせずエンター
Enter same passphrase again: 
# 何もせずエンター
$ vi config
```

```config:.ssh/config
# 以下を追加
Host github
  Hostname github.com
  User git
  IdentityFile ~/.ssh/git_rsa
```

```shell:local
$ cat git_rsa.pub
# 中身をコピー
```

[GitHub](https://github.com/)にログイン
右上アイコン『Settings』 → 『SSH and GPG keys』 → 『New SSH key』

```
Title：任意
Key：コピーした公開鍵をペースト
```

```shell:local
$ cd ~
$ git clone -b ブランチ名 git@
github.com:GitHubのユーザー名/アプリ名.git
```

以上でRailsアプリのクローンは完了です。

参考になりました↓
[リモートから特定のブランチを指定してcloneする](https://qiita.com/icoxfog417/items/5776e0f0f758f0f0e48a)

# ローカルの開発環境を整える
ローカル（Mac）の開発環境を整えます。
ここでの手順は以下の通りです。
**※ すでにローカルでRailsアプリを開発している方は飛ばしてください。**

* Homebrewをインストールする
* Rubyをインストールする
* Bundlerをインストールする
* MySQLをインストールする

##  Homebrewをインストールする
[Homebrew（公式）](https://brew.sh/index_ja.html)をインストールします。
HomebrewはMacでよく利用されるパッケージ管理です。

```shell:local（Mac）
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
```

## Rubyをインストールする
Rubyをインストールします。
RubyはMacに標準で入っていますが、バージョン管理のためにrbenvを使います。

```shell:local（Mac）
$ brew install rbenv ruby-build
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source .bash_profile
$ rbenv install 使いたいバージョン
$ rbenv global 使いたいバージョン
$ rbenv rehash
```

## Bundlerをインストールする
Bundlerをインストールします。

```
$ gem install bundler
```

## MySQLをインストールする
MySQLをインストールします。

```
$ brew install mysql
```

参考になりました↓
[Ruby初学者のRuby On Rails 環境構築【Mac】](https://qiita.com/TAByasu/items/47c6cfbeeafad39eda07)

# Dockerをインストールする
[Docker Desktop for Mac](https://hub.docker.com/editions/community/docker-ce-desktop-mac)からアカウントを作成し、インストールします。
手順は以下の通りです。

1. 『Get Started』からDcokerHubアカウントを作成する
2. 『Get Docker』からDockerをインストールする

参考になりました↓
[DockerをMacにインストールする](https://qiita.com/kurkuru/items/127fa99ef5b2f0288b81)

# Dockerの仕組みを理解する
それではDockerを使って、環境を構築します。
仕組みとしては、app（RailsとPuma）db（MySQL）nginx（Nginx）のイメージをDockerfileなどで用意し、Docker Composeでプロセスを起動させます。

早速、以下のようにホスト側のディレクトリを構成します。
Dockerの各ファイル、用語は後述します。

```
lantern_app
├── config
│   ├── database.yml（既存）
│   └── puma.rb（既存）
├── containers
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.yml
├── Dockerfile
├── environments
│   └── db.env
├── Gemfile（既存）
├── Gemfile.lock（既存）
└── 他
```

用語を説明します。

**Image**
色々な環境を提供してくれる入れ物のことです。
イメージからRubyやNginxなどを取得します。

**Dockerfile**
イメージを作るためのファイルです。
イメージを元に機能を拡張するなどの用途があります。

**Volume**
データ永続のための保存場所です。
Dockerのデータはコンテナの終了とともに消えるため、必要に応じて設定します。

**Container**
イメージを元に作られたプロセスのことです。
イメージを起動すると、環境がプロセスとしてコンテナに隔離されます。

**Docker Compose**
複数のコンテナを定義・実行するツールです。

# Dockerのファイルを作成する
それではDockerのディレクトリとファイルを作成します。

```shell:local（Mac）
$ touch Dockerfile docker-compose.yml
$ mkdir containers containers/nginx environments
$ cd containers/nginx
$ touch Dockerfile nginx.conf
$ cd ../..
$ touch environments/db.env
```

各ファイルを編集します。
各ファイルの詳細は、後述します。

```dockerfile:Dockerfile
# 元にするイメージ
FROM ruby:2.6.3
# コンテナを機能させるまでの準備のコマンドを実行する
RUN apt update -qq && \
    apt install -y build-essential nodejs
RUN mkdir /lantern_docker
# 環境変数を設定する
ENV APP_ROOT /lantern_docker
# コマンドを実行するディレクトリを設定する
WORKDIR $APP_ROOT
# ホストのファイルをコンテナにコピーする
ADD Gemfile $APP_ROOT/Gemfile
ADD Gemfile.lock $APP_ROOT/Gemfile.lock
# 既出
RUN gem install bundler
RUN bundle install
ADD . $APP_ROOT
RUN mkdir -p tmp/sockets
# コンテナ起動時のポートを設定する
EXPOSE 3000
```

RailsアプリはRubyイメージを元にして作成します。
RUN apt ...でRailsが動く環境をインストールし、ADD . $APP_ROOTでGemfileなどをコンテナにコピーし、RailsやPumaなどを動作させます。

```yml:docker-compose.yml
# Docker Composeのバージョン
version: '3'

# Docker Composeではコンテナをサービスとして扱う
services:
  # サービス名をappとして定義
  app:
    # 参照するDockerfileを指定（docker-compose.ymlから基準）
    build: .
    # 環境変数が設定されているファイルを指定
    env_file:
      - ./environments/db.env
    # ボリュームをマウント（後述）
    volumes:
      - .:/lantern_docker
    # サービス名dbが作成されたらappを作成
    depends_on:
      - db
    # コンテナを起動させ続ける際に使用
    tty: true
    # コマンドを実行
    command: bundle exec puma -C config/puma.rb

  db:
    # イメージを指定（後述）
    image: mysql
    env_file:
      - ./environments/db.env
    # ボリュームをマウント（後述）
    volumes:
      - db-data:/var/lib/mysql

  nginx:
    build: containers/nginx
    # ホスト:コンテナのポートを指定
    ports:
      - "80:80"
    depends_on:
      - app

# ボリュームとして扱うボリューム名
volumes:
  db-data:
```

Docker Composeの各サービスは、DockerfileまたはDockerHubのイメージを参照します。
ここでは、appとnginxがDockerfile、dbがイメージを参照しています。

appのボリュームは、ホスト側のホームディレクトリ全てをボリューム化しています。
dbのボリュームは、コンテナ側のディレクトリ/var/lib/mysqlをdb-dataというボリューム名でボリュームをマウントしています。

この辺りはややこしく、参考記事を読むことをおすすめします。

参考になりました↓
[Docker、ボリューム(Volume)について真面目に調べた](https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32)
[Dockerの-vや--volumeオプションはわかりづらいから、--mountを使おう](https://qiita.com/supaiku2452/items/5d6e78d10094f64d269f)
[バインドマウント | Docker docs（日本語）](https://matsuand.github.io/docs.docker.jp.onthefly/storage/bind-mounts/)
[Pumaの起動におけるpumaコマンドとpumactlコマンドの違い](https://qiita.com/himatani/items/33cbbe84815534f8b479)


```dockerfile:containers/nginx/Dockerfile
FROM nginx
RUN rm -f /etc/nginx/conf.d/*
ADD nginx.conf /etc/nginx/conf.d/lantern_docker.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

Nginxはフォアグラウンドでの動作を期待しているので、'deamon off;'とします。
フォアグラウンドとバックグラウンドの違いは、PCで表すとこんな感じです。

* フォアグラウンド：一番上のウインドウ
* バックグラウンド：それ以外のウインドウ

参考になりました↓
[フォアグラウンド(foreground)とは | 「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典](https://wa3.i-3-i.info/word12543.html)

```nginx:containers/nginx/nginx.conf
# Pumaとやり取りするための通信路
upstream lantern_docker {
  server unix:///lantern_docker/tmp/sockets/puma.sock;
}
# サーバーに関する情報
server {
  # ポート
  listen 80;
  # 処理するサーバー名
  server_name localhost;
  # ログに関するファイルの場所
  access_log /var/log/nginx/access_log;
  error_log /var/log/nginx/error_log;
  # ドキュメントルートの設定
  root /lantern_docker/public;
  try_files $uri/index.html $uri @app;

  # 内部リダイレクトの処理
  location @app {
    # バックエンドサーバーに送信するヘッダーを定義し直す
    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;
    # プロキシのパス（Pumaに渡す）
    proxy_pass http://lantern_docker;
    proxy_redirect off;
  }
}
```

アプリ名を自分のものに変更します。
Nginxの設定は[#18](https://qiita.com/aokyo17/items/281b5a1658ea58bc7b24)で説明済みなので、省略します。

```env:environments/db.env
MYSQL_USER=ユーザー名
MYSQL_ROOT_PASSWORD=パスワード
MYSQL_HOST=エンドポイント
```

環境変数の設定です。
エンドポイントが分からない場合、[#17.5](https://qiita.com/aokyo17/items/36c8231798bd2d46690c)をご覧ください。

```yml:config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  timeout: 5000
  reconnect: false
  pool: 5
  socket: /var/lib/mysql/mysql.sock
  username: <%= Rails.application.credentials.mysql[:user_name] %>
  password: <%= Rails.application.credentials.mysql[:password] %>
  host: <%= Rails.application.credentials.mysql[:host] %>

development:
  <<: *default
  database: lantern_development

test:
  <<: *default
  database: lantern_test

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

MySQLの設定です。
credentailsが未設定の場合は、[#18.5](https://qiita.com/aokyo17/items/98e8e48d309a506db4e8)をご覧ください。

```rb:config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  ActiveRecord::Base.establish_connection
end

plugin :tmp_restart

app_dir = File.expand_path("../..", __FILE__)
bind "unix://#{app_dir}/tmp/sockets/puma.sock"
pidfile "#{app_dir}/tmp/pids/puma.pid"
stdout_redirect "#{app_dir}/log/puma.stdout.log", "#{app_dir}/log/puma.stderr.log", true
```

Pumaの設定です。
ここも[#18](https://qiita.com/aokyo17/items/281b5a1658ea58bc7b24)で説明済みなので、省略します。

以上で、Dockerのファイル編集は完了です。

# Dockerを起動する
Docker Composeを使ってDockerコンテナを起動します。

```shell:local（Mac）
# Dockerfileからイメージをビルド
$ docker-compose build
# Docker Composeのコンテナを起動（-dでバックグラウンド起動）
$ docker-compose up -d
# Docker Composeのコンテナのプロセスを確認
$ docker-compose ps
```

以上でDockerの組み込みは完了です。
後は各コンテナに入り、開発を進めてください。

```shell:shell
# appのbashに入る
$ docker-compose exec app bash
# 例
$ rails db:migrate
```

参考になりました↓
[Docker + Rails + Puma + Nginx + MySQL](https://qiita.com/eighty8/items/0288ab9c127ddb683315)
[丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)](https://qiita.com/azul915/items/5b7063cbc80192343fc0)
[既存のRailsアプリにDockerを導入する手順](https://qiita.com/kenzoukenzou104809/items/b9e716204e0cd0cea447)
[Nginx + Rails (Puma) on Docker のいくつかの実用パターン](https://qiita.com/na-o-ys/items/1a863419e1f6c3063ace)
[Docker + Rails + Puma + Nginx + Postgres](https://itnext.io/docker-rails-puma-nginx-postgres-999cd8866b18)
[Docker ComposeでNginx Rails MySQL環境を構築してみる](https://qiita.com/SinceK13/items/6b7f2dc1b9dd76699103)

# トラブルシューティング
Dockerで起動する際、様々なエラーが発生します。
一例ですが、私が見舞ったトラブルの解決策を書き残します。

## よく使うスクリプトを確認する
先によく使うスクリプトを紹介します。
まずはDockerやDocker Composeのコマンドを紹介します。

```shell:shell
# ログの確認
$ docker-compose logs
# サービスの確認
$ docker-compose ps -a
# コンテナの確認
$ docker ps -a
# コンテナとネットワークの停止と削除
$ docker-compose down --rmi all
# コンテナの削除
$ docker rm ID名
# イメージの削除
$ docker rmi ID名
# コンテナのbash内に入る
$ docker-compose exec サービス名 bash
```

続いてbashを紹介します。

```shell:bash
# ディレクトリの有無を確認する（出力0＝有、出力1＝無）
$ test -f パス;echo $?
# プロセスが使用しているポートを確認する
$ lsof -i
# ポートを指定してプロセスを確認する
$ lsof -i:ポート番号
```

以上です。
以降から、具体的なトラブルシューティングに入ります。

参考になりました↓
[Docker実践〜dockerのコンテナ環境をきれいに消す](http://sagantaf.hatenablog.com/entry/2018/09/09/003826)
[docker-compose up したコンテナを起動させ続ける方法](https://qiita.com/sekitaka_1214/items/2af73d5dc56c6af8a167)
[Linux/UNIXでファイル・ディレクトリの存在確認をする](https://orebibou.com/2016/07/linuxunix%E3%81%A7%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%BB%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AE%E5%AD%98%E5%9C%A8%E7%A2%BA%E8%AA%8D%E3%82%92%E3%81%99%E3%82%8B/)
[Linuxでプロセスが何のポート使っているかを調べる](https://qiita.com/sonoshou/items/cc2b740147ba1b8da1f3)

## /usr/local/lib/ruby/2.6.0/rubygems.rb:283:in `find_spec_for_exe': Could not find 'bundler' (2.1.4) (Gem::GemNotFoundException)

Bundlerが見当たらないのでエラーが発生します。
GemからBundlerをインストールします。

```dockerfile:Dockerfile
# 以下を追加
RUN gem install bundler
```

参考になりました↓
[Docker + Rails のdocker-compose build でGemNotFoundExceptionの時の対処](https://qiita.com/hashimoto-atsunori/items/904aa696040014fd26e3)

## mysql client is missing.
MacにMySQLがないためにエラーが発生します。
HomebrewでMySQLをインストールします。

```shell:local（Mac）
$ brew install mysql
```

参考になりました↓
[mysql2 が原因でbundle installにてエラーを吐く(Mac OS X)](https://qiita.com/shakemurasan/items/b6b733bf9052c38bb83e)

## uses an image, skipping
こちらはエラーではありません。
すでにビルドが済んだイメージをリモートで取得するので、upすれば問題ありません。

```shell:local（Mac）
$ docker-compose up -d
```

参考になりました↓
[docker-compose.yml起動時にuses an image, skippingされる現象](https://teratail.com/questions/131280)
[docker-compose \`up\` とか \`build\` とか \`start\` とかの違いを理解できていなかったのでまとめてみた。](https://qiita.com/tegnike/items/bcdcee0320e11a928d46)

## error: database is uninitialized and password option is not specified
データベースのエラーです。
データベースが初期化されていない、パスワードがないため発生します。
環境変数のあたりを確認します。

```env:environments/db.env
MYSQL_USER=ユーザー名
MYSQL_ROOT_PASSWORD=パスワード
MYSQL_HOST=エンドポイント
```

```docker-compose.yml
# 中略
  db:
    image: mysql
    env_file:
      - ./environments/db.env
# 中略
```

参考になりました↓
[docker, docker-composeでmysqlが起動しない](https://qiita.com/takepan/items/0cbf13af3a0bb2c243ab)
[docker-compose.ymlで.envファイルに定義した環境変数を使う](https://kitigai.hatenablog.com/entry/2019/05/08/003000)
[How to link MySQL RDS in docker-compose.yml file?](https://stackoverflow.com/questions/41889684/how-to-link-mysql-rds-in-docker-compose-yml-file)

## NoMethodError: Cannot load database configuration:
データベースのエラーです。
データベースに関する設定に問題があります。

### 解決策1：ttyをtrueにする
ttyをtrueにすると解決するかもしれません。

```yml:docker-compose.yml
app:
# 中略
    tty: true
# 中略
```

参考になりました↓
[docker-compose up したコンテナを起動させ続ける方法](https://qiita.com/sekitaka_1214/items/2af73d5dc56c6af8a167)
[docker-composで起動したコンテナがすぐに停止する](https://teratail.com/questions/154778)

### 解決策2：database.ymlのERbを削除する
コメントアウトしていてもERbは評価されます（恐らく）。
database.yml内のERbは削除します。

```config/database.yml
# 以下で書かれた必要ない箇所を削除する
<%= %>
```

参考になりました↓
[Rails を起動したら Cannot load \`Rails.application.database_configuration\`: (NoMethodError) が出てハマった](https://obel.hatenablog.jp/entry/20180526/1527321948)

## 502 Bad Gateway
何らかのサーバーに関するエラーです。
私の場合は、以下のように変更すると解決しました。

```nginx:nginx.conf
upstream lantern_docker {
  # ソケットからサービス名に変更
  # server unix:///lantern_docker/tmp/sockets/puma.sock;
  server app:3000;
}
# 中略
```

参考になりました↓
[Docker Compose と nginx でリバースプロキシを作ろうとしたお話（出題編）](https://riina-k.net/programming/docker-compose-nginx-reverse-proxy/)

---
前回：[#18 EC2環境構築, Nginx+Puma+Capistrano編](https://qiita.com/aokyo17/items/281b5a1658ea58bc7b24)
番外：[#18.5 環境変数, Gmail送信設定編](https://qiita.com/aokyo17/items/98e8e48d309a506db4e8)
次回：準備中