Railsアプリ開発のためのDocker/Kubernetes入門2 Docker Compose/Dockerfile編

RailsアプリケーションをKubernetes(以後、k8s)で運用できるようにするための手順を書きます。 この記事はシリーズ連載記事の第二回です。

今回は下記について書きます。

  • 最小限のDocker Compose入門
  • Docker Composeを使った各種ミドルウェアのインストールと管理
  • RailsアプリケーションのDockerイメージの作り方
  • Docker Composeによるローカルプレビュー環境の構築

サンプルアプリケーション

簡単なRailsアプリを例に、Docker Composeの使い方やDockerfileの書き方を説明します。 このサンプルアプリrails-k8s-demoappのコードは下記のリポジトリに置いています。

https://github.com/kwhrtsk/rails-k8s-demoapp

rails-k8s-demoappは「フォームでメッセージを送信すると画面上のリストに追加して表示する」だけの小さなアプリです。

仕様は極小ですが、できるだけ一般的なRailsアプリの構成に近くなるように下記の要件を満たすものにしています。

  • ミドルウェアとしてMySQLとRedis(SidekiqとActionCableのため)を使用
  • ActiveJobを使用: Pumaの他にSidekiqプロセスの起動が必要なケースを想定
    • 新しいメッセージが追加されると、3秒後に逆順の文字列をさらにメッセージとして追加するようなジョブをSidekiqで実行します。
  • ActionCableを使用: websocketが必要なケースを想定
    • 新しいメッセージが追加された時にActionCableでブラウザに通知を行いページリロードさせます。
  • Webpackerを使用: アセットのビルドにnodeとyarnが必要なケースを想定
    • フロントエンドのコードはTypeScriptで書いています。
    • webpackでBootstrap v4を組み込んでいます。

下記のコマンドで動作を確認することができます。RubyやRailsの開発環境は必要ありません。 gitとDockerがインストールされていれば動きます。

$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git
$ cd rails-k8s-demoapp
$ docker-compose -f docker-compose-preview.yml up -d
$ open http://localhost:3000/

docker-composeコマンドの使い方やサンプルアプリケーションで使っているdocker-compose.ymlDockerfileについては順に説明します。

最小限のDocker Compose入門

Docker Composeとは

複数のコンテナをYAML形式の構成ファイルで一括管理するツールです。

  • この構成ファイルを Composeファイル と呼びます。
  • 通常、ファイル名はdocker-compose.ymlです。

Railsアプリ開発において、Docker Composeには大きく二つの用途があります。

  1. 開発環境におけるMySQLやRedisなどのミドルウェアのインストールと実行管理
  2. 完成したRailsアプリを(特に開発者以外が)簡単に手元で実行するための仕組み

以降、それぞれの詳細について説明します。

Docker Composeを使った各種ミドルウェアのインストールと実行管理

一般的なRailsアプリケーションでは、PostgreSQLやMySQLなどのRDBMSに加えて、 しばしばRedisやElasticsearchなどのミドルウェアを使用します。 従来、macOS上でRailsアプリの開発を行う場合にはこれらのミドルウェアのmacOS版をインストールし、 サービスとして起動しておく必要があったため、READMEには長々とセットアップの手順を書いたりしていました。

一方、先に挙げたようなメジャーなプロダクトはいずれも公式のDockerイメージが存在するため、 適切に書かれた docker-compose.yml ファイルさえあれば、 docker-compose up コマンドを一つ実行するだけで必要なミドルウェアのイメージの取得からコンテナの起動まですべて自動で行うことができます。

また、コンテナとしてミドルウェアを起動する際には個別にバージョンやデータの保存場所を指定できるため、 導入の手順が簡単になるだけでなく下記のようなメリットもあります。

  • 複数の開発プロジェクトを並行して進める際に、ミドルウェアの細かいバージョンや内部データを完全に独立して管理できる。
  • 古いバージョンのミドルウェアを要求するようなプロジェクトの開発環境を維持しやすい。

Composeファイル(docker-compose.yml)のサンプル

前述のサンプルアプリケーションではMySQLとRedisを使います。 この2つを起動するためのdocker-compose.ymlは下記のような内容です。 通常、このファイルはプロジェクトのルートディレクトリに置きます。

version: "3"
services:
  mysql:
    image: mysql:5.7.21
    environment:
      - MYSQL_ROOT_PASSWORD=$MYSQL_PASSWORD
    ports:
      - 3306:3306
    volumes:
      - ./tmp/mysql:/var/lib/mysql
  redis:
    image: redis:4.0.9
    ports:
      - 6379:6379
    volumes:
      - ./tmp/redis:/data
    command: redis-server --appendonly yes

servicesの下にコンテナ(Docker Comoposeの文脈ではサービス)の定義を書いていきます。

mysqlについては、前回Docker編のdocker container run(mysql編) に出てきた docker container runコマンドとやっていることはほぼ同じです。

  • environment: 環境変数を追加してコンテナを起動します。
    • .envrcに書かれている環境変数 MYSQL_PASSWORDをrootユーザのパスワードとして設定するため、MYSQL_ROOT_PASSWORDの値として設定しています。
    • ローカルの開発環境で作業を行う際には、direnvなどのツールで.envrcの内容をシェルに設定するという想定です。
    • MYSQL_ALLOW_EMPTY_PASSWORDyesを設定するとrootのパスワードを空にすることもできます。
  • ports: コンテナのポート(右側)をホスト側のポート(左側)にマッピングします。
  • volumes: ホスト側のパス(左側)をコンテナ上のパス(右側)にマッピングします。docker container run-vオプションとは異なり、ホスト側の相対パスで指定できます。

rediscommandには、データをストレージに永続化するためのオプションを指定しています。

Composeファイルには多数の機能があります。詳細については下記のリファレンスを参照してください。

mysqlredisのイメージに指定できる環境変数やコマンドのオプションについては、公式リポジトリのドキュメントを参照してください。

docker-compose up: 各種ミドルウェアのインストールと起動

まず、.envrcに書かれた環境変数を設定してください。

$ source .envrc

# direnvを使っている場合は下記でも可
$ direnv allow

下記のコマンドを実行すると、Docker Hubからmysqlredisのイメージを取得してコンテナが起動します。 (-dはバックグラウンドで起動するという意味です)

-fで任意のComposeファイルを指定できますが、省略するとカレントディレクトリのdocker-compose.ymlが参照されます。

$ docker-compose up -d
Creating network "railsk8sdemoapp_default" with the default driver
Pulling mysql (mysql:5.7.21)...
5.7.21: Pulling from library/mysql
2a72cbf407d6: Pull complete
38680a9b47a8: Pull complete
4c732aa0eb1b: Pull complete
c5317a34eddd: Pull complete
f92be680366c: Pull complete
e8ecd8bec5ab: Pull complete
2a650284a6a8: Pull complete
5b5108d08c6d: Pull complete
beaff1261757: Pull complete
c1a55c6375b5: Pull complete
8181cde51c65: Pull complete
Digest: sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28c
Status: Downloaded newer image for mysql:5.7.21
Pulling redis (redis:4.0.9)...
4.0.9: Pulling from library/redis
b0568b191983: Pull complete
6637dc5b29fe: Pull complete
7b4314315f15: Pull complete
2fd86759b5ff: Pull complete
0f04862b5a3b: Pull complete
2db0056aa977: Pull complete
Digest: sha256:6b9f935e89af002225c0dcdadf1fd74245b4cc1e3e91222f7e4769c236cf80d4
Status: Downloaded newer image for redis:4.0.9
Creating railsk8sdemoapp_redis_1 ... done
Creating railsk8sdemoapp_mysql_1 ... done

2回目以降は取得済みのイメージを使用するため、起動は少し早くなります。

MySQLとRedisが起動した後であれば、下記のように開発環境用の各種プロセスを起動できます。

$ gem install foreman
$ bundle install --path=vendor/bundle
$ yarn install
$ ./bin/rails db:setup
$ foreman start -f Procfile

foremanでは、pumasidekiqwebpack-dev-serverの三つを起動します。(ruby, node, yarnが必要です)

# Procfile
web: ./bin/rails s -p 3000
worker: ./bin/sidekiq
client: ./bin/webpack-dev-server

docker-compose ps: コンテナの一覧を取得する

docker-compose.ymlに定義されたサービスのコンテナ一覧を表示します。

$ docker-compose ps
Name                        Command               State           Ports
-----------------------------------------------------------------------------------------
railsk8sdemoapp_mysql_1   docker-entrypoint.sh mysqld      Up      0.0.0.0:3306->3306/tcp
railsk8sdemoapp_redis_1   docker-entrypoint.sh redis ...   Up      0.0.0.0:6379->6379/tcp

コンテナの名前は、${プレフィクス}_${サービス名}_${連番}になります。 プレフィクスはカレントディレクトリから-_を除いた文字列になります。 連番がついているのは一つのサービスに複数のコンテナが起動し得るためです。 (今回は紹介しませんが、docker-compose scaleコマンドでサービスごとのコンテナの数を増減させることができます)

docker-compose down: コンテナの削除

下記のコマンドを実行すると、docker-compose.ymlに書かれたすべてのサービスのコンテナを停止した後、削除します。 -vオプションは関連するデータボリュームも削除するという意味です。(ホスト側のボリュームをマウントしている場合は残ります)

$ docker-compose down -v
Stopping railsk8sdemoapp_mysql_1 ... done
Stopping railsk8sdemoapp_redis_1 ... done
Removing railsk8sdemoapp_mysql_1 ... done
Removing railsk8sdemoapp_redis_1 ... done
Removing network railsk8sdemoapp_default

最小限のDockerfile入門

RailsアプリのDockerイメージを作る方法を説明します。

Dockerイメージを作るには、まずDockerfileにイメージの作り方を記述します。

サンプルアプリ rails-k8s-demoapp に同梱している下記のようなDockerfileを例に説明します。

### image for build
FROM ruby:2.5.1-alpine AS build-env

ARG RAILS_ROOT=/app
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="libxml2-dev libxslt-dev mysql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata yaml"

ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

WORKDIR $RAILS_ROOT

# install packages
RUN apk update \
 && apk upgrade \
 && apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES

# install rubygem
COPY Gemfile Gemfile.lock $RAILS_ROOT/
RUN bundle install -j4 --path=vendor/bundle

# install npm
COPY package.json yarn.lock $RAILS_ROOT/
RUN yarn install

# build assets
COPY . $RAILS_ROOT
RUN bundle exec rake webpacker:compile
RUN bundle exec rake assets:precompile

### image for execution
FROM ruby:2.5.1-alpine
LABEL maintainer 'Kawahara Taisuke <[email protected]>'

ARG RAILS_ROOT=/app
ARG PACKAGES="tzdata yaml mariadb-client-libs bash"

ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

WORKDIR $RAILS_ROOT

# install packages
RUN apk update \
 && apk upgrade \
 && apk add --update --no-cache $PACKAGES

COPY --from=build-env $RAILS_ROOT $RAILS_ROOT

Dockerfileでは、上記の例のようにFROMRUNなどのコマンドを一行に一つ書きます。各コマンドの詳細は下記のリファレンスを参照してください。 docker image build コマンドでイメージの作成を行うと、Dockerfile に書かれたコマンドが上から順に実行されます。

このDockerfilerails-k8s-demoappのイメージをビルドするには下記のようにします。

$ git clone https://github.com/kwhrtsk/rails-k8s-demoapp.git
$ cd rails-k8s-demoapp
$ docker image build . -t demoapp:latest

.はビルドコンテキストです。通常、Dockerfileが置いてあるパスを指定します。 -tはイメージの名前です。

これでRailsアプリのイメージができました。下記のコマンドでコンテナを起動できます。

$ docker container run -it --rm demoapp:latest ls
Gemfile                     lib
Gemfile.lock                log
Procfile                    node_modules
README.md                   package.json
Rakefile                    public
app                         spec
bin                         storage
config                      test
config.ru                   tmp
db                          tsconfig.json
docker-compose-preview.yml  vendor
docker-compose.yml          yarn.lock
k8s

この例ではlsコマンドを実行してコンテナのファイルを表示しています。 上記のようにアプリケーションルートディレクトリの中身が表示されるはずです。

次にDockerfileの中身を順に解説していきます。

FROM

FROMコマンドではベースイメージを指定します。サンプルのDockerfileでは2回出てきますが、このケースでは2つイメージを作っています。

FROM ruby:2.5.1-alpine AS build-env
# ...
FROM ruby:2.5.1-alpine
# ...
COPY --from=build-env $RAILS_ROOT $RAILS_ROOT

前半がgemやnpmのC拡張やjsやcssなどのアセットの ビルド用イメージ で、後半がアプリケーションとして運用する 実行用イメージ です。 後半では前半のビルドの結果を単にコピーしています。このように2段階に分けてイメージを作成している理由は、イメージのサイズを小さくするためです。 詳細は レイヤについて で説明します。

最終的なイメージの成果物は後者の 実行用イメージ です。 前者は削除しても構いませんが、残しておくと2回目以降のビルドが差分で実行されるため速くなります(これについても後述)。

次にベースイメージのruby:2.5.1-alpineについて説明します。 Docker Hubのrubyリポジトリには大まかに3系統のタグがあります。

  • ruby:<version>: Debian stretchベース
  • ruby:slim: Debian stretchベースだがインストールされたパッケージが少ない
  • ruby:alpine: Alpine Linuxベース

今回はイメージサイズが最も小さいruby:2.5.1-alpineを使います。

% docker images
ruby                2.5.1-alpine        b620ae34414c        9 days ago           55.5MB
ruby                2.5.1-slim          85b814a932e6        9 days ago           172MB
ruby                2.5.1               1624ebb80e3e        9 days ago           863MB

また、Alpine LinuxのパッケージマネージャであるapkはDebianのaptと比べてNode.jsやYARNのインストールやキャッシュの制御がより簡単というメリットもあります。

ENV, ARG

ENVARGはどちらもコンテナに環境変数を設定するコマンドですが、 ARGで指定した環境変数はイメージのビルド時にだけ設定され、 作成済みのイメージをコンテナ化した際には残っていないという特徴があります。 また、docker image buildコマンドの--build-argdocker-compose.ymlargsオプションで上書きすることができます。 例えば、プロキシ環境下でイメージをビルドする際にHTTP_PROXYのような環境変数を指定する際には、ENVではなくARGを使うのが望ましいです。

このサンプルではインストールするパッケージの名前などをARGで設定しています。 また、ENVでは環境変数 RAILS_ENVproduction に設定しています。

### image for build
# ...
# アプリケーションのインストール先
ARG RAILS_ROOT=/app
# gemやnpmのC拡張やjs, cssなどのアセットのビルドに必要なパッケージ
ARG BUILD_PACKAGES="build-base curl-dev git"
ARG DEV_PACKAGES="libxml2-dev libxslt-dev mysql-dev yaml-dev zlib-dev nodejs yarn"
ARG RUBY_PACKAGES="tzdata yaml"

ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

### image for execution
# ...
# Railsアプリの実行に必要なパッケージ
ARG PACKAGES="tzdata yaml mariadb-client-libs bash"

ENV RAILS_ENV=production
ENV BUNDLE_APP_CONFIG="$RAILS_ROOT/.bundle"

BUNDLE_APP_CONFIGrubyイメージがもともと持っている環境変数です。 このDockerfileでは、ビルド済みのbundleディレクトリを丸ごと実行イメージにコピーしたいので、 gemのインストール先を${RAILS_ROOT}/vendor/bundleに指定しています。 このようにインストール先を変更した場合には BUNDLE_APP_CONFIG を上記のように上書きしないとbundle execが正常に動作しません。

ただし、この挙動は紛らわしいという指摘もあるため、将来の更新で修正されるかもしれません。

introduction of BUNDLE_APP_CONFIG leads to unexpected behavior for ‘statically’ bundled apps Issue #129 · docker-library/ruby

WORKDIR

WORKDIRはこのイメージから作成したコンテナ上でコマンドを実行するときのカレントディレクトリです。 この例では /app をRailsアプリのルートディレクトリとして指定しているので、WORKDIRも同じパスにしています。

ARG RAILS_ROOT=/app
WORKDIR $RAILS_ROOT

また、RUNCOPYなどのDockerfileのコマンドもWORKDIRで指定したパスで実行されます。

RUN, COPY

RUNはコマンドを実行します。主にパッケージのインストールやアプリケーションのビルドなどを行います。

COPYはホスト側のファイルをコンテナ側に複製します。 この時、.dockerignoreファイルで指定されたファイルは複製されません。 パスワードやクレデンシャルのような秘匿値を書いたファイルは忘れずに.dockerignoreに追加してください。 また、.dockerignoreに一致したファイルはイメージのビルド時にdockerdへ転送されなくなるため、 .gitnode_modulesなどビルド時に不要でかつサイズやファイル数が大きいディレクトリも指定するのがセオリーです。

今回ベースイメージにしているのはruby:2.5.1-alpineというイメージですが、これはAlpine Linuxというディストリビューションをベースにしています。 Alpine Linuxではapkというパッケージマネージャを使います。使用可能なパッケージを下記のサイトで検索できます。 Alpine Linux packages

RUN apk update \
 && apk upgrade \
 && apk add --update --no-cache $BUILD_PACKAGES $DEV_PACKAGES $RUBY_PACKAGES

レイヤについて

Dockerイメージのデータはレイヤと呼ばれる単位で記録されており、 DockerfileRUNCOPYなどのコマンドは実行するたびに新しいレイヤに結果が記録されます。 またイメージのビルドや送受信はレイヤ単位でキャッシュされて差分実行されるため、 適切な単位でレイヤを分割しなければビルドやデプロイに無駄な時間がかかるようになります。 特に、一度追加したファイルは別のレイヤで削除したとしても以前のレイヤに残り続けるため、 イメージ全体のファイルサイズは減らない点に注意が必要です。

一般的に、Dockerfileではイメージのファイルサイズを減らしたりビルドやデプロイの速度を上げるために次のような工夫をします。

  • GemfileGemfile.lockのコピーとbundle installの実行はアプリケーションコードのCOPYより前で個別に行う。

gemの追加は比較的頻度の少ない作業なので、こうしておくとbundle installの頻度を減らしてビルドを高速化できます。 package.jsonyarn.lockのコピー、yarnの実行についても同様です。

  • ビルド用のイメージと実行用のイメージを分けて、実行時に必要なファイルだけをビルド用のイメージから実行用のイメージにコピーする。

このようにすることでビルドに必要なパッケージが含まれるレイヤを丸ごと削除できます。

「1つのRUNコマンドでパッケージのインストール、ビルド、パッケージやキャッシュの削除を行う」ことでも同じことを実現できますが、 パッケージのインストールとビルドのレイヤが同じになるので、アプリのコードを更新しただけでもビルド時にはパッケージのインストールからやり直しになり、余分に時間がかかるようになります。 COPYコマンドの--fromオプションは比較的最近追加された機能なので、 古いドキュメントにはよくこのようなやり方が書いてあります (古いランタイムに配慮して止むを得ずこのような実装になっているケースもあるかもしれません)。

docker-composeコマンドによるローカルプレビュー環境

RailsアプリのDockerイメージを作れるようになったので、 次は「Ruby/Railsの開発環境がなくても、DockerさえあればRailsアプリのイメージを作成して起動できるようにする」ための Composeファイルを用意します。

ローカルでの開発用に docker-compose.yml がすでにあるので、docker-compose-preview.yml というファイル名で用意します。

version: "3"
services:
  puma:
    image: demoapp
    build:
      context: .
    env_file:
      - .dockerenv/rails
    ports:
      - 3000:3000
    depends_on:
      - mysql
      - redis
    command: ./bin/setup-db-and-start-puma

  sidekiq:
    image: demoapp
    env_file:
      - .dockerenv/rails
    depends_on:
      - puma
    command: ./bin/start-sidekiq

  mysql:
    image: mysql:5.7.21
    env_file:
      - .dockerenv/mysql

  redis:
    image: redis:4.0.9
    command: redis-server --appendonly yes

コードをチェックアウトした後、下記のコマンドを実行するだけでRailsアプリが http://localhost:3000/ に起動するようになります。

$ docker-compose -f docker-compose-preview.yml up -d

ログを見たい場合は-dオプションを外すか、下記のようにしてください。

$ docker-compose -f docker-compose-preview.yml logs -f

後始末は下記のようにします。-vを外すとデータボリュームが残ってしまうので注意してください。

$ docker-compose -f docker-compose-preview.yml down -v

このYAMLファイルのポイントは下記の通り。

  • mysqlredisにはpumasidekiqのコンテナから接続できれば良いので、ports(ホスト側へのポートマッピング)エントリを書きません。
    • コンテナ同士は特に設定をしなくても互いのポートにアクセスできます。
  • pumaにはbuildエントリを追加して、docker-compose up実行時にイメージをビルドするようにしています。
    • 2回目以降、イメージを強制的にビルドする際には --build オプションをつける必要があります。
  • pumadepends_onmysqlを指定して、mysqlコンテナの起動後にrake db:setupが実行されるようにしています。
    • ただしこれだけだと不十分なのでncmysqlの3306番ポートを確認して起動するまでループします。後述。
  • pumasidekiqmysqlにはenv_fileを指定し、共通の環境変数をファイルで一括指定します。後述。

pumaとsidekiqのcommandでは、./bin/setup-db-start-puma, ./bin/start-sidekiqというコマンドをそれぞれ指定しています。 これは下記のような内容です。

# ./bin/setup-db-and-start-puma

cd $(dirname $0)/..

trap "pkill -P $$" EXIT

./bin/wait-for $MYSQL_HOST 3306
./bin/wait-for $REDIS_HOST 6379
./bin/rails db:setup_if_not_yet
./bin/pumactl start
# ./bin/start-sidekiq

cd $(dirname $0)/..

trap "pkill -P $$" EXIT

./bin/wait-for $MYSQL_HOST 3306
./bin/wait-for $REDIS_HOST 6379
./bin/sidekiq -t ${SIDEKIQ_TIMEOUT:-8}

pumaやsidekiqの起動をシェルスクリプトでラップする場合、ラッパ側のシェルがTERMやINTなどのシグナルで停止すると 子プロセスのpumaやsidekiqが起動したまま残ってしまいます。 上記のtrapはそれを防ぐための記述で、スクリプトの停止時に子プロセスへTERMシグナルを送るように指定しています。

sidekiqの-tオプションは実行中のジョブを強制的に停止するまでのタイムアウト値で、オプションを指定しない場合のデフォルト値は8秒です。 このスクリプトでは環境変数SIDEKIQ_TIMEOUTでこの値を変更できるようにしています。 環境変数が存在しない場合はデフォルト値と同じ8秒が指定されます。

./bin/wait-for は下記のような内容で、 パラメータで指定したホストのポートをncコマンドでチェックして接続可能になるまで待機するだけのスクリプトです。

# ./bin/wait-for

HOST=$1
PORT=$2

while :
do
  nc -w 1 -z $HOST $PORT
  if [[ $? = 0 ]]; then
    break;
  fi
  sleep 1
done

db:setup_if_not_yetは下記のようなRakeタスクで、まだrake db:setupを実行したことがなさそうな時だけ実行します。

# lib/tasks/db.rake
namespace :db do
  task setup_if_not_yet: [:environment] do
    begin
      ActiveRecord::Base.connection
    rescue ActiveRecord::NoDatabaseError
      # database not exists
      Rake::Task["db:setup"].invoke
      exit 0
    else
      if !ActiveRecord::SchemaMigration.table_exists?
        # database exists but tables not exists
        Rake::Task["db:setup"].invoke
        exit 0
      end
    end
  end
end

.dockerenv/railspumasidekiqに共通の環境変数の設定ファイルです。

RAILS_SERVE_STATIC_FILES=true
RAILS_LOG_TO_STDOUT=true
SIDEKIQ_TIMEOUT=60
SECRET_KEY_BASE=123
MYSQL_HOST=mysql
MYSQL_USER=demoapp
MYSQL_PASSWORD=secret
MYSQL_DATABASE=demoapp_production
REDIS_HOST=redis
REDIS_URL=redis://redis:6379/1

.dockerenv/mysqlmysql用の環境変数の設定ファイルです。デーベース名、ユーザ名、パスワードを書いています。

MYSQL_USER=demoapp
MYSQL_PASSWORD=secret
MYSQL_DATABASE=demoapp_production
MYSQL_ROOT_PASSWORD=topsecret

公式のmysqlイメージでは、rootユーザのパスワードの他、 コンテナの起動時に作成するデータベースとそのデータベースに アクセスできるユーザとパスワードも環境変数で指定できます。 .dockerenv/mysqlで設定している4つの環境変数はそのためのものです。

また、Railsアプリ側ではMySQLやRedisの接続先など実行環境に依存するようなパラメータを全て環境変数で 受け取れるようにしておく必要があります。

まずデータベースの設定ファイルでは、MYSQL_HOST, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORDで それぞれDBのホスト名、データベース名、ユーザ名、パスワードを受け取れるようにしておきます。 (話を簡単にするため、mysqlコンテナと環境変数の名前を合わせています)

# config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch("MYSQL_USER") { "root" } %>
  password: <%= ENV.fetch("MYSQL_PASSWORD") { "" } %>
  host: <%= ENV.fetch("MYSQL_HOST") { "0.0.0.0" } %>

development:
  <<: *default
  database: demoapp_development

test:
  <<: *default
  database: demoapp_test

production:
  <<: *default
  database: <%= ENV.fetch("MYSQL_DATABASE") { "demoapp_production" } %>

また、ActionCableの設定ファイルでは環境変数 REDIS_URL でRedisの接続先を指定できるようにしておきます。

# config/cable.yml
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>

test:
  adapter: async

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: demoapp_production

Sidekiqについてはもともと環境変数 REDIS_URL で接続先を切り替えられる仕様なので特に何もしなくて良いです。 別の方法で接続先を指定したい場合は config/initializers/sidekiq.rb というファイルを作って設定を追加します。 詳細は下記のドキュメントを参照してください。

Using Redis mperham/sidekiq Wiki

REDIS_HOSTは前述のbin/wait-forに渡すパラメータとして使っています。

SECRET_KEY_BASEはCookieに改ざん検知のためのHMACダイジェストをつけるときの秘密鍵を指定するための環境変数ですが、 Rails 5.2.0のデフォルトの動作ではCredentialsで暗号化されたファイル config/credentials.yml.enc に 書かれた値を鍵として使うようになっています。下記のコマンドでこのファイルの中身を確認できます。

% ./bin/rails credentials:show
# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: f758dcb894cf0fee1b68f7989ebd65f5fd0dbb3c366e61789c112836789dfda22def856b6419ae96447352bbdd8873b90ae0096cd61d9532775caca8c04e0783

ただ、この値は本番環境でも使う値になるので、今回のような不特定多数に配布するのが前提のプレビュー環境には使えません。 HMAC用の鍵はCredentials の値よりも環境変数SECRET_KEY_BASEの値が優先される実装になっているので、今回は環境変数を設定します。 また、Credentialsの秘密鍵(環境変数RAILS_MASTER_KEYまたはconfig/master.key)は設定しません。(デフォルトでは設定しなくてもエラーにはなりません。)

RAILS_SERVE_STATIC_FILESは、jsやcssなどのファイルをpumaが応答するかどうかを指定するための設定値です。 これはRails標準の仕組みで、実装は config/environments/production.rb を参照してください。 本番環境ではこのような静的なアセットファイルはnginxやCDNで配信するのが望ましいのですが、 今回は構成を簡単にするためにtrueにしておきます。

RAILS_LOG_TO_STDOUTは、production環境でRailsのログ出力先を標準入力に切り替えるための環境変数です。 これもRails標準の仕組みで、実装は config/environments/production.rb を参照してください。

こういった環境変数設定用のファイルに本番環境の値を書く場合には、 GitリポジトリやDockerイメージに含めないように注意してください。 .gitignore.dockerignore にそれらのファイル名を書いておくと意図せず混入させることを防ぐことができます。

本稿では、docker-compose-preview.ymlで構築するのはあくまでプレビュー用の環境であり、 本番環境はKubernetesで構築するという前提です。 Kubernetes上のアプリに環境変数を設定する際には全く別の方法を用いるため、 Composeファイルなどに書いた設定値は全て本番環境とは異なる値という想定なので、リポジトリにコミットしています。

また、env_fileで指定するファイルは、direnvで使う.envrcなどシェルに環境変数を設定するスクリプトとは 根本的に異なるものなので下記の点に注意してください。

  • exportはつけない。
  • コマンドは実行できない。
  • 値をクオートしてはいけない。(環境変数に'"を含む形で設定されてしまいます)

コンテナ間の通信について

コンテナにはそれぞれ固有のIPアドレスが割り当てられます。 docker container runコマンドで起動したコンテナ同士が互いのIPアドレスを知るためにはオプション指定が必要ですが、 docker-composeで起動したコンテナ同士は、互いのサービス名をホスト名として通信できるように自動的に設定されます。 また、ホスト側からコンテナに接続する際にはportsエントリでポートマッピングの設定を書く必要がありましたが、 docker-composeで起動したコンテナ同士はこういった設定なしで互いの全てのポートにアクセスできます。

MySQLの接続設定をもう一度掲載します。下記のようにMYSQL_HOST環境変数でホスト名を指定するようになっています。

# config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch("MYSQL_USER") { "root" } %>
  password: <%= ENV.fetch("MYSQL_PASSWORD") { "" } %>
  host: <%= ENV.fetch("MYSQL_HOST") { "0.0.0.0" } %>

# 省略

production:
  <<: *default
  database: <%= ENV.fetch("MYSQL_DATABASE") { "demoapp_production" } %>

.dockerenv/railsでは下記のようにmysqlという文字列をMYSQL_HOST環境変数に設定していました。

MYSQL_HOST=mysql

このmysqlというのは、docker-compose-preview.ymlのサービス名を指しています。

version: "3"
services:
  # 省略
  mysql: # <- これがサービス名
    image: mysql:5.7.21
  # 省略

docker container runコマンドに--linkオプションをつけると docker-composeが自動的に準備してくれていたようなことを手動で行うこともできますが、 Railsアプリ開発という主題においては必要になる場面はほぼ無いと思いますので、詳細は割愛します。

ローカル開発環境のRailsアプリをDocker化するかどうか

少なくともホストOSがmacOSなのであれば、 ローカルの開発環境においてはRailsアプリまでDockerコンテナの上で動かす必要はないと筆者は考えています。

  • RailsアプリがmacOSで動いてLinux(コンテナ)だと動かないケースは多くない。
  • 多くの場合、macOS上でRailsアプリの開発環境を用意するのはそれほど面倒ではない。
  • ./bin/rails g./bin/rails cなどちょっとしたコマンドが全てdockerコマンド経由になるのは煩雑すぎる。
  • springの対応が面倒

以上の理由で、私の主観ではメリットをデメリットが上回っていると感じるので、開発環境においては下記のような構成をとっています。

  • Railsプロセス自体はホスト側(macOS/Linux)で直接起動する。
  • Railsプロセスが接続するMySQLやRedisなどのミドルウェアはdocker-compsoeで起動する。

参考情報

Docker Composeのより詳細な使い方は下記のドキュメントを参照してください。

Dockerfileのより詳細な仕様については下記のドキュメントを参照してください。

おすすめの書籍は「プログラマのためのDocker教科書」です。2018/4/11に第2版が出ました。

前回のDocker編で扱った範囲も含めて、 本ドキュメントでは紹介しなかったコマンドやDockerfile/Composeファイルの機能が一通り紹介されています。 また、コンテナ技術の概要や使いどころ、プライベートレジストリやイメージの公開方法などDocker周辺を広く浅く解説してあり、 入門には良い書籍だと思います。後半ではKubernetesにも軽く触れてあります。

次回は Kubernetes入門編 です。