開発環境をDockerに乗せる方法とメリットを3ステップで学ぶチュートリアル

## この記事の対象

- Dockerを使ったことがない。もしくは、触ってみたけどよくわからない
- Webアプリの開発中に「MySQLを起動しわすれていた」とか「nodeのバージョン違った」で悩まされている人

## 背景

Dockerの事例は増えてきたけど、なかなか手を出しづらい人も多いんじゃないだろうか。

個人的に、ここ数ヶ月でいろいろとDockerの構成を試しているので、それをふまえて**開発環境でのDockerの使い方を解説**しようというのがこの記事の目的。

- [Dockerでnginx＋node.jsのSPA構成を試す](http://qiita.com/KeitaMoromizato/items/bfc3e22dae47211eff4f)
- [React SSR＋WordPress REST APIをDocker Composeで試す](http://qiita.com/KeitaMoromizato/items/37a732831ed21a9ef318)
- [RailsのToDoアプリチュートリアル（on Docker）](http://qiita.com/KeitaMoromizato/items/bc7887b33cf637aa2310)

productionでのDocker活用となると、触る機会も限られてくるし、気軽に試せるものじゃない。そこで今回は、Dockerのポータビリティ（production/test環境でもそのまま動く）という面ではなく、**環境の同一性**という面に焦点を当て、開発環境で使ってみる。

Dockerは「デプロイする時のイメージはこうで〜」と最初から考えて始めると、学習コストは結構高い。でも開発環境で使うだけならセキュリティとかも気にしなくていいし、そういうところで慣れてから、本番環境での活用を考え始めたらいいじゃないか。

[Docker for Mac](https://docs.docker.com/engine/installation/mac/)、[Docker for Windows](https://docs.docker.com/engine/installation/windows/)の正式リリースでDocker環境の構築も楽になったし、仕事のプロジェクトでも、個人の開発で使うちょっとしたものでも、全部Docker上で開発してみるのも良いと思う。

## Docker（Docker Compose）を使うメリット

開発環境をDockerに乗せることのメリット。もちろん、本番環境でも使うようになったら、メリットはもっと増える。

### バージョン違いによるミスが起きない
複数人で開発をしていると、Rubyやnode.jsなどの実行環境のバージョン違いで動かなくなることがある。Docker上で動かすことで実行環境は固定されるので、そのようなミスが起きない。

### ミドルウェアの構成を含めて他の人にシェアできる
「MySQLを起動していなかった」「nginxを挟んでいなかったから動きが変わっていた」など、アプリケーションを開発しているとよくある問題。

Docker Composeという機能を使えば、そのようなミドルウェアの構成を含めてコード化できるので、コマンドひとつで同じ構成で立ち上がる。

そして、それはGitHub上で共有できる。例えばElasticsearchを組み込んでみたサンプルがあるとすると、それを誰もがcloneして、コマンドひとつで実行できる。アプリケーションのコードだけでなく、ミドルウェアをどう使うのかというTipsも簡単に共有できるようになる。

## そもそもDockerとは？

**Dockerfile**という設定ファイルに書かれた内容を元に、仮想環境のイメージ（**Dockerイメージ**）を作れる仕組み。そして、そのイメージを元に仮想環境上にコンテナという、ひとつのサーバーのようなものを起動できる。

そしてDockerイメージは**DockerHub**というサイトで公開もできる。DockerHubにはRubyやnode.jsといった基本的なイメージだけでなく、Wordpressのイメージも公開されていたりする。つまり、イメージをpullしてきて`docker run`を実行するだけで、仮想環境上にWordpressを立ち上げることが可能。

そして、その公開されているDockerイメージを拡張して、独自のイメージを作ることが可能。例えば、Rubyのイメージを拡張してRuby on Railsを動かすためのイメージを作ったりする。

MacでもLinuxでもWindowsでも、同じ**Dockerfile**から起動したコンテナは同一の環境になるので、環境差異が発生しにくいことも特徴。

## そしてDocker Composeとは？

Dockerは、**1コンテナ1プロセス**という思想が基本。1つのコンテナにすべての機能を詰め込まず、複数のコンテナを起動して、協調してシステムを構成しましょうと。

例えば、リバースプロキシのnginx、バックエンドのアプリケーション、データベースをそれぞれ別のコンテナで起動して、システムを構築する。それをYAMLの設定ファイルひとつで簡単に実現するのが**Docker Compose**。

`docker-compose.yml`という設定ファイルに、どのコンテナを立ち上げるかを記述し、`docker-compose up`というコマンド1つ実行するだけで、必要なサービスがすべて立ち上がる。

これを活用することで、ミドルウェアの構成まで含めた開発環境の統一が可能になる。

## Dockerを始めてみよう
そういうわけで、開発環境をDockerに乗せるための方法を次の3ステップで確認していく。それぞれGitHubのリポジトリもあるので、細かいところはREADMEを見て手元で動かして確認してみて下さい。

1. Dockerfileの使い方
2. Docker Composeの使い方
3. Docker Composeでアプリケーションを構築する

_注意：ここで紹介しているDockerのコマンドは、環境によっては`sudo`が不要です。_

### Step1. Dockerfileの使い方

まずは、**DockerHubにあるイメージを元に、独自のイメージを作る方法**から。Dockerfileを書いていく。

リポジトリはこちら
https://github.com/KeitaMoromizato/docker-sample-1

`Dockerfile`と`app.js`の2ファイルだけの単純な構成。`app.js`はコンテナ上で動かすアプリケーションで、ここでは1000msごとに`Hello World!`と表示するだけのアプリを動かす。

```app.js
setInterval(() => {
  console.log('Hello World!');
}, 1000);
```

そして、上記ファイルをnode.jsのコンテナ上で実行するための設定が`Dockerfile`に書かれている。`Dockerfile`には、`FROM`や`COPY`などの独自のコマンドを組み合わせて、どのようなイメージを作るのかを記述していく。

```Dockerfile:Dockerfile
FROM node:6.9.1

ENV HOME=/home/app

COPY app.js $HOME/
WORKDIR $HOME

CMD ["node", "app.js"]
```

コマンドの解説

| コマンド | 意味 |
| --- | --- |
| FROM | ベースとなるDockerイメージを指定。[DockerHub](https://hub.docker.com/)から探す |
| ENV | Dockerfile上で使う変数を定義する |
| COPY | ホスト上のファイルを、イメージ上にコピーする |
| WORKDIR | コンテナを起動したときのワーキングディレクトリを指定する |
| CMD | コンテナを起動したときに実行するコマンドを定義する |

つまりこのDockerfileから生成されるイメージは、

1. node.jsのイメージをベースにしている
2. ワーキングディレクトリに`app.js`がある
3. 起動時には`node app.js`コマンドを実行する

というもの。このDockerfileを元に、次のコマンドでイメージを作成し

```
$ sudo docker build -t docker-sample:1.0 .
```

できたイメージを元にコンテナを起動する。ここで起動するコンテナは、先に上げた「node.jsの実行環境が整っていてワーキングディレクトリに`app.js`がある」コンテナ。

```
$ sudo docker run docker-sample:1.0
```

これで、コンソール上にひたすら`Hello World!`が流れるコンテナが起動する。コンテナを停止するには下記のコマンドを実行する。

```
$ sudo docker rm -f <コンテナID>
```

ちなみに、コンテナ上で実行するコマンド（`RUNコマンド`）は、**フォアグラウンドで起動し続けるプロセスでなければならない**。仮にここで作成した`app.js`が、`Hello World!`を1度表示しただけで終了するプログラムであれば、プログラムの終了と同時にコンテナも停止する。

### Step2. Docker Composeの使い方

次に、**Docker Composeを使用して複数のコンテナを管理する方法と、Dockerボリュームについて**学ぶ。

リポジトリはこちら
https://github.com/KeitaMoromizato/docker-sample-2

`docker-compose.yml`というYAMLファイルを使用して、各コンテナの設定を記述していく。ここでは、`app`というアプリケーションコンテナと、`nginx`というnginxのリバースプロキシコンテナを管理する。

```yaml:docker-compose.yml
version: '2'
services:
  app:
    build: .
    container_name: 'node'
    ports: 
      - '3000:3000'
  nginx:
    image: nginx
    container_name: 'nginx'
    ports: 
      - '8080:8080'
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d:ro
      - ./nginx/www:/var/www:ro
```

Docker Composeで構築したアプリケーションは、`docker-compose build`コマンドでイメージを作成する。

```
$ sudo docker-compose build
```

buildが終われば、`docker-compose up`コマンドを実行。すると、`docker-compose.yml`に記述されたすべてのコンテナが起動する。

```
$ sudo docker-compose up
```


YAMLのディレクティブも、最低限下記を覚えておけば問題ない。

#### build

自作のDockerfileを元にコンテナを起動する場合は、buildディレクティブを使用し、相対パスでDockerfileの位置を指定する。

#### image

配布されているイメージを元にコンテナを起動する場合は、imageディレクティブでイメージ名を指定する。

#### ports

`ホストポート:コンテナポート`でポートのマッピングを指定する。

#### volumes

コンテナにマウントするボリューム（≒ ディレクトリ）を指定する。主な用途としては

1. コンテナを停止してもデータを永続化したい
2. ホスト上にあるファイルをコンテナ内で使用したい
3. あるコンテナから出力されるファイルを他のコンテナから使用したい

など。ここではホスト上のファイルをコンテナにマウントしており、`ホスト上のパス:コンテナ上のパス`と指定する。`:ro`を付けると、コンテナからはReadOnlyとなるので、基本的には付ける。


### Step3. Docker Composeでアプリケーションを構築する

最後に、**Ruby on Railsを例にして実際のアプリケーションでの活用方法**を学ぶ。

リポジトリはこちら
https://github.com/KeitaMoromizato/docker-sample-3

実際のアプリケーションをDockerに乗せて開発するには、いくつか気をつけるべきポイントがある。ここでは5つ上げてみたので、順に見ていく。

1. コンテナではバックグラウンドで実行しない
2. アプリケーションのディレクトリは`Dockerfile`でコピーせず、ボリュームでマウントする
3. データベースのデータはボリュームで永続化する
4. アプリケーションのコマンドはコンテナ上で実行する
5. モジュールのインストール先に気を使う

#### 1. コンテナではバックグラウンドで実行しない

Dockerコンテナは、1つのプロセスのみ実行する。フォアグラウンドでプロセスが動いている間だけコンテナは動くので、バックグラウンドで動かすことはできない。そこで、unicornの起動を`foreman`で管理している。

コンテナ上で実行するコマンドは`docker-compose.yml`のcommandディレクティブで指定できる。

```yaml:docker-compose.yml
    command: foreman start -f Procfile.dev
```

#### 2.アプリケーションのディレクトリは`Dockerfile`でコピーせず、ボリュームでマウントする

Railsコンテナに、本リポジトリのディレクトリを丸ごとマウントしている（本当は不要なものは削除してもよいが、今回は簡易的に）。

`Dockerfile`のCOPYディレクティブを使い、イメージ自体にファイルをバンドルすることも可能。ただし、その場合はファイルを変更するたびに`docker-compose build`を実行する必要があるため、開発環境では現実的ではない。

そこで、開発時にはファイルをまるっとマウントし、ファイルの変更がコンテナで動くアプリケーションにそのまま反映されるようにする。

```yaml:docker-compose.yml
services:
  app:
    build: .
    volumes:
      - .:/usr/src/app
```

#### 3.データベースのデータはボリュームで永続化する

MySQLのデータをDockerボリュームに保存することで永続化している。コンテナを再起動しても、データが保存される。

`mysql-data`というボリュームを作成し、MySQLのデータが生成されるディレクトリ（`/var/lib/mysql`）にマウントする。

```yaml:docker-compose.yml
  mysql:
    image: mysql:5.7.10
    volumes:
      - mysql-data:/var/lib/mysql
volumes:
  mysql-data:
    driver: local
```

#### 4.アプリケーションのコマンドはコンテナ上で実行する

アプリケーションを開発していると、データベースのマイグレートやモジュールのインストールなど、コマンドを実行する必要が出てくる。それらのコマンドをホスト上で実行すると、コンテナとの環境差異によってエラーが出る可能性がある。よって、コマンドはコンテナ上で実行する。

```
$ sudo docker-compose run --rm <コンテナ名> <コマンド>
```

```:例
$ sudo docker-compose run --rm app bundle install
```


コマンド実行後にコンテナを削除するときは`--rm`オプションをつける。

#### 5.モジュールのインストール先に気を使う

RubyのGem、node.jsのnpmのようにアプリケーションを開発するためには、モジュールをインストールし、活用するのが一般的。それらのモジュールも、永続化しないとコンテナを起動するたびにインストールが必要になる。

よく`Dockerfile`内でインストールを完了しているサンプルを目にするが、開発初期は頻繁にモジュールのインストールが発生するので、その度に`docker-compose build`をするのは効率が悪い。

解決策としては、以下の2つが考えられる。

1. モジュールのインストール先ディレクトリをローカルにマウントして永続化する
2. コンテナにvolumeをマウントし、volume上にモジュールをインストールするように設定する

どちらでもよいが、エディタ上でeslintを使っている場合など、ホスト上にモジュールが存在するかをチェックするケースがあるので、今回は1を選択する。

`Dockerfile`上でGemのインストール先を`vendor/bundle`に変更している。

```Dockerfile
RUN \
  bundle config --local path vendor/bundle && \
```

## お役立ちコマンド集

### 起動しているコンテナの一覧

```
$ sudo docker ps
```

### コンテナをすべて停止する
変な動作をしたときの、とりあえず停止コマンド

```
$ sudo docker rm -f $(sudo docker ps -qa)
```

### イメージをすべて削除する
いろいろなイメージを作りすぎて面倒になったときに

```
$ sudo docker rmi $(sudo docker images -q)
```

### 起動しているコンテナの中身を確認する

まずはコンテナIDを下記のコマンドで確認して

```
$ sudo docker ps
```

`docker exec`を実行。あとは普通にLinuxを使っている感覚でいろいろ調べる。

```
$ sudo docker exec -i -t <container ID> bash
```

## 感想
少しDockerを触ってみて、DockerfileのCOPYディレクティブでバンドルされるファイルと、ボリュームとしてマウントされるファイル、そしてコンテナ実行時に生成されるファイルの違いに一番引っかかった。それらの違いを理解して、効果的に役割を分けるのが難しいかもしれない。

でも、慣れてくるとローカルPCに環境構築はせずに、すべてをDockerに乗せたくなる。

## 参考

- [RailsアプリをDockerで開発するための手順](http://qiita.com/togana/items/30b22fc39fe6f7a188ec)
- [開発におけるDocker導入のメリット](http://qiita.com/minodisk/items/5ffd20588b995523756f)