Dockerを使ってRailsの開発環境をまるっと構築する


## 1. はじめに
本記事は、Dockerを使ってNginx + Unicorn + Rails + Redis + MongoDBの構成を作成する場合についてまとめました。
僕が上の構成を作る際にたどった道のりをささっと書いていこうと思います。

内容は

- Docker-Machineを使った環境構築
- Dockerコマンドを使ってコンテナ作成
- Docker-Composeを使って複数のコンテナを一括立ち上げ

についてです。
読者の方が、システムのDocker化する際に参考にしていただけるとありがたいです。僕も勉強中の身ですので、間違いがあれば指摘して頂けるととてもとてもとてもありがたいです:):)

## 2. DockerをMac上で使えるようにする
OS X上で使えるようになるために、検索を行うとまだboot2dockerの記事が出てきたりします。しかし、現在はDocker-Toolboxを導入する方が一般的です。
https://www.docker.com/docker-toolbox

Dockerを使って遊ぶために必要なコマンド郡が一括で入るので、とても楽チンです。

## 3. Docker-Machineでdockerコマンドを使える仮想環境を準備
たった1つのコマンドを実行すれば、virtualbox上にdockerを使う環境が整います。

```
$ docker-machine create --driver virtualbox default
```

defaultの部分はVMの名前なので、環境に合わせて変更可能です。
立ち上がったらdefaultの情報を確認してみます

```
$ docker-machine env default
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="/Users/gryotaro/.docker/machine/machines/default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval "$(docker-machine env default)"
```

OS Xにdefaultの情報を認識させるために以下のコマンドを叩きます。

```
$ eval "$(docker-machine env default)"
```

> ![](https://docs.docker.com/engine/installation/images/mac_docker_host.svg)
> reference https://docs.docker.com/engine/installation/mac/ 

現状の構造はこのようになっています。
OS XがLinux VM(default)を認識して、OS X上のターミナルでdockerに対して行った操作がdaemonへ伝達されます。
現状は、コンテナはまだひとつもない状態です。

余談なのですが、2回目以降は特にVMを作成する必要はないので、

```
$ docker-machine start default
$ eval "$(docker-machine env default)"
```

で上記の状態を作れます。僕は毎回evalするのが面倒なので、.zshrcへもう書き込んじゃってます。

## 4. Dockerコマンドを使ってみる
Dockerコマンドを使ってみます。
nginxを作ってWebサーバーのコンテナを立ち上げるサンプルです。

イメージとかコンテナとか説明なしに言っているのですが、
イメージ: インフラのスナップショット
コンテナ: 動作している状態のインフラ
です。

オブジェクト指向を例にすると、
イメージ => クラス
コンテナ => インスタンス
みたいな感じです。

自作のイメージを作ると時間がかかります。
世界中の有志の方がすでに作成したイメージを[DockerHub](https://hub.docker.com/)に挙げてくださっているのでそれを利用します。

```sh:
$ docker run -d -p 80:80 nginx
```

192.168.99.100へアクセスしてみると(IPは環境によって変わります)

![スクリーンショット 2016-01-03 20.55.17.png](https://qiita-image-store.s3.amazonaws.com/0/74197/b6396464-0fbb-c161-8dcb-4689b06071de.png "スクリーンショット 2016-01-03 20.55.17.png")

nginxが動作しているのがわかります。
-dはデーモン起動
-p 80:80はDOCKER_HOSTのポート80番とコンテナのポート80番をつなぎますよという意味です。(図を参照)

dockerコマンドのオプションはたくさんあるので、ググりましょう。

## 5. コンテナの設定をファイルに書き込み再現を可能にする
Chef等を触ったことがある人はわかるかなと思うのですが、recipeのようなものです。
Dockerfileを見ればコンテナがどのような状態なのかわかります。

どのイメージを使うか
どのポートを開けるか
ファイルをコピーしてコンテナに渡したり
apt-getやyum installしたり
環境変数の設定をしたりが出来ます。

記述できる命令は以下のようになっています。
[こちら](http://docs.docker.jp/engine/reference/builder.html)のリファレンスを参照してください

例えばnginxの例だと

```Dockerfile
FROM nginx
COPY ./nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]
```

ここで注意が必要なのですが、それぞれの命令は独立して実行されるので、RUN cd /etcを実行しても次の命令には影響を与えません。

ちなみにnginx.confはこのような感じ

```
events {
   worker_connections 512;
}

http {

  server {
    listen       80;
    server_name sample.com;

    root /usr/share/nginx/html;
    index index.html index.htm;
    location / {
      try_files $uri $uri/ /index.html;
    }
  }

}
```

作成したDockerfileを元にイメージを作成します。

```
Dockerfileがあるディレクトリで
$ docker build -t sample-image-nginx:1.0 .
```

sample-image-nginxというイメージを1.0というタグで作成しました。

先ほどのイメージからコンテナを作成します。

```
$ docker run -p 80:80 --name nginx-sample01 sample-image-nginx:1.0
```

これでnginxが起動してブラウザから確認できると思います。
IPアドレスでも確認できますし、sample.comでも確認できると思います(Mac側の/etc/hostsへIPとドメイン名を追加しておく必要があります)。

ちょっと例としては短くなってしまったのですが、雰囲気は伝わったかなと...^^

ちなみにDockerfileのベスト・プラクティスは[こちら](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)


## 6. コンテナ同士の関係を考慮して一気に環境を立ち上げる(docker-compose, docker-compose.yml)
たくさんのコンテナを立ち上げたい + コンテナ間で依存関係がある場合上記の手法だと面倒です。

そんな時にはDocker-Compose

docker-compose.ymlという設定ファイルを作成して、それぞれのコンテナの設定をDockerfileとして読み込みます。

```
docker-compose.ymlを設置したディレクトリで
$ docker-compose build
$ docker-compose up
```

これで設定ファイルに記述したコンテナの依存関係を解釈して、一気に立ち上げを行ってくれます。

docker-compose.ymlの例は
[リファレンス](http://docs.docker.jp/compose/rails.html)を参照してくださいm(_ _)m

## 7. Railsの開発環境を構築する
いよいよ本題です。
Nginx + Unicorn + Rails + Redis + MongoDB
最終的なフォルダの構成は以下のようになります。
時間がもったいないよ!!という人は、さっとチェックしてみてください。

```
app01
├── app
├── config
├── Dockerfile
├── containers
│   ├── datastore
│   │   └── Dockerfile
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
└── docker-compose.yml

# rails new app01によって作成したアプリのroot上で、dockerに関係ある部分のみ記述しています。
```

大切な部分は、2つかなと思います。

- DB、Redisの情報をunicornコンテナが知る方法
- DBのデータをコンテナに置かない方法(データの永続化)

作成の際の方針は、
サービスごとにコンテナを作成すること
docker-compose.ymlには各コンテナの関係に関わるものを記述すること(commandの実行もできてしまうので、そこはDockerfileへ任せる感じで)

今回使った設定ファイル達です。

```yaml:docker-compose.yml
nginx:
  build: ./containers/nginx/
  ports:
    - "80:80"
  volumes_from:
    - datastore
  links:
    - rails

rails:
  build: .
  volumes:
    - .:/app01
  links:
    - mongodb:mongodb
    - redis:redis
  environment:
    - RAILS_ENV=development
    - REDIS_URL=redis://redis:6379
  volumes_from:
    - datastore

mongodb:
  image: mongo:2.6.11
  volumes_from:
    - datastore
  ports:
    - "27017:27017"
  command: --smallfiles

redis:
  image: redis:2.8.13
  ports:
    - "6379:6379"

datastore:
  build: containers/datastore
```

composeファイルは結局はdockerコマンドの実行をいい感じにしてくれているだけなので、できることはdockerコマンドと同じです。

登場するコンテナは5つ
nginx、rails、mongodb、redis、datastore
です。

```app01/Dockerfile
FROM ruby:2.1.2
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev apt-utils
# 追加
RUN apt-get install -qq -y nodejs
RUN mkdir /app01
WORKDIR /app01
VOLUME /share
COPY ./Gemfile /app01/Gemfile
COPY ./Gemfile.lock /app01/Gemfile.lock
RUN bundle install
COPY . /app01
CMD ["bundle", "exec", "unicorn", "-c", "config/unicorn.rb"]
```

nginx関連の設定

```app01/containers/nginx/Dockerfile
FROM nginx:1.9.8
COPY nginx.conf /etc/nginx/nginx.conf
VOLUME /var/log/nginx/log
VOLUME /usr/share/nginx/html
VOLUME /share
```

```app01/containers/nginx/nginx.conf
events {
  worker_connections 512;
}

http {
  upstream uni_app {
    server unix:/share/unicorn.sock fail_timeout=0;
  }

  server {
    listen       80;
    root /app01/public;

    access_log /var/log/nginx/sample_access.log;
    error_log /var/log/nginx/sample_error.log;

    try_files $uri/index.html $uri @uni_app;

    fastcgi_read_timeout 120;

    location @uni_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_pass http://uni_app;
    }
  }

}
```

データ永続化のためのデータストアコンテナ設定

```app01/containers/datastore/Dockerfile
FROM busybox:latest

VOLUME /data
VOLUME /share
VOLUME /var/log/
VOLUME /app01
COPY . /app01

CMD /bin/sh
```

### コンテナ間の情報共有

後は、railsアプリ側の設定です。
redis、mongoをunicornのコンテナがどのように認識するかです。
具体的にはIPアドレス等ですね。

railsがredisを使用する際に、config/environments/development.rbにredisの接続先を指定したり、config/mongoid.ymlにmongodbの接続先を指定したりする必要がありますよね

詳しくは[こちら](https://docs.docker.com/compose/env/)に書いているのですが、linksを設定した時に参照先のコンテナでは以下のルールで環境変数が設定されます。
nameというのはコンテナの名前です。
*)例の場合、DBというコンテナ名になっています。

```
name_PORT
プロトコルも含む完全なURLを返します。DB_PORT=tcp://172.17.0.5:5432

name_PORT_num_protocol
こちらも完全なURLを返します。DB_PORT_5432_TCP=tcp://172.17.0.5:5432

name_PORT_num_protocol_ADDR
コンテナのIPアドレスを返します。 DB_PORT_5432_TCP_ADDR=172.17.0.5

name_PORT_num_protocol_PORT
開いてるポートを返します。 e.g. DB_PORT_5432_TCP_PORT=5432

name_PORT_num_protocol_PROTO
プロトコルを返します。 DB_PORT_5432_TCP_PROTO=tcp

name_NAME
コンテナのフルネームを返します。 DB_1_NAME=/myapp_web_1/myapp_db_1
```

全部書いてみたのですが、僕が実際に使ったのは name_PORT_num_protocol_ADDR
name_PORT_num_protocol_PORT
の2つです。

(プロトコルを含んだものは、速度的な課題に影響してくるのかな??なんて思ったりします) => わかる方はぜひアドバイスを下さい!!m(_ _)m

今回の場合は

```config/environments/development.rb
config.cache_store = :redis_store, "redis://#{ENV['REDIS_PORT_6379_TCP_ADDR']}:6379/0/cache", {expires_in: 30.day}
```

```config/mongoid.yml
development: &default
  sessions:
    default:
      database: development
      hosts:
        - <%= ENV['MONGODB_PORT_27017_TCP_ADDR'] %>:27017
      options:
  options:
```

のような感じにしました。

### データの永続化
コンテナは基本使い捨てです。
じゃあデータベースのデータはどうなるのでしょうか?

ここでデータを永続化するためのコンテナが必要になります。
datastoreというコンテナがそれです。

busyboxという極小のOSイメージ(3MBだそうです)上で、マウントしたディレクトリを他のコンテナで使用します。

mongodbのデータは/data以下に保存されるため、volumes_fromでdatastoreを参照しています。

nginx、unicornコンテナでもdatastoreを参照しているのですが、それはnginxとunicornの間でunix_domain_socketを利用しているからです。
その理由は[こちら](http://qiita.com/ma2shita/items/6f1905847b2059f7cf7d)

## 8. まとめ
Dockerに触れるようになって、ポートを意識することが多くなりました。
環境を作る際にも、ポートを開けずに「つながらねえ」となる部分があったり、volumeのマウントを忘れていたりすることで動かないということがあったので、動作が変な時は見なおしてみるといいかもしれないです。
何事もそうだと思うのですが、基礎って大切ですね。TCP/IPも復習しないといけないです。
今後はホストOSを跨いだdocker環境構築やデプロイに関して調べていこうと思います。

## 9. 参考サイト・文献
http://docs.docker.jp/
Docker実践入門
プログラマのためのDocker教科書