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

  • 126
    いいね
  • 0
    コメント

この記事の対象

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

背景

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

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

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

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

Docker for MacDocker for 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

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

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

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

Dockerfile
FROM node:6.9.1

ENV HOME=/home/app

COPY app.js $HOME/
WORKDIR $HOME

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

コマンドの解説

コマンド 意味
FROM ベースとなるDockerイメージを指定。DockerHubから探す
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のリバースプロキシコンテナを管理する。

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ディレクティブで指定できる。

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

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

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

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

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

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

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

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

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

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に変更している。

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に乗せたくなる。

参考