複雑な構成のrailsアプリをdockerで動かしたい場合はどうするべきか
はじめに
本記事は複雑な構成をとるrailsアプリケーションをdockerで動作させる場合についてまとめています。
これまでにrailsを動作させるコンテナのDockerfileをどう記述するのが良いかという観点と、railsアプリケーションのプレビュー環境をdockerで構築出来るかという検証の二つの記事を公開していました。
先に公開した二つの記事では、railsアプリケーションを単独のコンテナで動作させる場合について言及しています。
しかしアプリケーションの規模が大きくなるとrails単体だけでは無く、キャッシュにmemcachedやredis, 検索にsolrにelasticsearch, フロントにはapache, nginxと複数の要素で一つのシステムを構築することになってきます。
このような環境を出来るだけ簡単そして簡潔に構築することが本記事の目的です。
コンセプト
複数のサービスからなるシステムを構築する大きな方向性としては、サービス全部入りのコンテナを用意するものと、各機能毎にコンテナを用意するものが考えられます。
“全部入り"のコンテナを用意するとなると、Dockerfileにセットアップをごりごりと記述していくことになるでしょう。
Dockerfileの記述は複雑にならざるを得ず、少しの変更でコンテナ全体のビルドをやり直す必要も出てきます。
そこで本記事では、各サービス毎にコンテナを分離する方法をとります。
今回はコンポジションおよびオーケストレーションツールであるdocker/composeを用います。
目指す構成としてはフロントにnginx, railsアプリはunicornを動作させ、データベースにはmysqlを、キャッシュと全文検索としてredis, elasticsearchを取り扱います。
検証環境
検証はmac上にboot2dockerでdocker環境を構築して行っています。
dockerは1.5.0, docker-compose 1.0.1 を使用しています。
$ boot2docker version
Boot2Docker-cli version: v1.5.0
Git commit: ccd9032
$ docker version
Client version: 1.5.0
Client API version: 1.17
Go version (client): go1.4.1
Git commit (client): a8a31ef
OS/Arch (client): darwin/amd64
Server version: 1.5.0
Server API version: 1.17
Go version (server): go1.4.1
Git commit (server): a8a31ef
$ docker-compose --version
docker-compose 1.1.0
本記事で掲載しているソースコードの全体をk-shogo/mini_doc at figで公開しています。
railsコンテナ
主役はrailsのコンテナです。従ってリポジトリもrailsがメインで、その中に他のコンテナの設定を含めることになります。
railsアプリのDockerfileはプロジェクトルートに設置します。
FROM ruby:2.2.0
RUN apt-get update && apt-get install -y nodejs mysql-client --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/
RUN bundle install -j4 --without development test
COPY . /usr/src/app
EXPOSE 3000
CMD ["bundle", "exec", "unicorn", "-c", "config/unicorn.rb"]
rubyのバージョンを切り替える場合にはFROM
の公式rubyコンテナを切り替えることで容易に対応可能です。
今回はデータベースにmysqlを選択したことと、assetsのコンパイル目的で mysql-clientとnodeをインストールしています。
Gemfile
及びGemfile.lock
をアプリケーションのソースの前に追加しているのは、キャッシュを用いてbundle installが必要ない場合にスキップして高速化を図るためです。
RUN bundle install
でGemfileに記述されたライブラリをインストールしますが、development
やtest
グループを除外するかは好みで変更してください。
今回はrailsアプリケーションのDockerfile内部ではdb:create
やdb:migrate
を行っていません。
これはdbに関するrakeタスクはデータベースに接続されていなければ実行できないためであり、railsアプリケーションのコンテナを単独でもビルドできるようにするためです。
Gemfileにはmysql, redis, elasticsearchに接続するためのライブラリ、そしてアプリケーションサーバーであるunicornを追加します。
# database
gem 'mysql2'
# redis
gem 'redis'
# elasticsearch
gem 'elasticsearch-model'
gem 'elasticsearch-rails'
# Use Unicorn as the app server
gem 'unicorn'
オーケストレーション
構成全体の設定を定義するfig.ymlもルートディレクトリに置いておきます。
構成全体の設定を定義するdocker-compose.ymlもルートディレクトリに置いておきます。
docker-compose.ymlではmysql, elasticsearech, redis, nginx, railsそしてdatastoreの5つのコンテナが定義されています。
記述方法の詳細はcompose/yml.md at 1.1.0 · docker/composeを参照してください。
mysql:
image: mysql:5.6.23
environment:
MYSQL_ROOT_PASSWORD: 'pass'
ports:
- '3306:3306'
volumes_from:
- datastore
elasticsearch:
build: containers/elasticsearch
ports:
- '9200:9200'
- '9300:9300'
volumes_from:
- datastore
redis:
image: redis:2.8.19
ports:
- '6379:6379'
volumes_from:
- datastore
nginx:
build: containers/nginx
ports:
- '8080:80'
volumes_from:
- datastore
links:
- rails
datastore:
build: containers/datastore
rails:
build: .
ports:
- '3000:3000'
environment:
RAILS_ENV: preview
MYSQL_ROOT_PASSWORD: 'pass'
DATABASE_URL: mysql2://root:pass@mysql:3306
REDIS_URL: redis://redis:6379
ELASTICSEARCH_URL: http://elasticsearch:9200
SECRET_KEY_BASE: hogehoge
volumes_from:
- datastore
links:
- mysql
- elasticsearch
- redis
サービス毎のコンテナ定義
まずはimageとbuildについて。
mysqlとredisはimageによってdocker hubの公式イメージを取得しています。
elasticsearchは日本語用プラグインを導入したイメージを使いたかったので、buildディレクティブで自前のDockerfileを指定しています。
今回は独自のDockerfileはリポジトリのルート以下にcontainersディレクトリを作り、さらにサービス毎にディレクトリを分割しています。
- containers/
|- datastore/
| Dockerfile
|- elasticsearch/
| Dockerfile
|- nginx/
| Dockerfile
| nginx.conf
FROM elasticsearch:1.4.3
RUN plugin install mobz/elasticsearch-head
RUN plugin install elasticsearch/elasticsearch-analysis-kuromoji/2.4.2
elasticsearchのDockerfileについて見てみましょう。
独自の定義とはいってもベースは公式のelasticsearchを利用しています。
追加で必要になるプラグインのインストールのみ記述することで意図が明確になります。
FROM nginx:1.7.9
COPY nginx.conf /etc/nginx/nginx.conf
nginxも公式イメージをベースにします。
js, cssなどの静的アセットを配信しつつ、unicornに対してリバースプロキシとしても動作させるので、このイメージには設定ファイルを追加します。
mysqlやredisも設定を変更したい場合は公式イメージに設定を上書きする運用が良いでしょう。
コンテナ間のリンク
docker-compose.ymlのlinksディレクティブではコンテナ間のリンクを定義します。
アクセスの方向性は nginx -> rails -> (mysql, redis, elasticsearch)のため、nginxのlinksにはrailsを、railsのlinksにはmysql, redis, elasticsearchを指定しています。
さて、railsのdatabase.ymlを指定するときにはどうするでしょうか。
dockerのコンテナ間のリンクの場合、MYSQL_PORT_3306_TCP_ADDR
のようなdockerが自動的に設定してくれる環境変数を用いる例が出てきます。
しかしこれは得策とはいえません。
redisへの接続を例に考えてみると、REDIS_PORT_6379_TCP_ADDR
のような環境変数になりますが、この使いにくい環境変数を活用しようとすると、config/initializers/redis.rb
等で切り分けが必要になります。
if Rails.env.docker?
# docker環境の場合
Redis.current = Redis.new host: ['REDIS_PORT_6379_TCP_ADDR'], port: ENV['REDIS_PORT_6379_TCP_PORT']
else
# それ以外の場合...
end
このような記述をしないための設定がrailsのenvironmentディレクティブです。
DATABASE_URL: mysql2://root:pass@mysql:3306
REDIS_URL: redis://redis:6379
ELASTICSEARCH_URL: http://elasticsearch:9200
docker-composeでlinkディレクティブを指定すると、hostsに他コンテナの設定が追加されます。
$ docker-compose run rails cat /etc/hosts
172.17.2.69 6866a8e73892
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.2.66 minidoc_elasticsearch_1
172.17.2.67 minidoc_redis_1
172.17.2.67 redis
172.17.2.67 redis_1
172.17.2.66 elasticsearch
172.17.2.66 elasticsearch_1
172.17.2.65 mysql_1
172.17.2.65 minidoc_mysql_1
172.17.2.65 mysql
直接railsからhostsの値を用いても良いですが、environmentディレクティブで整理してrailsに環境変数として渡すことで、プロダクションなどの他環境に移しやすくなります。
例えばredis, elasticsearchはgemの設計として、それぞれ REDIS_URL
, ELASTICSEARCH_URL
が設定されていた場合に接続先として用いてくれるので、それらに合わせることでdocker環境でもプロダクション環境でも対応できます。
database.yaml
でも環境変数DATABASE_URL
で接続情報を渡すと決めていれば環境の切り替えが容易になります。
default: &default
adapter: mysql2
encoding: utf8
pool: 5
development:
<<: *default
database: minidoc_development
username: root
password:
host: localhost
test:
<<: *default
database: minidoc_test
username: root
password:
host: localhost
preview:
<<: *default
database: minidoc_preview
url: <%= ENV['DATABASE_URL'] %>
production:
<<: *default
database: minidoc_production
url: <%= ENV['DATABASE_URL'] %>
データボリュームコンテナ
データボリュームコンテナには永続化、共有したいパスをVOLUMEコマンドで定義しているだけです。
FROM busybox:latest
VOLUME /var/lib/mysql
VOLUME /usr/share/elasticsearch/data
VOLUME /data
VOLUME /usr/src/app/public
VOLUME /usr/src/app/log
CMD /bin/sh
mysqlやelasticsearch用には永続化するだけですが、ここでのポイントはrailsが生成するassetsの扱いです。
今回フロントのnginxから静的ファイルを配信したいので、アセットをnginxコンテナに渡さなければなりません。
もちろんローカルでassets:precompileしたものをnginxコンテナにADDする方法もとれます。
その場合、ローカルにnode.js等が必要になってくるため、今回の例ではnodeが用意されているrailsコンテナでコンパイルを行い、そのファイルを共有したボリュームによってnginxから読み出す方法を紹介します。
そのためのパスが/usr/src/app/public
です。
nginxの設定ファイルを見てみましょう。
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024; # increase if you have lots of clients
accept_mutex off; # "on" if nginx worker_processes > 1
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on; # off may be better for *some* Comet/long-poll stuff
tcp_nodelay off; # on may be better for some Comet/long-poll stuff
gzip on;
gzip_http_version 1.0;
gzip_proxied any;
gzip_min_length 500;
gzip_disable "MSIE [1-6]\.";
gzip_types text/plain text/html text/xml text/css
text/comma-separated-values
text/javascript application/x-javascript
application/atom+xml;
upstream app_server {
# for UNIX domain socket setups:
# server unix:/path/to/.unicorn.sock fail_timeout=0;
# for TCP setups, point these to your backend servers
# server 192.168.0.7:8080 fail_timeout=0;
server rails:3000 fail_timeout=0;
}
server {
listen 80;
server_name localhost;
client_max_body_size 4G;
keepalive_timeout 5;
# path for static files
root /usr/src/app/public;
try_files $uri/index.html $uri.html $uri @app;
location @app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}
# Rails error pages
error_page 500 502 503 504 /500.html;
location = /500.html {
root /usr/src/app/public;
}
}
}
ポイントはroot /usr/src/app/public;
の部分です。
/usr/src/app
はrailsアプリケーションが配置されている場所です。
そのためrailsコンテナでrake assets:precompile
すると/usr/src/app/public
にコンパイルされたアセットが配置されます。
このパスをデータボリュームコンテナで共有することでnginxコンテナから読み出すということです。
その他設定 dockerignore
dockerignoreファイルも用意しておきましょう。
ログや一時ファイルなどを除外しておかないと、アプリケーションの変更だと見なされてしまいます。
## Rails
public/assets/*
## Environment normalisation:
.bundle
vendor/bundle
.git
## log
log
tmp
## OSX
.DS_Store
## docker
.dockerignore
containers
ビルド & 実行
一気にビルドする事も出来ますし、サービスを指定することも可能です。
$ docker-compose build
$ docker-compose build rails
rakeタスクなどを実行する場合にはdocker-compose run rails
に続けて指定できます。
docker-compose.ymlのなかで
RAILS_ENV`を指定している場合はそのRails.envで動作します。
$ docker-compose run rails rake db:create
$ docker-compose run rails rake db:migrate
$ docker-compose run rails rake db:seed_fu
$ docker-compose run rails rake assets:precompile
docker-compose up
で複数のコンテナを一気に立ち上げましょう。
$ docker-compose up
あとはboot2docker ip
でアドレスを確認し、今回の場合はnginxの80と8080番を繋いでいるので、http://192.168.59.103:8080
にアクセスします。(IPアドレスは人によって異なります)
まとめ
本記事では複雑な構成のrailsアプリケーションをdockerで動作させる場合についてまとめました。
今回はrailsのコードをコンテナに入れ、プロダクション相当で稼働させる例でした。
本番環境もdockerで運用することが出来たなら、ビルドしたコンテナをそのまま本番に持って行くことが出来るようにです。
開発環境で使いたい、つまりローカルでコードを編集し、結果をコンテナで実行させているrailsで確認したい場合などは、プロジェクトルートをData Volumeでマウントする運用も出来るでしょう。
この方法ならdocker build
やコンテナに対してrsync
等すること無くコードの反映が可能になります。
dockerを用いた環境の構築では、railsのリポジトリに必要なサービスの設定ファイルを含められること、開発環境を複数開発者で揃えられること、そして容易に環境の破棄が可能になること(例えばブランチの切り替え時にデータボリュームコンテナを差し替えるなど)のメリットが考えられます、