Rails on DockerのQuickstartをalpine linuxでやってみる


Abstract

Rails on DockerはDocker docsにサンプルアプリが載っています。
--> Quickstart: Compose and Rails | Docker Documentation

ただ、Dockerイメージはサイズを軽量化するのがよしとされており、そのためにalpine linuxを使うというのはよくある話のようです。ので、上記のQuickstartをalpineで実施するメモです。

Define the project

まず、Dockerfileを作ります。Quickstartでは以下の内容。

Dockerfile(Quickstart版)
FROM ruby:2.5
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

これをalpine化するには、ベースのイメージとしてalpineを選択すればOKです。
Dockerhubのrubyイメージから最新の物を探しましょう。最新・安定のruby-alpineイメージにはalpineタグがついています。
FROM ruby:alpineと宣言してもいいのですが、この場合はビルドのタイミングでバージョン変わっちゃう可能性あるのでその時のバージョンを指定するといいと思います。2019/11/01時点では2.6.5-alpine3.10

また、alpine linuxではapt-get installは使えず、apk addを使います。必要なパッケージもちょっと違います。そこら辺に気をつけて以下のようなDockerfileを作ります。

Dockerfile(Quickstartをそのままalpine化してみた)
FROM ruby:2.6.5-alpine3.11
RUN apk update && \
    apk upgrade && \
    apk add --no-cache linux-headers libxml2-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql && \
    apk add --virtual build-packages --no-cache build-base curl-dev
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
RUN apk del build-packages
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

alpine linuxは軽量のため、色々とRailsアプリを実行するのに足りないパッケージとかがありますのでそれらをapk addしています。
build-basecurl-devについては、--virtualオプションでbuild-packagesという名前をつけて、bundle install後にapk del build-packagesで削除してます。これらはRailsアプリのビルド時にのみ必要(実行時は不要)なパッケージなので削除することでDockerイメージを軽量に保つようにしてます。

さらに、Rails5.2以降はcredentialsで秘匿情報を管理するようになりますが、これを編集するためにはeditorのパッケージがインストールされている必要があります。のでvimを入れときます。
Rails6からはwebpackerが採用されているためyarnが必要です(QuickstartはRails5が対象でしたが、Rails6リリースされたのでそちらに合わせます)。これも追加します。
また、文字コードやタイムゾーン、インストールパッケージを環境変数として管理するともうちょっと可読性が上がりそうです。
WORKDIRは実はmkdirも兼ねるのでそこらへんも省略したい。
最終的に以下のようなDockerfileでいかがでしょうか?

Dockerfile(alpine化最終版)
FROM ruby:2.6.5-alpine3.11

ENV ROOT="/myapp"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
        gcc \
        g++ \
        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 ${ROOT}
COPY Gemfile.lock ${ROOT}

RUN bundle install
RUN apk del build-packs

COPY . ${ROOT}

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

続いてGemfileを作成します。現在のRailsの最新バージョンは6ですので、そこだけQuickstartの内容とは異なりますが、基本一緒です。

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

また、Quickstartと同様にGemfile.lockの空ファイルを作成しておきます。

$ touch Gemfile.lock

続いて、server.pid問題を解決するためのentrypoint.shを作成します。これもQuickstartの内容と一緒ですが、alpine linuxではbashではなくashが使われているところに違いがあります。(shebangが#!/bin/sh

entrypoint.sh
#!/bin/sh
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

最後にdocker-compose.ymlを記載します。ここもalpineを使っているのでbashではなくashを使います。
せっかくなので、postgresqlもalpineのイメージを使っています。postgres - Docker Hub
またpostgresqlのタイムゾーンをDockerfileと合わせておくのもよしです。

docker-compose.yml
version: '3'

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

  web:
    build: .
    command: ash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

ここまでで作成したファイルはGithubにupしておきました。

Build the project

ここまで作ったファイルは全てRailsアプリのルートディレクトリとなるディレクトリに格納してある前提です。
Railsアプリを新規作成するために、そのルートディレクトリで以下のコマンドを実行します。

$ docker-compose run --rm --no-deps web rails new . -fT -d postgresql

Quickstartと少し違うポイントはalpineだからとかでなく趣味ですスミマセン。

  • --rm:実行完了後コンテナが削除される。ゴミ掃除。
  • --no-deps:リンクしているサービス(今の場合はdb)を起動しない。docker-composeのオプションなので、この位置が正しいのではないだろうか...
  • -f--forceと同じです。ファイルをオーバーライドします。(Gemfile, Gemfile.lock)
  • -T:テスト系のファイル作成をスキップ。RSpecを使うことが多いので。
  • -d postgresql--database=postgresqlと同じです。

rails newが完了したらDockerイメージをビルドします。

$ docker-compose build

Connect the database

ここはまるパクリですね。

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

データベースが作成してコンテナを立ち上げます。

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

あ、コンテナをバックグラウンドで実行したい場合は-dオプションです。

$ docker-compose up -d

View the Rails welcome page!

ブラウザでhttp://localhost:3000にアクセスすればQuickstart同様、RailsのWelcome pageが表示されるはずです。

Stop the application

アプリのストップは以下のコマンドで。

$ docker-compose down

Hello world with scaffold

ここまでだとデータベースがちゃんと使えているのかHello worldできていないので、ちゃちゃっとscaffoldでそこまで確認します。確認のためだけなので、name属性をもつUserモデルで。

$ docker-compose run --rm web rails g scaffold user name:string

マイグレーションファイルが作成されるので、db:migrateを実行します。

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

さらにルートパスをusers#indexのページにしておきます。

rb;config/routes.rb
Rails.application.routes.draw do
  resources :users #scaffildで自動追加されている

  # 以下を追加
  root to: 'users#index'
end

これでコンテナを起動すると、ルートパスが以下の画面に変わっていて、scaffoldでCRUDできるようになっているはず。

docker-compose.ymlでpostgresqlのデータをtmp/dbとマウントしているのでコンテナを停止再起動したりしてもデータは消えません。

Conclusion

Rails on DockerのQuickstartをalpine linuxベースで実施してみました。
Dockerfileで多少の違いが、特にパッケージ系は違いがありますが、buildしてしまえば差は無いように感じます。
サイズの比較はしてないですが、いろいろな方の調査結果ではrubyruby-alpineではイメージサイズが数倍〜十数倍違ったりするらしいので、アップロードダウンロードやディスクにとってメリットがあるのでオススメです。

Reference