dockerでRails5+postgres+redisの開発環境を丁寧に構築してみた

  • 18
    いいね
  • 1
    コメント

はじめに

こんにちは、OthloTechの@pokohideです。

この記事はOthloTech Advent Calendar 2016の最終日(25日)の記事です!

世間はクリスマスムードで、こんな記事を読んでいる人もいないとは思いますが...最近MacBookProを購入して、新しいのを買ったら環境汚染しないぞ!と思っていたので、これを機に勉強してみました。なので、ステップごとに自分なりの解説コメント等付きますが、ご了承ください。

事前知識

dockerとは

docker.png

Docker Inc.が提供している仮想化のためのオープンソースソフトウェアです。

dd.png
『Docker実戦活用ガイド』p.7 より

Vagrantなどの仮想マシンとは違って、dockerのコンテナはOSレベルでの仮想化を行ってそのOS上でプロセスを動かすのですが、仮想マシンはマシンレベルで仮想化を行います。なので、以下のような特徴があります。

コンテナ(Docker) 仮想マシン
起動 早い 遅い
リソース消費 少ない 多い
OS Linuxのみ 何でも可

vagrantはマシンレベルで構築するので、ベースとなるOSは特に限定されませんが、dockerはLinux上でのみしか動きません。しかし、dockerはLinuxの機能を借りて仮想環境を実現しているので必要なリソースは少なくて起動が早いのが特徴です。逆に仮想マシンはOS単位で仮想化しているのでリソース消費量が高く起動は遅いです。

以前はVirtualBoxが必要だったり環境設定が必要だったり、色々面倒だったそうですが、今はDocker社が公式で出している Docker for (Mac/Windows/Linux)をインストールするとすぐに使えるので、まだインストールをしていない人は入れておきましょう。

docker-composeとは?

dockerをインストールするとdockerコマンドが使用できて、それを用いることで色々なコンテナを立ち上げることができるのですが、Railsアプリの環境を構築するのにPostgresだったり、Rubyだったり、Redisだったり複数のコンテナからなるサービスを手作業で作るのは大変。ということで、docker-composeというツールを用いることにします。

docker-composeとは複数のコンテナからなるdockerアプリケーションを定義・実行るツールで、compose用の設定ファイルを用意してコマンドを実行することで、記述されたコンテナを一気に起動できます

docker-composeはリリースページにアクセスして最新のバージョンをcurlコマンドを使ってインストールしてください。インストールの方法はリリースページに書かれているので、コンソールにコピペすれば大丈夫だと思います。

Docker compose release page

Dockerfile / docker-compose.yml

dockerコマンドでイメージの作成やコンテナの起動などを実行してもいいのですが、コンソールから入力するのは疲れるし、作業を一括して行いたくなってくるのでそんな時に使うのがDockerfileです。makefileのようなものですが、個人的にはそれ以上に取っ付きやすいくて気に入りました。

使えるコマンドも10種類程度しかないので、すぐなれると思います。
Dockerfileのコマンド

docker-compose.ymlはdocker-composeというツールを使うための設定ファイルで、ここにアプリケーションを構成する各サービスを定義します。

検証環境

検証はmac上でdockerは1.12, docker-composeは1.8.1を使用してdockerの環境を構築しています。2016年12月現時点の最新バージョンだと思います。

$ docker version
Client:
 Version:      1.12.3
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   6b644ec
 Built:        Wed Oct 26 23:26:11 2016
 OS/Arch:      darwin/amd64

Server:
 Version:      1.12.3
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   6b644ec
 Built:        Wed Oct 26 23:26:11 2016
 OS/Arch:      linux/amd64

$ docker-compose version
docker-compose version 1.8.1, build 878cff1
docker-py version: 1.10.3
CPython version: 2.7.9
OpenSSL version: OpenSSL 1.0.2h  3 May 2016

Dockerを活かす原則

1) 1 Container 1 Service

1つのコンテナには1つのサービスを動作させるようにしましょう。たとえば、今回用いるPostgresとRedisを同じコンテナで動かすことも可能ですが、コンテナが複雑化してしまうので、極力シンプルに保つため意識しましょう。

2) Dockerfileに書く

コンソールでdockerコマンドでコンテナを作成することも可能ですが、結局誰かと共有するにはその作成手順を明示しなくてはいけないので、Dockerfileに統一しましょう。

Railsアプリの開発環境を構築してみる

今回の設定として、開発中のRailsアプリ(Postgres+Redis)をDockerの仮想環境に移行することを考えます。ここで個人的な前提として、開発環境ではsqlite3を使っていたのでそこでの変更手順もまとめてあります。

  • ruby: 2.3.3
  • Ruby on Rails: 5.0.1.rc2

Rails用のDockerfileを用意する

Dockerfileはrailsアプリのルートディレクトリに配置してください。

Dockerfile
FROM ruby:2.3.3

ENV APP_ROOT /my_app

RUN apt-get update -qq && apt-get install -y nodejs build-essential libpq-dev postgresql-client

RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

ADD Gemfile ${APP_ROOT}/Gemfile
ADD Gemfile.lock ${APP_ROOT}/Gemfile.lock
RUN bundle install

ADD . $APP_ROOT

Dockerfileの最初はFROM IMAGE:TAGコマンドで、使用するベースイメージを選択しなくてはいけません。使用するイメージはCentOSだったり、りRedisだったり他人の作成したイメージを指定できますが基本的には、公式のイメージリストから選択すればいいでしょう。

Ubuntuを入れて、apt-getで1からrubyの環境を構築することも出来ますが、今回はあらかじめrubyの入っているイメージを選択しました。

ENVコマンドは環境変数を設定するコマンドでAPP_ROOTという変数に/my_appという文字列を格納しています。

RUNコマンドはを今回だとrubyの入ったコンテナ内のコンソールで実行する感じで、apt-getnodejsなどをインストールしています。

コンテナ内に、作業用にディレクトリを作ってそこをWORKDIRコマンドで起点とします。その作業ディレクトリにADDコマンドで自分のローカルのGemfileGemfile.lockをコピーして、コンテナ内でbundle installを実行した後、他のローカルのファイルすべてをコンテナ内にコピーしています。

メモ

他のサイトではGemfile/tmpフォルダにコピーしてbundle installをすることで高速化している例がありますが、Docker 1.1以上からはADDコマンドにキャッシュ機能が増えたので、変更がなければ、再ビルドをしても高速で行えます。

同様の理由で、ADD . $APP_ROOTコマンドで、ローカルの作業ディレクトリを全てコンテナ内にコピーをしているのですが、ここでログファイル等もコピーしてしまうと、変更を加えていないのに再ビルドが走る可能性があるのでコピー対象から明示的に外しておきましょう。これは.dockerignoreで実現できます。

.dockerignore
## Rails
public/assets/*
.ruby-version
.env

## Environment normalisation:
.bundle
vendor/bundle
.git*

## log
log/*
tmp/*

## OSX
.DS_Store

## docker
.dockerignore

docker-composeでオーケストレーション

ちなみに...

オーケストレーションとは、複雑なコンピュータシステム/ミドルウェア/サービスの配備/設定/管理の自動化を指す用語

お恥ずかしいですが、意味が分からなかったのでググりました。docker-composeで色々なサービス(Postgres, Redis, Nginx, etc...)などを一括で設定・起動するので、しっくりきますね。

docker-compose.yml
version: '2'
services:
  db:
    image: postgres
    ports:
      - "5432"
  redis:
    image: redis:latest
    ports:
      - 6379:6379
  web:
    build: .
    command: bundle exec rails s -p 3000 -b 0.0.0.0
    ports:
      - "3000:3000"
    links:
      - db
      - redis
    environment:
      DATABASE_USER: postgres
      DATABASE_PASSWORD:
      DATABASE_PORT: 5432
      DATABASE_HOST: db
      REDIS_URL: redis://redis:6379

version '2'や、はじめのservicesはDocker1.5系以降のバージョンで必要なおまじないだと思ってください。servicesの下の階層のdb, redis, webはそれぞれサービス名を意味していて、この名前のコンテナが立ち上がって、それ以下の内容がそのコンテナ内で実現される感じです。

RedisコンテナとDBコンテナは、redispostgresは公式のイメージが用意されているので、それをそのまま利用しています。

RailsコンテナはDockerfileで先程準備したコンテナをbuild: .で指定して、linksでDBコンテナとRedisコンテナを同時に起動するように設定しています。environmentは、そのコンテナ内で使用できる環境変数を定義していてます。

docker-composeで定義したserviceは同一のネットワーク配下に置かれるので、linksで定義しなくても、お互いのホスト名として利用できるので、記述しなくても大丈夫なそうです。

@hidekuro さんにコメントで訂正していただきました。ありがとうございます。

メモ

image: postgresと公式のpostgresイメージを使用していますが、正式な書き方はイメージ名:タグ名です。このようにタグ名を省略した場合は、postgres:latestと同じとみなされて最新のバージョンが適用されます。なので、バージョン指定が必要な場合は、以下から見つけるといいでしょう。

https://hub.docker.com/_/postgres/

portsはDocker内外にポートを開放する設定でRailsコンテナはブラウザからアクセスしたりするので、portsでポートを開放する必要がありますが、RedisコンテナやDBコンテナはRailsコンテナからしかアクセスしないなら、exposeでDocker内の他のコンテナに向けてポートを開放すべきかもしれないです。

postgresredisの公式イメージはportsを指定しなくても、デフォルトでそれぞれ54326379を指しますが、明示しておきます。

RailsのDBの設定

今回は前提として、開発環境ではsqlite3を使用していたので、まずそれをpostgresに変更します。その次に、RailsコンテナとDockerで用意したDBコンテナは別のコンテナなので、database.ymlを書き換えて接続の設定を行います。

sqlite3 postgres

Gemfileからsqlite3を削除して、本番環境のみに記述していたpgを上に持ってきました。

Gemfile
+ gem 'pg'

group :development, :test do
-  gem 'sqlite3'
end

group :production do
-  gem 'pg'
end

database.yml

環境変数を多様していますが、この中身は先ほどdocker-compose.ymlで定義しているので、改めて確認します。

DATABASE_USER: postgres
DATABASE_PASSWORD:
DATABASE_PORT: 5432
DATABASE_HOST: db

HOST名やUSER, PASSWORDはこれじゃないといけないの?と思った方は下記のメモ欄に書いているので参考にしてください。自分も結局他の方法はよくわからなかったのでデフォルトの設定で行きました...

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  timeout: 5000
  port: <%= ENV.fetch('DATABASE_PORT') { 5432 } %>
  host: <%= ENV.fetch('DATABASE_HOST') { 'localhost' } %>
  username: <%= ENV.fetch('DATABASE_USER') { 'root' } %>
  password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

redisの設定

database.ymlで環境変数にREDIS_URL: redis://redis:6379と設定しました。なので、redisの初期設定を行っているファイルを以下のように変更しました。

config/initialize/redis.rb
REDIS ||= Redis.new(url: ENV['REDIS_URL'] || 'redis://localhost:6379')

ここは適宜、自分の環境に合わせて変更してください。

メモ

今回は公式のPostgresを使用しましたが、本家ドキュメントの日本語訳によると

デフォルトでは、データベースは localhost で実行するとみなされます。そのため、db コンテナに指示しなくてはいけません。postgres イメージにデフォルトで設定されている database と username に変更する必要があります。

と書かれています。これに気付かずかなりハマりました。そのデフォルトのdatabaseとusernameは

development: &default
  adapter: postgresql
  encoding: unicode
  database: postgres
  pool: 5
  username: postgres
  password:
  host: db

となっているので、これを参考に database.ymlを変更しました。

また、上記でREDISに接続するために、redis://redis:6379でRedisコンテナに、database.ymlではhost: dbでDBコンテナにRailsコンテナから接続していますが、これにもDockerのおかげです。
docker-compose.ymlでlinksを書くと、コンテナ内の/etc/hostsに指定したサービス名とそのipアドレスが追加されます。以下は例です。

/etc/hosts
172.17.0.6  db
172/17/0.4  redis

このように、今回指定したサービス名がエイリアスとなって、ipアドレスを参照できるようになるので、host: dbとコンテナ名を入力したり、redis://redis:6379で他のコンテナにアクセス出来ました。わざわざipアドレスとか気にしなくていいのは楽ですね!!

実行してみよう!

これで設定は完了です。

$ docker-compose build
... # これで設定したpostgresやrubyやら色々なイメージとかを取ってくる
... # 何もエラーが無ければ、環境構築完了なので、RailsアプリのDB設定を行います
$ docker-compose run web rake db:create
$ docker-compose run web rake db:migrate
... # docker-compose run <サービス名> <コマンド>
... # 上の構文でサービス名のコンテナ内でコマンドを実行できる
$ docker-compose up

これで全てのコンテナが起動して、以下のようなログが出力されると思います。

web_1    | => Booting Puma
web_1    | => Rails 5.0.1.rc2 application starting in development on http://0.0.0.0:3000
web_1    | => Run `rails server -h` for more startup options
web_1    | Puma starting in single mode...
web_1    | * Version 3.6.2 (ruby 2.3.3-p222), codename: Sleepy Sunday Serenity
web_1    | * Min threads: 5, max threads: 5
web_1    | * Environment: development
web_1    | * Listening on tcp://0.0.0.0:3000
web_1    | Use Ctrl-C to stop

ログの通り、http://0.0.0.0:3000にアクセスすれば、ちゃんと動いているのが確認できました!!これにて完了...ではないんです実は(´・ω・`)すいません。

このままだとbuildするたびにデータが消えてしまう!!

このままだと、RedisやPostgresのデータベースを再起動するたびに中に入っていたデータが消えてしまいます。なので、Dockerの標準のAPIを用いてvolumesで設定を行います。今回はpostgresとredisのデータを永続化してほしいので、その設定を行います。

docker-compose.yml
version: '2'
services:
  db:
    image: postgres
    ports:
      - "5432"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
  redis:
    image: redis:latest
    ports:
      - 6379:6379
    volumes:
      - ./data/redis:/data
    command: redis-server --appendonly yes

DBコンテナとRedisコンテナにvolumesとRedisコンテナにはcommandを付与しました。volumesはオプションとしてホスト側のパスとコンテナ側のパスを指定して、ホスト側のそのパスにコンテナ側のパスをマウントします。こうすることで、コンテナが再起動した時に、自分のローカルの方にも保存されたデータがコンテナ側にも同期されて、それを読み込んだ状態からスタートする...みたいな感じになるので、データを永続化できます。

volumes: <ホストのパス>:<コンテナのパス>

redisは初期起動時にコマンドを変更して--appendonly yesというオプションをつけています。これをつけないとデータが生成されないらしいので、気をつけてください。
redis/dataに、postgres/var/lib/postgresql/dataにデータをマウントすればいいので、以上のように編集すれば、完了です!

ハマった事一覧

A server is already running

Dockerの環境が出来て,docker-compose buildも出来て、いざdocker-compose upじゃあ~!ってなったら顔を出したエラー。

web_1    | => Booting Puma
web_1    | => Rails 5.0.1.rc2 application starting in development on http://0.0.0.0:3000
web_1    | => Run `rails server -h` for more startup options
web_1    | A server is already running. Check /myapp/tmp/pids/server.pid.
web_1    | Exiting

すでにサーバーが立っているよという。でも、pa aux | grep 3000で探してみても、docker ps -a | grep 3000で探してみても別にプロセスは立っていなさそう。。
これの原因はRailsのWEBサーバが正常に終了出来ていないことによるそうで、本来ならpidはWEBサーバを起動する時にtmp/pids/server.pidに書き込まれて、終了する時に削除されるはずだけれど、そのファイルの中に何か書かれているがために、WEBサーバが起動中であると誤認されていることによるエラーのよう。

rm /tmp/pids/server.pidでファイルを消せばサーバを起動できました。server.pidはなければ、生成されるので消しても大丈夫です。

Bundler::GemRequireError: There was an error while trying to load the gem 'uglifier'.

Dockerfileを実行時のbundle installでこんなエラーが発生しました。ググったら、node.jsが入っていれば大丈夫って書いてあったので、Dockerfileapt-get install nodejsを追記したら直りました。これは上では対応済みです!

dockerのTips

エラーが起きたらログを見よう

docker-compose logsで実行中のコンテナ内のログが見れます。サービス名を指定したい場合は、docker-compose logs dbとすれば、DBコンテナ内のログが見れます。

不要なプロセスとイメージを削除する

いま起動中のコンテナはdocker imagesコマンドで、実行中のプロセスはdocker psコマンドで分かります。しかし、起動していなかったり、すでに必要のないプロセスを削除しないと以外と容量を食ってしまうので、定期的に不要なプロセスやコンテナを削除するといいでしょう。

ステータスがexitedのプロセスを削除して、イメージ名がとなっているコンテナを削除するワンライナーです。

docker ps -aq -f "status=exited" | xargs docker rm -v && docker images -q -f "dangling=true" | xargs docker rmi

docker-compose コマンドを短くする

基本的に今まで普通に実行していたrake db:migrateなどのコマンドは今回構成したDockerコンテナ上だとdocker-compose run web rake db:migrateというようにかなり長くなってしまいます。なので、.zshrcや.bashrcに以下のように記述してエイリアス化しましょう。

alias fig='docker-compose'

さいごに

かなり長文になってしまい申し訳ありません...。自分なりに後から読み返しても納得できるように詳しく書いたつもりなので、もし間違っていたら訂正リクエスト等していただけると嬉しいです!

dockerの公式ドキュメントはかなり充実しているので、迷ったらまずそこを見るのがいいと思います。といって、全て英語なので、もしいきなりは厳しいという方がいましたら、日本語訳のドキュメントもあるのでぜひ一度は読んでみてください!

あと、書いていて完全にOthloTechのAdventCalendarということを忘れていましたが、OthloTechは来年も東海のIT界隈を盛り上げるために楽しく活動していきますので、応援していただけると嬉しいです\(^o^)/

(」゚ロ゚)」<<< { A Happy New Year ♬♬ ♪}

hny.jpg

参考リンク

この投稿は OthloTech Advent Calendar 201625日目の記事です。
  • この記事は以下の記事からリンクされています
  • Dockerの使い方からリンク