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

# Abstract
Rails on DockerはDocker docsにサンプルアプリが載っています。
--> [Quickstart: Compose and Rails | Docker Documentation](https://docs.docker.com/compose/rails/)

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

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

```dockerfile: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イメージ](https://hub.docker.com/_/ruby/)から最新の物を探しましょう。最新・安定のruby-alpineイメージには`alpine`タグがついています。
`FROM ruby:alpine`と宣言してもいいのですが、この場合はビルドのタイミングでバージョン変わっちゃう可能性あるのでその時のバージョンを指定するといいと思います。2019/11/01時点では`2.6.5-alpine3.10`。

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

```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-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でいかがでしょうか？

```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の内容とは異なりますが、基本一緒です。

```ruby: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`）

```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](https://hub.docker.com/_/postgres)
またpostgresqlのタイムゾーンをDockerfileと合わせておくのもよしです。

```yml: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](https://github.com/at946/base-rails-on-docker)に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が表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/9ef11dde-7082-24ed-ef0c-84c12e67a4b8.png)


# 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できるようになっているはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/2aa8216a-db13-8618-ef2c-7b417a47ecea.png)

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

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

# Reference
- [Quickstart: Compose and Rails | Docker Documentation](https://docs.docker.com/compose/rails/)
- ["rails new" options explained · GitHub](https://gist.github.com/eliotsykes/ace0222174804372b51a)
- [run — Docker-docs-ja 17.06.Beta ドキュメント](http://docs.docker.jp/compose/reference/run.html)