RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

長期間コミットのない社内向けRailsアプリのローカル開発環境をDockerで構築した話

こんにちは。ハコベルカーゴのサーバサイドの開発を担当している貞元です。

ハコベルカーゴの運用のためのWebアプリケーションが、サービス本体とは別に存在します。 こちらは改善や変更の必要なく、長期間コミットがない状況となっていました。 ただ、時間が経つことにより、完全に理解できている人は減っている事態となっていました。 久々に手を入れる必要ができたので、まずは開発環境を構築して対応していこうということになりました。 そのため、まずはローカルの開発環境の構築を簡単にするため、Docker, Docker Composeで構築した内容を紹介します。

なお、ハコベルカーゴとは荷物を送りたい荷主と、空いた時間に仕事を受注したい運送事業者を直接つなぐマッチングサービスです。 詳しくはこちらを見てみてくださいね。

ローカル開発環境

  • Docker for Mac
  • direnv

Docker関連ファイル

一旦、Docker関連のファイルを全て記載し、注目ポイントについては別途記載します。 すべてRAILS_ROOTへ配置したものとなります。

docker-compose.yml

version: '3'

services:
  mysql:
    image: mysql:5.7.30
    environment:
      LANG: C.UTF-8
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      MYSQL_ROOT_PASSWORD: ''
    volumes:
      - mysql_volume:/var/lib/mysql
    expose:
      - '3306'
  app: &app_base
    build: ./docker/app
    command: /bin/sh -c "rm -f tmp/pids/server.pid && rails s -p 3000 -b 0.0.0.0"
    ports:
      - '3000:3000'
    environment: &app_environment
      PATH: /app/bin:/app/node_modules/.bin:/usr/local/bundle/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
      USER_ID: ${USER_ID}
      GROUP_ID: ${GROUP_ID}
      MYSQL_ROOT_PASSWORD: ''
      SPRING_SOCKET: /home/docker/.spring/spring.sock
    volumes:
      - .:/app:delegated
      - bundle_volume:/usr/local/bundle
      - home_volume:/home/docker
    working_dir: /app
    depends_on:
      - mysql
      - spring
      - webpack
    hostname: app
    privileged: true
    entrypoint: ./docker/app/docker-entrypoint.sh
    tty: true
    stdin_open: true
  spring:
    <<: *app_base
    command: spring server
    ports: []
    hostname: spring
    depends_on: []
  webpack:
    <<: *app_base
    command: webpack-dev-server
    ports:
      - '8081:8081'
    environment:
      <<: *app_environment
      WEBPACKER_DEV_SERVER_HOST: 0.0.0.0
    hostname: webpack
    depends_on: []

volumes:
  mysql_volume:
    driver: local
  bundle_volume:
    driver: local
  home_volume:
    driver: local

docker/app/Dockerfile

FROM node:X.X.X-slim as node
FROM ruby:X.X.X-slim-stretch

COPY --from=node /opt/ /opt/
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/bin/node /usr/local/bin/node
RUN cd /usr/local/bin \
  && ln -s /usr/local/bin/node nodejs \
  && ln -s ../lib/node_modules/npm/bin/npm-cli.js npm \
  && ln -s ../lib/node_modules/npm/bin/npx-cli.js npx \
  && ln -s /opt/yarn-vX.X.X/bin/yarn yarn \
  && ln -s /opt/yarn-vX.X.X/bin/yarnpkg yarnpkg

RUN set -ex \
  && apt-get update \
  && apt-get upgrade -y \
  && apt-get install -y --no-install-recommends \
    iputils-ping locales task-japanese gosu sudo curl git vim less zsh \
    build-essential patch ruby-dev zlib1g-dev liblzma-dev \
    default-mysql-client default-libmysqlclient-dev \
    graphviz \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/* \
  && echo 'ja_JP.UTF-8 UTF-8' > /etc/locale.gen \
  && locale-gen \
  && update-locale LANG=ja_JP.UTF-8 \
  && cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ENV LANG ja_JP.UTF-8

docker/app/docker-entrypoint.sh

#!/bin/bash

if [ -z "$USER_ID" ]; then
  echo "USER_ID variable is not defined."
  exit 1;
fi

if [ -z "$GROUP_ID" ]; then
  echo "GROUP_ID variable is not defined."
  exit 1;
fi

getent passwd $USER_ID > /dev/null
user_result=$?
getent group $GROUP_ID > /dev/null
group_result=$?

set -e

# User settings
USER=$USER_ID
unset USER_ID
GROUP=$GROUP_ID
unset GROUP_ID
if [ $user_result -ne 0 ]; then
  echo "docker:x:$USER:$GROUP:docker:/home/docker:/bin/bash" >> /etc/passwd
fi
if [ $group_result -ne 0 ]; then
  echo "docker:x:$GROUP:" >> /etc/group
fi
echo "docker ALL=NOPASSWD: ALL" > /etc/sudoers.d/docker

# Directories settings
chown $USER:$GROUP /home/docker
if [ ! -e /home/docker/.spring ]; then
  mkdir /home/docker/.spring
  chown $USER:$GROUP /home/docker/.spring
fi

# Exec
exec gosu $USER:$GROUP "$@"

.envrc

export USER_ID=`id -u`
export GROUP_ID=`id -g`

注目ポイント

ホストディレクトリのマウント設定にオプションを指定

  app: &app_base
    volumes:
      - .:/app:delegated

有名な内容ですが、Docker for Macのマウントは遅いです。 そのため、Railsアプリのコードをコンテナにマウント設定にオプションを追加しています。 オプションは、 consistent(default), cached, delegated の3種類ありますが、今回は delegated を指定しました。 詳しくはこちらを参照ください。

Railsを実行するappコンテナはマルチステージビルド

FROM node:X.X.X-slim as node
FROM ruby:X.X.X-slim-stretch

COPY --from=node /opt/ /opt/
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/bin/node /usr/local/bin/node
RUN cd /usr/local/bin \
  && ln -s /usr/local/bin/node nodejs \
  && ln -s ../lib/node_modules/npm/bin/npm-cli.js npm \
  && ln -s ../lib/node_modules/npm/bin/npx-cli.js npx \
  && ln -s /opt/yarn-vX.X.X/bin/yarn yarn \
  && ln -s /opt/yarn-vX.X.X/bin/yarnpkg yarnpkg

Railsは、rubyとnodeを合わせて使用することが多いです。 rubyの公式Dockerイメージにはnodeは含まれていないので、nodeの公式イメージよりファイルをコピーしてruby + nodeのDockerイメージを作成しています。

appコンテナの実行ユーザー・グループを変更

rubyの公式Dockerイメージの実行ユーザー・グループは root:root です。 そのため、コンテナ内で作成されたファイルは root:root となり、マウントしたホストディレクトリのファイルのオーナーがrootになります。

そのため、以下のような方法でホストと同じユーザー・グループでappコンテナの実行ユーザー・グループを変更しています。

  1. .envrc(direnv)にてホストのユーザーID・グループIDを環境変数に登録
  2. docker-entrypoint.sh にて環境変数から取得したIDをもとにユーザー・グループを作成
  3. 作成したユーザー・グループにてappコンテナを実行

なお、Docker for Macはユーザー・グループを変換してくれているみたいで、この内容を実施しなくても大丈夫です。

タスク実行用のコンテナの作成

  app: &app_base
    environment:
      SPRING_SOCKET: /home/docker/.spring/spring.sock
  spring:
    <<: *app_base
    command: spring server

Railsにはspringというpreloader機能がありますが、各コンテナが別れているため有効に使えません。 そのため、環境変数SPRING_SOCKETを指定し、volumeで永続化したところへパスを指定すれば別々のコンテナでもspringを共有でき、Railsタスクの実行等が早くなります。

binding.pry

gem 'pry-rails' を使用したデバッグで、 binding.pryを使うことがあると思います。 ただ、docker-compose upの場合は入力待ち状態にならないため、別途dockerのコンテナに接続する必要があります。

$ docker ps # 該当のコンテナを確認
$ docker attach コンテナID or コンテナ名

また、コンテナを抜ける場合は、 Ctrl + p を押し Ctrl + q を押します。

まとめ

社内で使用するアプリケーションなど、正常に動いていると改善や変更が長期間ないものがあると思います。 時間が経つことによって、ミドルウェアのバージョンやその他環境の変化も出てくると思うので、今回のようにDockerを使用して環境に依存しない構築手順を用意しておくのはいかがでしょうか。