Rails
MySQL
nginx
docker
docker-compose

なるべく自動化してNginx, Rails, MySQLの開発環境をDockerで作った

前置き

いつでも簡単にRailsの開発環境を構築したかったのでDockerで開発環境を構築することにしました。公式の手順やご参考にさせて頂いた記事を基に少し自動化してみることにしました。筆者のDockerの知識は入門書を一読した程度なので学習も兼ねています。なお、個々のコマンドの説明などは既に素晴らしい記事がたくさんありますので作成したファイルにコメントを振ることをメインにします。
そのまま使えるはずの状態でGitHubにも公開しました。興味ありましたら使ってください:smiley:

動作環境

   
OS MacOS High Sierra 10.13.6
Docker Docker for Mac CE 18.06.1-ce-mac73 (26764)

構成

GitHubに公開しています。併せてご覧ください。
Docker周りだけを管理したいディレクトリと永続化だけを管理したいディレクトリに分けました。
以降、粛々とファイルとその説明が続きます。「とにかくGitHubのコードを動かしたい」方はこちらまで飛んでください:airplane_departure::airplane_departure::airplane_departure:

$ tree
├── README.md
├── docker                              # DockerやDocker Compose
│   ├── containers                       # 各コンテナ(イメージ)
│   │   ├── mysql                         # MySQL 5.7
│   │   │   ├── Dockerfile                 # MySQL 5.7のDockerファイル
│   │   │   ├── grant_user.sql             # 初期セットアップでユーザを作成するスプリプト
│   │   │   └── my.cnf                     # 初期セットアップで反映するmy.cnf
│   │   ├── nginx                         # Nginx 1.15.2
│   │   │   ├── Dockerfile                 # NginxのDockerファイル
│   │   │   └── nginx.conf                 # 初期セットアップで反映するnginx.conf
│   │   └── rails                         # Rails ~>5.2.0
│   │       ├── Dockerfile                 # Dockerファイル
│   │       ├── Gemfile                    # 初期セットアップで反映するGemfile
│   │       ├── application.rb             # 初期セットアップで反映するapplication.rb
│   │       ├── database.yml               # 初期セットアップで反映するdatabase.yml
│   │       ├── docker-entrypoint.sh       # コンテナ起動時動作するシェルスクリプト
│   │       └── puma.rb                    # 初期セットアップで反映するpuma.rb
│   ├── docker-compose.yml               # Composeファイル
│   └── environments                     # 環境変数定義
│       ├── common.env                    # 各コンテナ共通
│       └── db.env                        # DB接続用MySQL関連
└── volumes                             # 永続化したリソース
    ├── app                              # Railsのソースコード このディレクトリは最初は無し
    ├── db                               # MySQLのデータ このディレクトリは最初は無し
    ├── ssl                              # SSL証明書
    │   ├── privkey.pem                   # 秘密鍵
    │   └── server.crt                    # サーバー証明証
    └── web                              # Nginxのログ このディレクトリは最初は無し

Composeファイル

docker/docker-compose.yml
version: '3'                                     # 特に意思は無いが新しいバージョンで。
services:                                        # 各コンテナ(サービス)
  db:                                             # MySQLのコンテナ 「db」と命名
    build: containers/mysql                        # Dockerファイルのパス
    env_file:                                      # 環境変数
      - ./environments/common.env                   # 各コンテナ共通
      - ./environments/db.env                       # DB接続用MySQL関連
    volumes:                                       # 永続化
      - ../volumes/db/data:/var/lib/mysql           # MySQLのデータ
  app:                                            # Railsのコンテナ 「app」と命名
    build: containers/rails                        # Dockerファイルのパス
    env_file:                                      # 環境変数
      - ./environments/common.env                   # 各コンテナ共通
      - ./environments/db.env                       # DB接続用MySQL関連
    command: bundle exec puma -C config/puma.rb    # 実行するコマンド
    volumes:                                       # 永続化
      - ../volumes/app:/app                         # Railsのソースコード
    depends_on:                                    # 起動する順番
      - db                                          # 「db」後で起動
  web:                                            # Nginxのコンテナ 「web」と命名
    build: containers/nginx                        # Dockerファイルのパス
    env_file:                                      # 環境変数
      - ./environments/common.env                   # 各コンテナ共通
    volumes:                                       # 永続化
      - ../volumes/app:/app                         # 静的ファイル
      - ../volumes/web/log:/var/log/nginx/          # Nginxのログ
      - ../volumes/ssl:/etc/nginx/cert/             # SSL証明書
    ports:                                         # 解放ポート
      - 443:443                                     # HTTPS この開発環境はHTTPSのみ想定
    depends_on:                                    # 起動する順番
      - app                                         # 「app」後で起動

db(MySQL)コンテナ

Dockerファイル

docker/containers/mysql/Dockerfile
# MySQL 5.7
FROM mysql:5.7

# 初期セットアップで利用するmy.cnfをイメージへコピー
# アーカイブを展開する必要などが無ければADDで無くてCOPYで良い。
COPY my.cnf /etc/mysql/conf.d

# 初期セットアップで実行したいスクリプトをイメージへコピー
COPY grant_user.sql /docker-entrypoint-initdb.d

コピーするファイルの概要

ホスト イメージ 概要
docker/containers/mysql/my.cnf /etc/mysql/conf.d/ 文字コード
docker/containers/mysql/grant_user.sql /docker-entrypoint-initdb.d/ Railsアプリ向けDBアカウント

Docker Comopseから設定される環境変数

docker/environments/common.env
TZ=Asia/Tokyo
docker/environments/db.env
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=rails
MYSQL_PASSWORD=rails

Docker Comopseから設定される永続化

ホスト コンテナ 概要
volumes/db/data /var/lib/mysql MySQLのデータ

app(Rails)コンテナ

Dockerファイル

docker/containers/rails/Dockerfile
# 公式を参考
FROM ruby:2.5.1
RUN apt-get update -qq && apt-get install -y build-essential default-libmysqlclient-dev nodejs

# MySQLのイメージを参考にコンテナ起動時のエントリポイントを作成
# 元のスクリプトをコピー
COPY docker-entrypoint.sh /usr/local/bin/
# 権限付与
RUN chmod 744 /usr/local/bin/docker-entrypoint.sh

# 永続化されるパスへ直接ファイルをコピーしてしまうとマウント時にホスト側の内容で上書きされ
# イメージ構築でコピーされたファイルは全て消失してしまうので一時ディレクトリで作業
ENV READY_RAILS_DIR=/ready_rails
# ディレクトリ作成とワークディレクトリに指定
WORKDIR $READY_RAILS_DIR

# 初期セットアップで反映する各ファイルをコピー
COPY Gemfile .
COPY application.rb .
COPY database.yml .
COPY puma.rb .

# 永続化でマウントされるパスをワーキングディレクトリに指定
WORKDIR /app

ENTRYPOINT ["docker-entrypoint.sh"]

コピーするファイルの概要

ホスト イメージ 概要
docker/containers/rails/Gemfile /ready_rails/ gem ~> Rails 5.2.0
docker/containers/rails/application.rb /ready_rails/ タイムゾーンを設定したapplication.rb
docker/containers/rails/database.yml /ready_rails/ ユーザ/パスワードを設定したdatabase.yml
docker/containers/rails/puma.rb /ready_rails/ NginxとPuma間のUNIXドメインソケットを設定したpuma.rb
docker/containers/rails/docker-entrypoint.sh /usr/local/bin/ 後述

コンテナ起動時のENTRYPOINT

MySQL公式のENTRYPOINTを参考に自動化したシェルスクリプトを作成しました。

docker/containers/rails/docker-entrypoint.sh
#!/bin/bash

set -e

# ホストにあるRailsのソースコードが無く永続化でマウントされた場合
# /app/Gemfileは存在しないはず。
if [ ! -e Gemfile ]; then
  # 一時ディレクトリからGemfileをコピー
  cp -a $READY_RAILS_DIR/Gemfile .
  touch Gemfile.lock

  bundle install
  rails new . -d mysql -f

  # 初期セットアップのファイル群をコピー
  cp -a $READY_RAILS_DIR/database.yml \
    $READY_RAILS_DIR/puma.rb \
    $READY_RAILS_DIR/application.rb config/

  # NginxとPuma間をUNIXドメインソケットで通信するためのディレクトリを作成
  mkdir -p tmp/sockets

  # 初期セットアップのファイル群が/appにコピーされて
  # /appがホストにマウントされたので一時ディレクトリは削除
  rm -r $READY_RAILS_DIR

  # depends_on指定でdb(MySQL)コンテナが先に起動されている。
  # 順番に起動するものの先行のコンテナの準備が整うまで待機は行えない。
  # つまりDBの準備が完了していない状況があるので待機する。
  until rails db:drop &> /dev/null; do
    >&2 echo "MySQL is unavailable - sleeping"
    sleep 1
  done

  # DBの準備が整ってからdb:create
  rails db:create

# ホストにあるRailsのソースコードがあり永続化でマウントされた場合
# この場合/app/Gemfileは存在する一時ディレクトリは残っているはず。
elif [ -e $READY_RAILS_DIR ]; then
  bundle install
  rm -r $READY_RAILS_DIR
fi

exec "$@"

Docker Comopseから設定される環境変数

docker/environments/common.env
TZ=Asia/Tokyo
docker/environments/db.env
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=rails
MYSQL_PASSWORD=rails

Docker Comopseから設定される永続化

ホスト コンテナ 概要
volumes/app/ /app Railsのソースコード(Nginxと共用)

web(Nginx)コンテナ

Dockerファイル

docker/containers/nginx/Dockerfile
FROM nginx:1.15.2

# 初期セットアップで反映するファイルをコピー
COPY nginx.conf /etc/nginx/conf.d/app.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf

コピーするファイルの概要

ホスト イメージ 概要
docker/containers/nginx/nginx.conf /etc/nginx/conf.d/app.conf 後述

WEBサーバーの設定

トリッキーな設定は無いと思いますが簡単に補足します。
GitHubに公開しているものはSSL証明書については「*.example.com」の自己署名SSL証明書(所謂「オレオレ証明書」)を利用しています。筆者の環境ではLet's EncryptのSSL証明書を利用しています。

余談ですがssl on | offは廃止になったようです。

docker/containers/nginx/nginx.conf
# PumaのSocketを指定
upstream app {
  server unix:///app/tmp/sockets/puma.sock;
}

server {
  # 今回HTTPは使用しない。ssl onは廃止のよう・・・。
  listen 443 ssl;
  server_name devnokiyo.example.com;

  ssl_certificate     /etc/nginx/cert/server.crt;
  ssl_certificate_key /etc/nginx/cert/privkey.pem;
  ssl_prefer_server_ciphers on;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  # Railsの静的ファイルはNginxで返す。
  root /app/public;
  try_files $uri/index.html $uri @app;
  client_max_body_size 10m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;

  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;
    proxy_set_header X-CSRF-Token $http_x_csrf_token;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://app;
  }
}

Docker Comopseから設定される環境変数

docker/environments/common.env
TZ=Asia/Tokyo

Docker Comopseから設定される永続化

ホスト コンテナ 概要
volumes/app/ /app/ Nginxのドキュメントルートの/app/public
PumaのUNIXドメインソケット
volumes/web/log/ /var/log/nginx/ ログ
volumes/ssl/ /etc/nginx/cert/ SSL証明書

実行してみましょう!

Docker Composeで起動

だいぶ簡単でしたが各ファイルの説明が終わりましたので実行して確認します。

  1. GitHubから一式ダウンロードします。
  2. dockerディレクトリへ移動します。

    $ cd docker
    
  3. ビルドします。(初回なので起動でも構いません)

    $ docker-compose build --no-cache
    Building db
    Step 1/3 : FROM mysql:5.7
    ---> 563a026a1511
    Step 2/3 : COPY my.cnf /etc/mysql/conf.d
    ---> 691e66ddfe3c
        :
        :
    Successfully built 5f5831d39d90
    Successfully tagged docker_web:latest
    
  4. 起動します。

    $ docker-compose up
    Creating network "docker_default" with the default driver
    Creating docker_db_1 ... done
    Creating docker_app_1 ... done
    Creating docker_web_1 ... done
    Attaching to docker_db_1, docker_app_1, docker_web_1
    db_1   | Initializing database
    db_1   | 2018-09-09T12:32:32.270957Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
        :
        :
    db_1   | Version: '5.7.23'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
        :
        :
    app_1  | Fetching gem metadata from https://rubygems.org/..........
    app_1  | Fetching gem metadata from https://rubygems.org/.
    app_1  | Resolving dependencies...
    app_1  | Fetching rake 12.3.1
    app_1  | Installing rake 12.3.1
        :
        :
    app_1  | * bin/rake: spring inserted
    app_1  | * bin/rails: spring inserted
    app_1  | Created database 'app_development'
    app_1  | Created database 'app_test'
    app_1  | Puma starting in single mode...
    app_1  | * Version 3.12.0 (ruby 2.5.1-p57), codename: Llamas in Pajamas
    app_1  | * Min threads: 5, max threads: 5
    app_1  | * Environment: development
    app_1  | * Listening on unix:///app/tmp/sockets/puma.sock
    app_1  | Use Ctrl-C to stop
    

ブラウザからアクセス

hostsファイルにdevnokiyo.example.comをループバックIPで設定してhttps://devnokiyo.example.com/ にアクセスします。不正な証明書ということでエラーが出ると思いますが便宜上例外を許可してください。以下はChromeの例ですが表示自体はされています。
スクリーンショット 2018-09-09 21.58.48.png
スクリーンショット 2018-09-09 22.00.13.png

前述のとおり筆者専用環境ではLet's EncryptのSSL証明書を利用しています。ご参考までに正常な証明書扱いの画像も載せておきます。独自ドメインを所有しておりdevxxx.devnokiyo.comで取得した例になります。
スクリーンショット 2018-09-09 21.42.41.png

Scaffoldを作成

問題なく表示されたら取り敢えずscaffoldでDB周りを確認します。

$ docker-compose exec app rails g scaffold food name:string price:integer calorie:integer
Running via Spring preloader in process 55
      invoke  active_record
      create    db/migrate/20180909124600_create_foods.rb
      create    /models/food.rb
        :
        :
      invoke  scss
      create    /assets/stylesheets/scaffolds.scss

$ docker-compose exec app rails db:migrate
== 20180909124600 CreateFoods: migrating ======================================
-- create_table(:foods)
   -> 0.0234s
== 20180909124600 CreateFoods: migrated (0.0235s) =============================

CRUDで確認

https://devnokiyo.example.com/foods にアクセスします。
New/Edit/Show/Destroyを確認してみてください。
スクリーンショット 2018-09-09 22.02.59.png

終わりに

基礎的なコマンドの説明よりは「筆者はこうやった」的なことをメインに記事を書いてみました。Dockerの入門書やネットで情報だけ得て「分かった気」になっていましたが、実際に手を動かすと上手くいかないこともありました。やはり手を動かしてみると大きく理解が進みますね。しばらくこれをベースにRailsをやっていこうと思います。