コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.2 -Hello, Rails on Docker-


はじめに

第二回目の今回は、Ruby on RailsをDockerコンテナで起動させるHello worldをやっていきます。

今日のゴール

  • Ruby on Rails on Docker で Hello world する

では早速、Ruby on Rails on Docker で Hello world していきましょう!

Hello, Ruby on Rails on Docker

今回は↓の図のように、Docker 上に Rails アプリケーション用のコンテナと PostgreSQL (database) 用のコンテナを作っていきます。

まず、作業用のディレクトリを作っておきましょう。

$ mkdir Handson
$ cd Handson

今後、このHandsonディレクトリをホームディレクトリとして話を進めますので、特に指定がない場合、Handsonディレクトリでコマンドを叩いたり、Handsonディレクトリから見た相対パスでファイルを編集していると思ってください。

では早速、Ruby on Rails on Docker な環境を構築するために以下の4つのファイルを作成していきます。

  • Dockerfile: Rails アプリ用の Docker image の元となる設計図
  • Gemfile: Rails アプリに必要なgemを記載するファイル
  • Gemfile.lock: Gemfileによってインストールされたgemのバージョン情報などを管理するファイル
  • docker-compose.yml: 今回のアプリをコンテナ起動させるための Dcoker Compose ファイル

Dockerfile

Dockerfile
FROM ruby:2.6.5-alpine3.11

ENV HOME="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR $HOME

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
      gcc \
      g++ \
      less \
      libc-dev \
      libxml2-dev \
      linux-headers \
      make \
      nodejs \
      postgresql \
      postgresql-dev \
      tzdata \
      yarn && \
    apk add --virtual build-packs --no-cache \
      build-base \
      curl-dev

COPY Gemfile $HOME
COPY Gemfile.lock $HOME

RUN bundle install && \
    apk del build-packs

COPY . $HOME
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

Dockerfile は初めて見る人にとっては「なんだこれ?」なものな気がしますが、読んでみると意外とシンプルです。
頭に大文字で書かれているのが命令と呼ばれるものでコマンドみたいなものです。
今回の Dockerfile では以下の命令を利用しています。

  • FROM: ベースイメージを定義する命令です。ruby イメージを指定しているので、元々 Ruby を使用できるイメージの上に Rails を動かす環境を作っていきます。
  • ENV: 環境変数を定義する命令です。
  • WORKDIR: 作業ディレクトリを定義する命令です。ベースイメージ内に該当のディレクトリがない場合は、そのディレクトリを作成することもしてくれます。
  • COPY: ホストのファイルやディレクトリをイメージ内にコピーする命令です。
  • RUN: コマンドを実行する命令です。
  • EXPOSE: コンテナがリッスンするポートを宣言する命令です。 Rails ではデフォルトで 3000 番ポートを使用するので 3000 を指定してあげています。
  • CMD: ソフトウェアを実行するためのコマンドを定義する命令です。コンテナが起動する時に実行されるコマンドといった方がイメージ湧きやすいかもしれません。少し特別な書き方(["xxx", "xxx"]みたいな)をしますが、Rails アプリケーションを起動させるコマンドはrails server -b 0.0.0.0でして、それをCMDの記法で書いています。

あらかた命令と1行1行の内容について述べてしまいましたが、取りこぼしているところをキャッチアップ。

RUN宣言でapk ~と色々書いている9行目からの部分がありますが、apk addは Alpine linux でパッケージをインストールするコマンドです。
apk updateでパッケージリポジトリの最新のインデックス(インストールできる最新バージョンは何か)を取得してきて、apk upgradeですでにインストールしているパッケージで最新版にアップデートできるものをアップデートします。その後、apk addで Rails を起動するのに必要なパッケージをインストールしていきます。--no-cacheオプションはキャッシュを残さないようにするためのオプションです。不要なキャッシュを残さないことでコンテナ自体を軽量に保つことができます。(コンテナは軽量に保っておいた方がダウンロードに時間がかからなかったり、ホストのボリュームを圧迫しないのでよいとされています。)--virtualオプションはそのインストールしたパッケージ達を一つのグループとして名前づけしています。今回の例だとbuild-packsという名前をつけています。ここでインストールしたパッケージは Rails をビルドする上では必要なのですが起動させるためには不要なのであとでapk delで削除するために名前づけしています。

その後、COPY命令でGemfileGemfile.lockをイメージ内にコピーして、bundle installを実行してます。bundle installGemfileの内容に沿ってgemをインストールするコマンドです。Rails 自体もgemでインストールできるのでGemfilerailsを記入しておけばこのbundle installの際にインストールされます。Gemfile.lockはすでにインストール済のgemのバージョンなどを管理して無闇にバージョンアップさせないようにしてくれます。

最後にCOPY . $HOMEでローカルホストのファイルを一式イメージ内にコピーすることで Rails アプリケーションを起動させられる Docker image を作ることができます。

Gemfile

Gemfile
source 'https://rubygems.org'
gem 'rails', '~>6'

Gemfileはかなりシンプルで、インストールするgemのソースとrailsgem をインストールすることを定義しています。Rails は2020.02.02時点で最新バージョンが 6.0.2 なのでまぁメジャーバージョンとして 6 のものをインストールしてくださいというような指定の仕方をしています。
Rails アプリケーションでは最初rails newコマンドでアプリに必要なgemやファイルをインストール・生成するため、初期ではこれほどシンプルなGemfileがあるだけで構わないのです。

Gemfileでバージョンを指定する表現方法はいくつかあります。gemはGitHubなどで公開されていることが多くて大体 README でこう Gemfile に記載してくれと書かれていることが多いのであんまり気にすることはないかもしれませんが一応紹介。

  • gem 'rails', '6.0.0': 絶対 6.0.0 をインストール(バージョンを定義)
  • gem 'rails', >= 6.0.0': 6.0.0 より最新のものをインストール(最低バージョンを定義)
  • gem 'rails', >= 6.0.0', < 6.0.2: 6.0.0 以上 6.0.2 未満のバージョンをインストール(バージョンの範囲を定義)
  • gem 'rails', '~> 6.0.0': 6.0.X のバージョンをインストール(マイナーバージョンを定義)

Gemfile.lock

Gemfile.lockは最初にbundle installされるときに書き込まれるので、最初は空ファイルで問題ありません。

$ touch Gemfile.lock

touchコマンドはファイルの更新日時を現在時刻に更新するためのコマンドですが、ファイルが存在しない場合は空ファイルを生成してくれるのでGemfile.lockを生成するために使いました。

docker-compose.yml

docker-compose.yml
version: '3'

services:
  db:
    image: postgres:12.1-alpine
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./tmp/db:/var/lib/postgresql/data

  web:
    build: .
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db

まず、サービスとしてdbwebの二つがあることがわかるかと思います。dbは文字通りデータベース用のコンテナ(サービス)、webは Rails アプリケーションを動作させるコンテナ(サービス)です。dbコンテナではimagepostgres:12.1-alpineを指定しています。
docker-compose.ymlについては前回もお話したので、前回お話していない項目を中心にお話します。

  • environment: コンテナを起動する時に環境変数としてセットする。この場合、TZ(タイムゾーン)を東京にしてる。
  • volumes: コンテナからホストのディレクトリをマウントしている。左がホストのパス、右がコンテナ内のパス。ホストのパスはこのdocker-compose.ymlの場所からの相対パスで書いてます。こう書くことで簡単にいえば、ホストのパスとコンテナのパスを同期しているイメージになり、ホストでファイルを編集すればコンテナ内にも反映され、コンテナ内でファイルが編集されればホストのファイルにも反映されるという関係を気づけます。コンテナはステートレスなので一度コンテナを削除して新しくコンテナを起動させた場合、最初のコンテナ(削除したコンテナ)内で変更されたデータは全てなかったことになってしまうのですが、ホストのディレクトリに同期しておくことで次に起動するコンテナもそのディレクトリをマウントするのでデータが永続化されるようになります。
  • build: build: .docker-compose.ymlと同じディレクトリのDockerfileをbuildしたイメージを使ってコンテナを起動するようになります。
  • depends_on: コンテナの依存関係を定義します。今回の場合『webdbに依存している』と定義していることになりますが、これは次の2つが実現されます。
    • docker-compose upした時に、dbコンテナが起動してからwebコンテナを起動する
    • docker-compose up webwebコンテナを起動させようとした場合、dbコンテナも起動させる

Rails アプリケーションを新規作成

まずはrails newコマンドで新規に Rails アプリを作成します。

$ docker-compose run --rm --no-deps web rails new . -fGTd postgresql
...
Webpacker successfully installed 🎉🍰

いろいろな要素のあるコマンドですね。ちょっと順を追って説明します。
まずこのコマンドの大枠はdocker-compose run [options] <service name> <command>です。今回の例では、

  • [options]: --rm --no-deps
  • <service name>: web
  • <command>: rails new . -fGTd postgresql

となっています。docker-compose rundocker-compose.ymlの定義にそって対象のコンテナを立ち上げてその中でコマンドを実行し、実行後にコンテナを停止するコマンドです。つまり、webコンテナを立ち上げてrails new . -fGTd postgresqlwebコンテナ内で実行してくれます。
--rmオプションはコマンド実行後にコンテナを停止した後に削除までしてくれるオプションです。基本停止したコンテナは不要だと思うのでこのオプションをつけるのがよきかと思います。
--no-depsオプションはdocker-compose.ymldepends_onが定義されていたとしてもそれを無視してdocker-compose runを実行することができます。

次にwebコンテナで実行されるrails new . -fGTd postgresqlを見ていきましょう。
まずrails newは Rails アプリケーションを新規作成するためのコマンドです。.はアプリケーションを作成する場所を指していてカレントディレクトリ(コマンドが実行されたディレクトリ)を示しています。-fGTdはオプションなので一つずつ紐解きます。

  • -f: ファイルの上書きを強制する。Gemfileなどに上書きが走りますがいちいち Yes or No を聞かれないようにするためにつけています。
  • -G: Gitの初期設定をスキップします。Rails 6 からなんかこのオプションをつけないとまともにrails newできなかったのでつけてます。
  • -T: minitestという Rails でデフォルトでインストールされるテストフレームワークのインストールをスキップします。僕はRSpecというテストフレームワークをよく使っているのでこのオプションをつけて無駄にminitestがインストールされないようにしています。
  • -d: -d <database name>で利用するデータベースを指定します。今回はpostgresqlを指定。

Rails アプリケーションの新規作成ができたら一度イメージをビルドしておきましょう。

$ docker-compose build
...
Successfully tagged handson_web:latest

docker-compose buildコマンドはdocker-compose.ymlDockerfileからのビルドが必要なサービスのイメージビルドをすべて実行してくれます。今回はdbは DockerHub のイメージを使っているのでwebのみがビルドが必要なサービスとしてビルドされます。

DBの接続設定

ビルドが終わったら、Rails アプリケーションの DB 接続設定をコーディングしていきます。 DB の接続設定はconfig/database.ymlに記載します。 Rails では設定系のファイルはconfigディレクトリに格納されています。

config/database.yml
  ...
  default: &default
    adapter: postgresql
    encoding: unicode
+   host: db
+   username: postgres
+   password:
    pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  ...

hostusernamepasswordがデフォルトから新たに追加した項目です。
hostはDBのホスト名です。今 Rails アプリが稼働しているwebコンテナはdocker-compose.ymlが作る Docker ネットワークの中にいます。この中ではサービス名で名前解決してコンテナが相互に接続することができます。つまりhostとしてdbを設定することでdocker-compose.ymldbのサービス名で定義されたコンテナに接続できるようになるのです。
usernamepasswordは何の値なのでしょうか?これはdbコンテナのイメージで指定したpostgresのデフォルト値です。passwordは同じホストからのアクセスであれば省略が可能になっています。postgresイメージのusernamepasswordはそれぞれPOSTGRES_USERPOSTGRES_PASSWORDの環境変数で定義することもできます。

データベースの設定はここまでです。このデフォルトの設定値が開発環境(development)、テスト環境(test)の設定値として反映されるようになっています。

データベースを作成する

データベースの接続設定を定義したので、データベースを実際に作成していきます。
Rails ではデフォルトで本番環境(production)、開発環境(development)、テスト環境(test)の3つの環境(environment)が用意されています。特別に環境を指定しない場合、開発環境で挙動するようになっています。
データベースの作成はrails db:createコマンドを使いますが、このコマンドは開発環境とテスト環境用にデータベースを作成してくれます。本番環境用のデータベースを作成する場合は、RAILS_ENV=productionをオプションとしてつけます。
今回はまず開発環境向けにデータベースの作成を行いたいので、以下のコマンドを実行します。

$ docker-compose run --rm web rails db:create

Hello Ruby on Rails on Docker!!

ここまでで Hello world に必要な作業は全て完了しました。
コンテナを立ち上げて Hello world ページが表示されることを確認しましょう。

$ docker-compose up -d

Rails アプリは少し起動に時間がかかります。すぐにhttp://localhost:3000にアクセスしてもまだアプリケーションが起動していないこともありますので、その場合はdocker-compose logsコマンドを使ってアプリケーションの起動状態を確認してみましょう。

$ docker-compose logs -f

-fオプションはログの変化をリアルタイムでコンソールに表示するためのオプションです。Rails アプリが起動した場合

web_1  | => Booting Puma
web_1  | => Rails 6.0.2.1 application starting in development
web_1  | => Run `rails server --help` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 4.3.3 (ruby 2.7.0-p0), codename: Mysterious Traveller
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

というログが表示されます。この表示を確認したらCtrl+Cdocker-compose logsから抜け出しましょう。

では、http://localhost:3000にアクセスしてみましょう!

このようなページが表示されたでしょうか?このページが Rails アプリケーションの第一歩、つまり Hello world です。おめでとうございます!これでもう『Rails は Hello world までならやったことあります』と自慢することができます!

後片付け

ここまでできたら後片付けをしておきましょう。
このままではコンテナが起動しっぱなしになってしまうので、最後にコンテナを停止させておきます。

$ docker-compose down

これでコンテナを停止させたので、http://localhost:3000にアクセスしても先ほどのページは表示されなくなっていることでしょう。

まとめ

今回は、Dockerfiledocker-compose.ymlなどのファイルを作成し、Rails アプリケーションを稼働させられる Docker イメージ、Docker コンテナを作成してみました。
さらに、Rails アプリを新規作成して Hello world に成功しました!
まだまだアプリケーション開発のほんの入り口ですが、Docker を使って Web アプリを起動させることができただけでもかなり感動があると思いますし、ここまでさほど大変ではないことも感じてもらえたかなと思います。これこそが Rails や Docker の偉大なところですね。

次回は、scaffoldという Rails の便利機能を使って、サンプル Web アプリケーションを作ってみようと思います。このscaffoldで作成できるアプリケーションが Rails の基本的なアプリケーションの形になりまして、その中には Rails アプリケーションを語る上では避けられないRESTfulMVCの要素が詰まっていますので、その辺りも合わせて学んでいけるようにしようと思います。

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Next: コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.3 - Scaffold, RESTful, MVC - - Qiita

本日のソースコード

Reference

Other Hands-on Links