Rails on Docker での PostgreSQL & unicorn が動く開発環境のメモ

自己学習のためにざっくり作ってみた

個人的には、本番はUnicornでも開発環境ではPumaでいいんじゃないかな...って思っている

SQLは合わせたほうが良いと思うけど

前準備

experimentalな機能を使うので環境変数を予め設定しておく

.envrc

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

Dockerfile

# syntax = docker/dockerfile:1.0-experimental
################################################################
# node Image
################################################################
# 先にマルチステージビルドでNode.jsとYarnを用意
# そうすることで、rails側のDockerfileの記述がシンプルになる
FROM node:12.18-alpine as node

# zipダウンロードの流派もあるけど、記述がシンプルなので apk add を使用
RUN apk --update add --no-cache yarn

################################################################
# rails Image
################################################################
FROM ruby:2.7.1-alpine

# node image から、Node.jsとYarnをコピー
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn

ENV RAILS_ENV development
ENV NODE_ENV development

ENV ROOT_PATH /app/
ENV LANG C.UTF-8
ENV PORT 80

WORKDIR $ROOT_PATH

# 必要なパッケージのインストール
RUN apk add --update --no-cache \
      postgresql-client \
      xz-dev \
      tzdata

# ビルドでしか使用しないパッケージは
# 後で削除するため virtual tag をつけている
RUN apk add --update --no-cache --virtual=build-dependencies \
      build-base \
      curl-dev \
      linux-headers \
      libxml2-dev \
      libxslt-dev \
      postgresql-dev \
      ruby-dev \
      yaml-dev \
      zlib-dev

# Gemfile.lock とおなじバージョンの bundler をインストール
RUN gem install bundler -v '2.1.4'

# 全部コピーするとRailsアプリの変更の度にここからビルドし直しになるので
# 必要なGemfileやyarnのファイルを先にコピーする
COPY Gemfile Gemfile.lock package.json yarn.lock $ROOT_PATH

# mount cacheを利用して、gemをキャッシュする
RUN bundle config set path .cache/bundle
RUN --mount=type=cache,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle

# ビルドでのみ使用するパッケージの削除
RUN apk del --purge build-dependencies

COPY . /myapp

# 起動
CMD rm -f tmp/pids/unicorn.pid && \
      bundle exec unicorn_rails -E $RAILS_ENV -c config/unicorn.rb -p ${PORT}

Dockerfileの方針

ビルド時間を短くして、無駄な時間を減らす

そのために、

  • 上の方は変更がなるべく無いようにして、イメージレイヤーのビルド数をへらす
  • alpineを利用したり、不要なパッケージを削除して、なるべくimageサイズを小さくする
  • mount cacheを利用して、gemをキャッシュする(2分半ほど処理が短くなった!)

docker-compose.yml

version: '3'
services:
  db:
    image: postgres:12-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: 'postgres'
    # 開発用。直接DBにアクセスしたい時のために
    ports:
      - "5432:5432"
  web:
    build: .
    ports:
      - "3000:80"
    volumes:
      - .:/app
    environment:
      DATABASE_USER: postgres
      DATABASE_PASSWORD: postgres
      DATABASE_HOST: db
    depends_on:
      - db

# databaseのデータはvolumeとして保存
volumes:
  db_data:

docker-compose.yml の方針

あんまり変わったことはしていない

Dockerfile側は、なるべく変数で制御して、環境毎の変数を docker-compose 側に持たせる流派もあるけど、まぁそこまではやらなくても良いのかな...? と思っている

  • 個人的には、Dockerfileはそこまで汎化せずに、環境毎にDockerfile作ってもいいんじゃないかなと思っている
  • だいたい事情で汎用化は破綻するので

以下に、Docker関係以外で初期状態から設定を追加/変更した部分を記す

config/unicorn.rb

# frozen_string_literal: true

APP_DIR = `pwd`.delete("\n")

working_directory APP_DIR

pid "#{APP_DIR}/tmp/pids/unicorn.pid"

worker_processes 2
# backlog は、workerが作業中でも受け取るTCPコネクションの数
# デフォルトの1024だと大量アクセスの時に待ちが大量発生するので減らしている
listen '/tmp/unicorn.sock', backlog: 64
timeout 60

config/unicorn.rb の方針

あんまり特筆すべきことはない。 backlog の数をデフォルトから減らしているところくらいかな...

config/database.yml

default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  username: <%= ENV['DATABASE_USER'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
  port: <%= ENV['DATABASE_PORT'] %>

development:
  <<: *default
  database: development

test:
  <<: *default
  database: test

production:
  <<: *default
  database: production

config/database.yml の方針

こちらも、各設定値を環境変数から取ってくるように変えたくらいかな...

config/environments/development.rb

ログ出力の標準出力化

  # Use an evented file watcher to asynchronously detect changes in source code,
  # routes, locales, etc. This feature depends on the listen gem.
  config.file_watcher = ActiveSupport::EventedFileUpdateChecker

  config.logger = ActiveSupport::Logger.new($stdout)
  # syncを有効にしないと、バッファリングされてログが一定量溜まらないと出力されない
  $stdout.sync=true

参考

👇 buildkit を使ったDocker buildの解説

medium.com

👇 docker-compose で buildkit を使う方法の解説

qiita.com

👇 効率の良いDockerfileの書き方

www.slideshare.net

👇 jokerさんによる、最近のRailsのDockerfileの書き方

speakerdeck.com

👇 効率の良いRailsのDockerfileの書き方

techracho.bpsinc.jp