Abstract
Rails on DockerはDocker docsにサンプルアプリが載っています。
--> Quickstart: Compose and Rails | Docker Documentation
ただ、Dockerイメージはサイズを軽量化するのがよしとされており、そのためにalpine linuxを使うというのはよくある話のようです。ので、上記のQuickstartをalpineで実施するメモです。
Define the project
まず、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を作ります。
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-base
、curl-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でいかがでしょうか?
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の内容とは異なりますが、基本一緒です。
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
)
#!/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と合わせておくのもよしです。
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
ここはまるパクリですね。
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
のページにしておきます。
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してしまえば差は無いように感じます。
サイズの比較はしてないですが、いろいろな方の調査結果ではruby
とruby-alpine
ではイメージサイズが数倍〜十数倍違ったりするらしいので、アップロードダウンロードやディスクにとってメリットがあるのでオススメです。