既存のRailsアプリをDocker化し、ローカルのDBに接続する方法

Ruby

Dockerを導入した新規Railsアプリの開発方法に関する記事を以前紹介しました。

今回は既存のRailsアプリをDocker化する方法について説明をします。

新規アプリとの違いですが、新規の場合はデータも特にないのでDBに関してもDockerイメージから作成し、docker-composeでアプリと接続をします。

一方、既存アプリの場合はローカルDBがすでに構築されておりデータもその中に存在しています。

今回はRailsのDokcer化ができればDBに関してはローカルのものを参照しても特に問題がありませんでした。

ですので、既存のRailsアプリについてRails部分をDokcer化し、DBはDocker上からローカルを参照するという環境を構築することをゴールに今回の説明をしていきたいと思います。

今回のゴール

なお、既存のアプリのサンプルは以下の記事の紹介したものを利用します。

Dockerファイルの作成

まずはDockerファイルを作成します。

Dockerfile

FROM ruby:2.4.0
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs less

# alias
RUN echo 'alias ll="ls -laG"' >> /root/.bashrc

ENV APP_HOME /usr/src/app

RUN mkdir $APP_HOME
WORKDIR $APP_HOME

COPY Gemfile $APP_HOME/Gemfile
COPY Gemfile.lock $APP_HOME/Gemfile.lock

RUN bundle install

docker-composeの作成

次にdocker-composeを作成します。

今回はRailsアプリだけをDocker上にのせるため、DBに関する情報はdocker-composeには書きません。

docker-compose.yml

version: "2"
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/usr/src/app
    ports:
      - 3000:3000
    command: /sbin/init
    env_file: .env.dev

環境変数の作成

DBに接続するための情報を.env.devに書きます。

.env.devはdocker-composeで読み込まれ、Railsの環境変数にセットされます。

DBに接続するユーザーがrailsuser、パスワードがrailspassである場合、以下のようになります。

DB_USERNAME=railsuser
DB_PASSWORD=railspass
DB_HOST=docker.for.mac.localhost

ここがDokcerからローカル環境にアクセスするためのポイントになります。

Dokcerからローカルにアクセスする場合はホスト名をdocker.for.mac.localhostに指定します。

2022年2月追記: docker.for.mac.localhostは非推奨になりました

Desktop 4.3.0でdocker.for.mac.localhostは非推奨および削除予定となっており、現在はhost.docker.internalが推奨されています。1

databse.ymlの変更

先ほどの環境変数を読み込むようにdatabase.ymlを変更します。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  username: <%= ENV.fetch('DB_USERNAME', 'root') %>
  password: <%= ENV.fetch('DB_PASSWORD', 'dummy') %>
  host:     <%= ENV.fetch('DB_HOST', 'db') %>
  socket: /tmp/mysql.sock

development:
  <<: *default
  database: hello_rails_development

test:
  <<: *default
  database: hello_rails_test

production:
  <<: *default
  database: hello_rails_production
  username: hello_rails
  password: <%= ENV['HELLO_RAILS_DATABASE_PASSWORD'] %>

結果確認

まずはbuildしてイメージを作成します。

$ docker-compose build

ビルドが完了したらdockerをupします。

$ docker-compose up

docker psでプロセスが立ち上がっていることを確認します。

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
7ebfcd9ead07        hellorails_web      "/sbin/init"        About an hour ago   Up 5 seconds        0.0.0.0:3000->3000/tcp   hellorails_web_1

先ほどのプロセス名を指定してdockerコンテナ内に入ります。

コンテナ内でRailsアプリを起動してあげればアプリにアクセスできるようになります。

$ docker exec -it hellorails_web_1 /bin/bash
[root@a1ba69d70f43:/usr/src/app]
bundle exec rails s -b 0.0.0.0
=> Booting Puma
=> Rails 5.1.5 application starting in development
=> Run `rails server -h` for more startup options

サンプルの例であれば、http://localhost:3000/homes にアクセスすれば既存のDBに存在するデータが表示されます。

より快適なDocker環境を構築するために

上記の方法で既存のRailsアプリをDocker化することができました。

しかし、開発効率をあげるために改善できる点があるので説明をしたいと思います。

改善の方針は以下の記事を参考にしています。

Dockerイメージのビルド回数を減らす

上記で作ったDockerの環境ではDockerファイルでbundle installを実行しているため、新しいgemを追加するためにbundle installを実行したい場合、毎回Dockerイメージを0から作成(ビルド)する必要があります。

Dockerイメージのビルドはゼロから改めてイメージを作成することになり時間がかかるので何度もビルドしていると開発効率が下がってしまいます。

そこでentrykitを導入し、bunble installはDockerのプロセスが起動する直前で実行されるように変更します。

Dockerfile

FROM ruby:2.4.0
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs less

# alias
RUN echo 'alias ll="ls -laG"' >> /root/.bashrc

ENV APP_HOME /usr/src/app

ENV ENTRYKIT_VERSION 0.4.0
RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && mv entrykit /bin/entrykit \
  && chmod +x /bin/entrykit \
  && entrykit --symlink


RUN mkdir $APP_HOME
WORKDIR $APP_HOME

ENTRYPOINT [ \
  "prehook", "ruby -v", "--", \
  "prehook", "bundle install -j3 --path /usr/local/bundle", "--"]

上記のように変更することでbundle installの実行をDokcerイメージのビルド工程から、Dockerプロセスの起動時(docker-compose up)に変更することができました。

その結果、bundle installをするたびに無駄な処理(Dockerイメージのビルド)がされなくなりました。

Dockerイメージを作成する時にbundle installをしないので、GemfileとGemfile.lockのコピーコマンドもDockerfileから削除しても大丈夫です。

bundle installの実行時間を減らす

entrykitを導入することで、bundle installをDockerイメージのビルドを毎回しなくて済むようになりました。

これでDockerプロセス起動時に純粋にbundle installだけを実行できるようになり、開発効率が上がるようになりました。

しかし、Dockerコンテナを削除してしまうとGemfileの内容を改めてゼロからインストールしなおさないといけなくなってしまいます。

そこでdata volumeを活用して、bundle installでインストールされたgemの内容を永続化できるように修正してあげます。

docker-compose.yml

version: "2"
services:
  datastore:
    image: busybox
    volumes:
      - bundle_install:/usr/local/bundle
  web:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/usr/src/app
    ports:
      - 3000:3000
    volumes_from:
      - datastore
    command: /sbin/init
    env_file: .env.dev
volumes:
  bundle_install:
    driver: local

これでDockerコンテナが破棄された場合でもdata volumeを参照することで今までインストールしたgemをすぐに利用することができるようになります。