この記事の対象
- Dockerを使ったことがない。もしくは、触ってみたけどよくわからない
- Webアプリの開発中に「MySQLを起動しわすれていた」とか「nodeのバージョン違った」で悩まされている人
背景
Dockerの事例は増えてきたけど、なかなか手を出しづらい人も多いんじゃないだろうか。
個人的に、ここ数ヶ月でいろいろとDockerの構成を試しているので、それをふまえて開発環境でのDockerの使い方を解説しようというのがこの記事の目的。
- Dockerでnginx+node.jsのSPA構成を試す
- React SSR+WordPress REST APIをDocker Composeで試す
- RailsのToDoアプリチュートリアル(on Docker)
productionでのDocker活用となると、触る機会も限られてくるし、気軽に試せるものじゃない。そこで今回は、Dockerのポータビリティ(production/test環境でもそのまま動く)という面ではなく、環境の同一性という面に焦点を当て、開発環境で使ってみる。
Dockerは「デプロイする時のイメージはこうで〜」と最初から考えて始めると、学習コストは結構高い。でも開発環境で使うだけならセキュリティとかも気にしなくていいし、そういうところで慣れてから、本番環境での活用を考え始めたらいいじゃないか。
Docker for Mac、Docker 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を見て手元で動かして確認してみて下さい。
- Dockerfileの使い方
- Docker Composeの使い方
- Docker Composeでアプリケーションを構築する
注意:ここで紹介しているDockerのコマンドは、環境によってはsudo
が不要です。
Step1. Dockerfileの使い方
まずは、DockerHubにあるイメージを元に、独自のイメージを作る方法から。Dockerfileを書いていく。
リポジトリはこちら
https://github.com/KeitaMoromizato/docker-sample-1
Dockerfile
とapp.js
の2ファイルだけの単純な構成。app.js
はコンテナ上で動かすアプリケーションで、ここでは1000msごとにHello World!
と表示するだけのアプリを動かす。
setInterval(() => {
console.log('Hello World!');
}, 1000);
そして、上記ファイルをnode.jsのコンテナ上で実行するための設定がDockerfile
に書かれている。Dockerfile
には、FROM
やCOPY
などの独自のコマンドを組み合わせて、どのようなイメージを作るのかを記述していく。
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から生成されるイメージは、
- node.jsのイメージをベースにしている
- ワーキングディレクトリに
app.js
がある - 起動時には
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のリバースプロキシコンテナを管理する。
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
コンテナにマウントするボリューム(≒ ディレクトリ)を指定する。主な用途としては
- コンテナを停止してもデータを永続化したい
- ホスト上にあるファイルをコンテナ内で使用したい
- あるコンテナから出力されるファイルを他のコンテナから使用したい
など。ここではホスト上のファイルをコンテナにマウントしており、ホスト上のパス:コンテナ上のパス
と指定する。:ro
を付けると、コンテナからはReadOnlyとなるので、基本的には付ける。
Step3. Docker Composeでアプリケーションを構築する
最後に、Ruby on Railsを例にして実際のアプリケーションでの活用方法を学ぶ。
リポジトリはこちら
https://github.com/KeitaMoromizato/docker-sample-3
実際のアプリケーションをDockerに乗せて開発するには、いくつか気をつけるべきポイントがある。ここでは5つ上げてみたので、順に見ていく。
- コンテナではバックグラウンドで実行しない
- アプリケーションのディレクトリは
Dockerfile
でコピーせず、ボリュームでマウントする - データベースのデータはボリュームで永続化する
- アプリケーションのコマンドはコンテナ上で実行する
- モジュールのインストール先に気を使う
1. コンテナではバックグラウンドで実行しない
Dockerコンテナは、1つのプロセスのみ実行する。フォアグラウンドでプロセスが動いている間だけコンテナは動くので、バックグラウンドで動かすことはできない。そこで、unicornの起動をforeman
で管理している。
コンテナ上で実行するコマンドはdocker-compose.yml
のcommandディレクティブで指定できる。
command: foreman start -f Procfile.dev
2.アプリケーションのディレクトリはDockerfile
でコピーせず、ボリュームでマウントする
Railsコンテナに、本リポジトリのディレクトリを丸ごとマウントしている(本当は不要なものは削除してもよいが、今回は簡易的に)。
Dockerfile
のCOPYディレクティブを使い、イメージ自体にファイルをバンドルすることも可能。ただし、その場合はファイルを変更するたびにdocker-compose build
を実行する必要があるため、開発環境では現実的ではない。
そこで、開発時にはファイルをまるっとマウントし、ファイルの変更がコンテナで動くアプリケーションにそのまま反映されるようにする。
services:
app:
build: .
volumes:
- .:/usr/src/app
3.データベースのデータはボリュームで永続化する
MySQLのデータをDockerボリュームに保存することで永続化している。コンテナを再起動しても、データが保存される。
mysql-data
というボリュームを作成し、MySQLのデータが生成されるディレクトリ(/var/lib/mysql
)にマウントする。
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つが考えられる。
- モジュールのインストール先ディレクトリをローカルにマウントして永続化する
- コンテナに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に乗せたくなる。