概要
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 専用のコンテナを立てるべきです)。
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"]
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
を先にする必要はありません。
そもそもこれは
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
しておくべきです。
-
rerun
gem のような、ファイルの変更を検出してプロセスを再起動するツールを使っている場合でも、そのツール自身が再起動していなければ大丈夫です ↩