Alpine LinuxベースのDockerイメージでRails6の開発環境を構築する

ruby-logo

Alpine Linux ベースの Ruby イメージで Rails6 + PostgreSQL の開発環境を作る機会がありました。構築手順を残しておきます。

目次

  1. Quickstart を確認する
  2. Rails6 をインストール
  3. Rails プロジェクトを作成する
  4. tzdata をインストールする
  5. Node.js と yarn をインストールする
  6. DB の設定をする
  7. ここまでの手順を Dockerfile に落とし込む
  8. Docker イメージの容量を小さくする
  9. ENTRYPOINT を設定する
  10. docker-compose.yml で command を指定

Quickstart を確認する

Rails の Docker による環境構築は Quickstart: Compose and Rails | Docker Documentation を参考に進めます。

Quickstart の Dockerfile はこんな感じ。


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 ベースに変更していきます。

Rails6 をインストール

まずは Rails のインストールから。Dockerfile を新しく作成します。

Dockerfile


FROM ruby:2.7.1-alpine3.11

WORKDIR /myapp

次に docker-compose.yml を作ります。Rails コンテナは Quickstart: Compose and Rails | Docker Documentation を参考に、PostgresSQL は postgres – Docker Hub を参考にしています。


version: '3'

services:
  app:
    build: .
    depends_on:
      - db
    ports:
      - "3000:3000"
    volumes:
      - .:/myapp:cached

  db:
    image: postgres:12-alpine
    environment:
      POSTGRES_PASSWORD: example
    volumes:
      - db:/var/lib/postgresql/data

volumes:
  db:

docker コンテナを起動して中に入ります。


docker-compose run --service-ports app ash

Rails をインストールしてみます。記事執筆時点では 6.0.3 が最新なのでバージョンを指定します。


gem install rails -v "6.0.3"

失敗しました。


/usr/local/lib/ruby/2.7.0/mkmf.rb:471:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /usr/local/bundle/extensions/x86_64-linux-musl/2.7.0/nokogiri-1.10.9/mkmf.log

エラーログを見ると、gcc などが入っていないのが原因のようなので、コンパイルに必要なツールが入っている build-base をいれます。


apk add build-base

もう一度 rails のインストールを実行。


gem install rails -v "6.0.3"

今度は成功しました。rails-6.0.3.1 が入りました。


Successfully installed rails-6.0.3.1
32 gems installed

Rails プロジェクトを作成する

引き続きコンテナ内で作業。rails new コマンドで rails プロジェクトを作ります。DB は PostgreSQL を使うのでオプションで指定します。


rails new . -d postgresql

pg の gem のインストールで失敗しました。


Fetching pg 1.2.3
Installing pg 1.2.3 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /usr/local/bundle/gems/pg-1.2.3/ext
/usr/local/bin/ruby -I /usr/local/lib/ruby/2.7.0 -r ./siteconf20200522-11586-1h6s4wi.rb extconf.rb
checking for pg_config... no
No pg_config... trying anyway. If building fails, please try again with
 --with-pg-config=/path/to/pg_config
checking for libpq-fe.h... no
Can't find the 'libpq-fe.h header

おそらく何かのパッケージが足りないので、pg の gem のドキュメントを確認します。pg gem の Requirements によると PostgreSQL -dev packages が必要なようです。

alpine のパッケージで postgresql を探してみると


apk search postgresql | grep dev

dev パッケージがありました。


postgresql-pllua-dev-2.0.4-r0
postgresql-bdr-dev-9.4.14_p1-r7
postgresql-dev-12.2-r0

postgresql-dev を入れます。


apk add postgresql-dev

先ほど止まった gem のインストールから再開します。今度は成功しました。


bundle install

tzdata をインストールする

rails server -b 0.0.0.0 コマンドでサーバを起動してみます。


rails server -b 0.0.0.0

エラーが出てきました。tzinfo の gem で TZInfo::DataSourceNotFound が出ているようです。


/usr/local/bundle/gems/tzinfo-1.2.7/lib/tzinfo/data_source.rb:182:in `rescue in create_default_data_source': tzinfo-data is not present.
Please add gem 'tzinfo-data' to your Gemfile and run bundle install (TZInfo::DataSourceNotFound)

tzinfo の gem のドキュメント を確認します。

By default, TZInfo will attempt to use TZInfo::Data. If TZInfo::Data is not available (i.e. if require ‘tzinfo/data’ fails), then TZInfo will search for a zoneinfo directory instead (using the search path specified by TZInfo::ZoneinfoDataSource::DEFAULT_SEARCH_PATH).

ドキュメントによると、TZInfo は最初に TZInfo::Data クラスを使おうとするようです。Gemfile を確認すると tzinfo-data という gem があります。TZInfo はこの中に入っていそうです。しかしこの gem は platforms で環境の指定があり(Windows 環境での利用を想定されているように見えます)、alpine linux のコンテナ環境では動いていません。


gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

TZInfo::Data が利用可能でない場合、TZInfo::ZoneinfoDataSource::DEFAULT_SEARCH_PATH の zoneinfo を使うと書かれています。ドキュメントには PATH が書かれていないので、ソースを確認します。

tzinfo/zoneinfo_data_source.rb


DEFAULT_SEARCH_PATH = ['/usr/share/zoneinfo', '/usr/share/lib/zoneinfo', '/etc/zoneinfo'].freeze

上記 3 つのディレクトリは Alpine Linux ベースの ruby イメージには存在しないので、TZInfo::DataSourceNotFound エラーにつながったと予想できます。

Alpine Linux ではタイムゾーン関連の情報は tzinfo パッケージにあるので、インストールします。


apk add tzdata

インストール後、/usr/share/zoneinfo ディレクトリにタイムゾーンの情報が配置されました。


ls /usr/share/zoneinfo

Node.js と yarn をインストールする

再び rails server -b 0.0.0.0 コマンドでサーバを起動してみます。


rails server -b 0.0.0.0

今度は Webpacker 周りでエラーが出てきました。


/usr/local/bundle/gems/webpacker-4.2.2/lib/webpacker/configuration.rb:95:in `rescue in load': Webpacker configuration file not found /myapp/config/webpacker.yml.
Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - /myapp/config/webpacker.yml (RuntimeError)

エラーメッセージの通り、rails webpacker:install を実行します。


rails webpacker:install

Node.js がインストールされていないエラーが出てきました。


Node.js not installed. Please download and install Node.js https://nodejs.org/en/download/

最新安定版の Node.js を入れます。この記事を書いた時点では 12.16.3 が最新安定版なので、12 系を入れます。


apk add nodejs~=12

12.15.0 が入りました。Alpine なのでちょっと古いです。


(4/4) Installing nodejs (12.15.0-r1)

エラーメッセージの通り、rails webpacker:install を実行します。


rails webpacker:install

再び rails webpacker:install を実行します。


rails webpacker:install

Yarn がインストールされていないエラーが出てきました。


Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/

Yarn を入れます。


apk add yarn

rails webpacker:install を実行します。


rails webpacker:install

今度はうまくいきました。


Webpacker successfully installed

rails server -b 0.0.0.0 コマンドでサーバを起動します。


rails server -b 0.0.0.0

ようやく起動しました。


Puma starting in single mode...
* Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000

DB の設定をする

ブラウザから http://localhost:3000 にアクセスすると、DB の接続エラーがでてきました。そういえば DB 周りの設定を忘れていました。

config/database.yml の development と test 環境を編集し DB の接続情報を環境変数から受け取るようにします。


development:
  <<: *default
      database: myapp_development
      username: <%= ENV['DATABASE_USERNAME'] %>
      password: <%= ENV['DATABASE_PASSWORD'] %>
      host: <%= ENV['DATABASE_HOST'] %>
test:
  <<: *default
      database: myapp_test
      username: <%= ENV['DATABASE_USERNAME'] %>
      password: <%= ENV['DATABASE_PASSWORD'] %>
      host: <%= ENV['DATABASE_HOST'] %>

docker-compose.yml で DB に接続するための環境変数を設定します。


services:
  app:
    build: .
    depends_on:
      - db
    environment:
      - DATABASE_HOST=db
      - DATABASE_USERNAME=postgres
      - DATABASE_PASSWORD=example
    ports:
      - "3000:3000"
    volumes:
      - .:/myapp:cached

Ctrl-C で rails server コマンドを止めて、rails db:create コマンドでデータベースを作成します。


rails db:create

DB ができました。


Created database 'myapp_development'
Created database 'myapp_test'

rails server -b 0.0.0.0 コマンドで起動します。


rails server -b 0.0.0.0

ブラウザから http://localhost:3000 にアクセスすると、「Yay! You’re on Rails!」が表示されました。


app_1  | Started GET "/" for 192.168.80.1 at 2020-05-22 08:22:50 +0000
app_1  | Cannot render console from 192.168.80.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
app_1  | Processing by Rails::WelcomeController#index as HTML
app_1  |   Rendering /usr/local/bundle/gems/railties-6.0.3.1/lib/rails/templates/rails/welcome/index.html.erb
app_1  |   Rendered /usr/local/bundle/gems/railties-6.0.3.1/lib/rails/templates/rails/welcome/index.html.erb (Duration: 7.1ms | Allocations: 294)
app_1  | Completed 200 OK in 28ms (Views: 16.1ms | ActiveRecord: 0.0ms | Allocations: 1634)

ここまでの手順を Dockerfile に落とし込む

ここまではコンテナの中で作業していました。docker run したときに rails server を起動できるように、これまでやってきた内容を Dockerfile に落とし込みます。


FROM ruby:2.7.1-alpine3.11

WORKDIR /myapp

COPY . .

RUN apk update \
    && apk add --no-cache \
    build-base \
    postgresql-dev \
    tzdata \
    nodejs~=12 \
    yarn \
    && bundle install \
    && yarn install

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

コンテナから抜けて、Docker イメージをビルド。


docker-compose build

コンテナ上で rails が起動してブラウザからアクセスできることを確認しておきます。


docker-compose up

Docker イメージの容量を小さくする

コンパイル用のパッケージ build-base は bundle install した後は不要になるはずなので、最後に削除するようにします。


RUN apk update \
    && apk add --no-cache --virtual=.build-deps \
    build-base \
    && apk add --no-cache \
    postgresql-dev \
    tzdata \
    nodejs~=12 \
    yarn \
    && bundle install \
    && yarn install \
    && apk del .build-deps

ENTRYPOINT を設定する

Quickstart との差分を埋めていきます。 entrypoint.sh を作ります。


#!/bin/ash
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 "$@"

ENTRYPOINT を Dockerfile に設定。


FROM ruby:2.7.1-alpine3.11

WORKDIR /myapp

COPY . .

RUN apk update \
    && apk add --no-cache --virtual=.build-deps \
    build-base \
    && apk add --no-cache \
    postgresql-dev \
    tzdata \
    nodejs~=12 \
    yarn \
    && bundle install \
    && yarn install \
    && rails assets:precompile \
    && apk del .build-deps

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]

docker-compose.yml で command を指定

docker-compose.yml も Quickstart に合わせ command を指定するようにします。


version: '3'

services:
  app:
    build: .
    command: ash -c "rm -f tmp/pids/server.pid && rails s -p 3000 -b '0.0.0.0'"
    depends_on:
      - db
    environment:
      - DATABASE_HOST=db
      - DATABASE_USERNAME=postgres
      - DATABASE_PASSWORD=example
    ports:
      - "3000:3000"
    volumes:
      - .:/myapp:cached

  db:
    image: postgres:12-alpine
    environment:
      POSTGRES_PASSWORD: example
    volumes:
      - db:/var/lib/postgresql/data

volumes:
  db:

Docker イメージをビルドし、コンテナを起動します。Rails の画面が出ていれば OK です。

次回は、このアプリケーションを Heroku にデプロイします。