Edited at

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

More than 1 year has passed since last update.


概要

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

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


目標


  • コマンド一発で必要なサービス群が全て立ち上がるようにする

  • Docker Compose を使い、1サービスごとに1コンテナを立ち上げる


  • vendornode_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

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"]



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 でビルドした vendornode_modulesホストのものに置き換えられてしまいます

そこで、 /app/vendor/app/node_modules を volume に切り出すよう指定しています。こうすると、Docker はイメージから vendornode_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 を先にする必要はありません。

そもそもこれは

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 のような、ファイルの変更を検出してプロセスを再起動するツールを使っている場合でも、そのツール自身が再起動していなければ大丈夫です