Dockerで作る最強のWeb開発環境2017

# 概要
Web アプリケーションを開発しているときに、開発環境に MySQL や Redis を用意しバージョンを揃え、いや Redis はキャッシュにしか使ってないし必須じゃないから開発環境に無い場合のコードも書いて…… というようなことを2017年にもなってやりたくないので、Docker を使って良い感じにやっていきます。

Docker や Docker Compose に関する基本的な説明は割愛するので、公式ドキュメントをあたってください。

# 目標
- コマンド一発で必要なサービス群が全て立ち上がるようにする
- Docker Compose を使い、1サービスごとに1コンテナを立ち上げる
- `vendor` や `node_modules` は、ホスト側のものと完全に分離する。OS が違う場合、Native extension があると問題の原因になるので避けたい。
- ホスト側ではエディタと git さえあれば完結、というぐらいを目指す

# 対象領域
Ruby on Rails をはじめ、PHP (mod_php でも php-fpm でも) や Node.js など、

- 1プロセスでリクエストを受け
- コードを書き換えた後にプロセスを再起動しなくても新しいコードが実行される

タイプの開発であれば問題なく使えます[^1]。JVM 系はよく分からないです。すみません。


# Dockerfile 例
Rails + MySQL に加え、Browserify のために Node という、ありがちな Web アプリケーション開発環境の構成例です（Server-side rendering のために Node を Rails の前段に置きたい、という話ならば Node 専用のコンテナを立てるべきです）。

```dockerfile:Dockerfile
FROM ubuntu:16.04

RUN locale-gen en_US.UTF-8
ENV LANG en_US.utf8
ENV LC_ALL en_US.UTF-8

ARG DEBIAN_FRONTEND=noninteractive

RUN \
  apt-get update && \
  apt-get install -y curl && \
  # Add Node.js repository
  curl -sL https://deb.nodesource.com/setup_7.x | bash - && \
  apt-get install -y \
    build-essential \
    libmysqlclient-dev \
    libreadline-dev \
    libssl-dev \
    libxml2-dev \
    libxslt-dev \
    libyaml-dev \
    nodejs \
    ruby2.3 \
    ruby2.3-dev \
    zlib1g-dev

WORKDIR /app

# Install RubyGems
ARG BUNDLE_JOBS=2
RUN gem update --system && gem install bundler
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install --path vendor/bundle

# Install npm modules
RUN npm install -g yarn
COPY package.json /app/package.json
COPY yarn.lock /app/yarn.lock
RUN yarn

EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
```

```yaml:docker-compose.yml
version: '3'
services:
  web:
    build:
      context: .
    volumes:
      - .:/app
      - /app/vendor
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      - RACK_ENV=development
      - DATABASE_URL=mysql2://webapp:webapp@mysql/webapp_development
    depends_on:
      - mysql

  mysql:
    image: mysql:5.7
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
    volumes:
      - "mysql-webapp-data:/var/lib/mysql"
    environment:
      - "MYSQL_DATABASE=webapp_development"
      - "MYSQL_ROOT_PASSWORD=password"
      - "MYSQL_USER=webapp"
      - "MYSQL_PASSWORD=webapp"

volumes:
  mysql-webapp-data:
```

## 起動・終了など
- イメージのビルド: `docker-compose build`
- サービスの起動: `docker-compose up` (`-d` でバックグラウンド)
- サービスの終了: `docker-compose down`

## 補足: マウントしているボリュームについて
Compose 内でホストのディレクトリをマウント (`.:/app`) していますが、この指定だけではせっかく Dockerfile でビルドした `vendor` や `node_modules` が**ホストのものに置き換えられてしまいます**。

そこで、 `/app/vendor` や `/app/node_modules` を volume に切り出すよう指定しています。こうすると、Docker は**イメージから** `vendor` や `node_modules` をコピーした volume を作成し、ホストディレクトリのそれらの代わりにアクセスさせてくれるようになります。

結果として、ソースコードはホストの変更が即時に反映され、依存ライブラリは volume に閉じ込められているのでホストの影響を一切受けない、理想的な構成となります。

# FAQ
上記の構成例に対応する FAQ です。

## Gemfile をアップデートしたあと、どうすれば良いか
Gemfile を編集した後、普通にイメージをリビルドすれば大丈夫です。

```
$ vim Gemfile
$ docker-compose build
```

## コンテナ内でコマンドを実行したい
`docker-compose run <service> <command>` するだけ！ 例えば：

```
docker-compose run web ./node_modules/.bin/watchify
```

## DBのデータとか `vendor` とかを消してスッキリしたい
`docker-compose down` に `-v` をつけると、volume も全消ししてくれます。

## 新規プロジェクトだと Gemfile.lock とか yarn.lock とかがなくてビルドできない
仕方ないので一度はホストで生成しましょう（どうせホスト側の `vendor` はコンテナからは見えません）。Dockerfile をいじって Gemfile / package.json だけでコンテナを起動し、手で `docker-compose run web bundle install` する手もありますが、煩雑ですし面倒です。潔癖症でなければホストでやるほうが楽だと思います。


# 余談: `COPY Gemfile` などについて
Dockerを使ったことのある方ならお気づきかもしれませんが、実は今回のケースでは必ずしも `COPY Gemfile` / `COPY Gemfile.lock` を先にする必要はありません。

そもそもこれは

```dockerfile
ADD .
RUN bundle install
```

のようなDockerfileで、Gemfileを変更してないときでも (実行する必要のない) `bundle install` が走ってしまい、長時間待たされるのを避けるための定番イディオムのようなものでした。しかし、今回のケースではそもそもファイルに変更があってもリビルドの必要が無いように構成しているので、この問題の対策は不要なのです。

しかし、Dockerfile 段階で `bundle install` しておかないとコンテナ内に `rails` コマンドが存在せず、起動が失敗してしまいます（当然）。`bash` を指定してコンテナを立ち上げ、手で `bundle install` を実行するという作業が必要になり、これはこれで非常に面倒なので、Gemfile だけ先に投入してあげています。

Build cache について: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/build-cache


# これはあくまで開発用の環境です
このDockerfileを本番の運用に使用するべきではありません。必要なファイルはすべてDockerfile段階で `COPY` しておくべきです。

[^1]: `rerun` gem のような、ファイルの変更を検出してプロセスを再起動するツールを使っている場合でも、そのツール自身が再起動していなければ大丈夫です