Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

SupervisorでDocker環境のRailsサーバを楽に再起動する

BPSの福岡拠点として一緒にお仕事をさせて頂いています、株式会社ウイングドアのタカキと申します。

ウイングドアでは複数のテーマの勉強会が定期的に開催されており、私は未経験であるRubyの勉強会に参加しています。
その際にDocker環境で試行錯誤した内容について書いていきたいと思います。

Rubyの話?Dockerの話?

本記事の内容はRubyをきっかけとしたDockerの話です。
「Docker環境のRailsサーバをコンテナを止めずに再起動する」ために迷走した記録になります。
結論としてはSupervisorを使用して実現しました。
Dockerについて有用な記事が多数ある中初歩的な内容で恐縮ですが、勉強の一環として見ていただけますと幸いです。

※Supervisorについては既にこちらの記事に大変詳しい内容が載っています。

Dockerでsupervisorを使う時によくハマる点まとめ

Docker環境でRailsサーバを再起動するには?

環境

RubyのDocker環境構築は主にこちらを参考にさせていただき、シェルを実行するだけで設定ファイルの作成〜Railsサーバの起動まで完了するようにしました。

参考:Rails 6 + MySQL on Dockerの環境を秒速で構築する
参考:丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)

Ctrl+Cできない

チュートリアルを進めていく中で、Railsサーバを再起動する必要が出てきました。
Ctrl+Cでサーバ停止できるとのことですが、Dockerコンテナ起動と共にバックグラウンドでサーバを起動している場合はどこでCtrl+Cを押せば良いのだろう?と詰まってしまいました。

調べてみると、Docker環境ではコンテナ自体を再起動させるしかないようでした。

コンテナ再起動で無事進めることができましたが、その後も何度か再起動する度に
コンテナ再起動→docker execでコンテナに入り直す
という一手間が地味に面倒に感じるようになりました。

そこで、コンテナ接続中にコマンドでRailsサーバを再起動したいと思い色々試してみることにしました。

プロセスのkill

Apache等とは違い、Railsサーバの停止・再起動のコマンドはないとのこと。
それならプロセスをkillして再度立ち上げればいいのではないかと思いました。

プロセスを確認します。

root@e7abaa0edd69:/test_app# ps aux
USER  PID %CPU %MEM VSZ  RSS TTY STAT START  TIME COMMAND
root  1 0.0 0.0  2384  636 ? Ss  09:25  0:00 /bin/sh -c rm -f tmp/pids/server.pid && bundle exec rails s -p 3900 -b '0.0.0.0'
root  7 1.4 5.1 1305396 105096 ? Sl  09:25  0:06 puma 4.3.7 (tcp://0.0.0.0:3900) [test_app]
root 68 0.2 0.1  5748 3364 pts/0 Ss  09:32  0:00 bash
root 73 0.0 0.1  9388 2956 pts/0 R+  09:32  0:00 ps aux

pumaというのがRubyのwebサーバなので、pid 7をkillします。

kill 7

コンテナが停止しました。😐

init: trueの追加

※結果としてこの設定及びpidは無関係でした。

webサーバのプロセスだけkillしたつもりが、コンテナごと落ちてしまいました。

よく見ると起動時のコマンドがpid 1で動いています。

USER  PID %CPU %MEM VSZ  RSS TTY STAT START  TIME COMMAND
root  1 0.0 0.0  2384  636 ? Ss  09:25  0:00 /bin/sh -c rm -f tmp/pids/server.pid && bundle exec rails s -p 3900 -b '0.0.0.0'

Linuxにおけるpid 1はinitプロセスと言って特別らしい🤔 だからkillできなかったのでは? と考えました。
そのため起動コマンドをpid 1以外で起動させるためにdocker-compose.ymlにinit: trueの設定を追加しました。

  • docker-compose.yml
version: '3.7'
services:
  web:
    build: .
    init: true  # 追加
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3300 -b '0.0.0.0'"

再度プロセスを確認します。

root@e7b5f06e79c9:/test_app# ps aux
USER  PID %CPU %MEM VSZ  RSS TTY STAT START  TIME COMMAND
root  1 0.0 0.0 992  4 ? Ss  09:39  0:00 /sbin/docker-init -- /bin/sh -c rm -f tmp/pids/server.pid && bundle exec rails s -p 3900 -b '0.0.0.0'
root  6 0.0 0.0  2384  764 ? S 09:39  0:00 /bin/sh -c rm -f tmp/pids/server.pid && bundle exec rails s -p 3900 -b '0.0.0.0'
root  8 1.2 3.6 1090032 73864 ?  Sl  09:39  0:02 puma 4.3.7 (tcp://0.0.0.0:3900) [test_app]
root 52 0.0 0.1  5748 3648 pts/0 Ss  09:39  0:00 bash
root 74 0.0 0.1  9388 3052 pts/0 R+  09:41  0:00 ps aux

先程pid 1で動いていたコマンドがpid 6になっているので、設定が反映されていそうです。
pumaをkillしてみます。

kill 8

コンテナが停止しました。😑

Supervisorを使用する

この後もtty: trueを設定してみたり起動用シェルを作って呼び出してみたりと迷走し、
Dockerは通常1コンテナにつき1プロセスが実行される
という基本的なことに気がつくまで随分かかりました。

docker-compose.ymlにて、docker起動時のcommandでbundle exec rails s -p 3300 -b '0.0.0.0'を実行しています。

command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3300 -b '0.0.0.0'"

これはもちろんRailsサーバとしてコンテナを立ち上げるためですが、そうして指定したRailsサーバのプロセスを殺しているのでコンテナも終了してしまうということでした。(改めて書くと当たり前過ぎてお恥ずかしい限りです…。)

そこでSupervisorを使い複数プロセスを起動できるようにしました。

設定ファイルの変更・追加

  • Dockerfile
# apt-getにsupervisorを追加
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs sudo nano vim psmisc supervisor && rm -rf /var/lib/apt/lists/*

(中略)

# supervisorの設定を追記
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

EXPOSE 80
CMD /bin/sh -c "rm -f tmp/pids/server.pid && /usr/bin/supervisord"
  • supervisord.conf
[supervisord]
nodaemon=true

[program:rails]
command=bundle exec rails s -p 3300 -b '0.0.0.0'
  • docker-compose.yml
version: '3.7'
services:
  web:
    build: .
    init: true
    # commandは使用しない
    #command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3300 -b '0.0.0.0'"

ビルド後のプロセス確認

supervisordからrailsが起動しています。
pumaをkillしてみます。

root@cbf135dc5728:/test_app# ps aux
USER  PID %CPU %MEM VSZ  RSS TTY STAT START  TIME COMMAND
root  1 0.0 0.0 992  4 ? Ss  11:21  0:00 /sbin/docker-init -- /bin/sh -c /bin/sh -c "rm -f tmp/pids/server.pid && /usr/b
root  7 0.0 0.0  2384  724 ? S 11:21  0:00 /bin/sh -c /bin/sh -c "rm -f tmp/pids/server.pid && /usr/bin/supervisord"
root  8 0.0 0.0  2384  744 ? S 11:21  0:00 /bin/sh -c rm -f tmp/pids/server.pid && /usr/bin/supervisord
root 10 0.2 1.0 27168 21136 ? S 11:21  0:01 /usr/bin/python2 /usr/bin/supervisord
root 13 1.7 3.8 1093432 77640 ?  Sl  11:21  0:08 puma 4.3.7 (tcp://0.0.0.0:3900) [test_app]
root 71 0.6 0.1  5748 3480 pts/0 Ss  11:29  0:00 bash
root 78 0.0 0.1  9388 2896 pts/0 R+  11:29  0:00 ps aux
kill 13

コンテナは停止していません🎉

再起動コマンド

直接killできるようになりましたが、その必要はなく
supervisorctl restart rails
を実行すればrailsのプロセスが再起動されます。

このrestartコマンドが使えるということもすぐには理解できず、プロセスkill+rails sするシェルを作成するなどしてもう一迷走したりしました…。🤦‍♀️

シェルにまとめる

ということで、最終的に環境構築用のシェルはこのようになりました。
あれこれ手順を踏まずにコマンド1つで楽に環境作成できます。
※可読性・保守性はあまり良くありませんが、あくまで勉強用として作成しています。

  • init.sh
#!/bin/bash

#config setting#############
APP_NAME="test_app"
###########################

echo "docker pull ruby2.7.2"
docker pull ruby:2.7.2

echo "docker images"
docker images

if [ ! -e "Dockerfile" ]; then
  echo "make Dockerfile"
  cat <<EOF > Dockerfile
FROM ruby:2.7.2

ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs sudo nano vim psmisc supervisor && rm -rf /var/lib/apt/lists/*

#yarnのセットアップ
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:\$PATH

# 作業ディレクトリの作成、設定
ENV APP_HOME="/$APP_NAME"
RUN mkdir -p \$APP_HOME
WORKDIR \$APP_HOME

# ホスト側(ローカル)のGemfileを追加する
ADD ./src/Gemfile \$APP_HOME/Gemfile
ADD ./src/Gemfile.lock \$APP_HOME/Gemfile.lock

# Gemfileのbundle install
RUN bundle install
ADD ./src/ \$APP_HOME

# gem版yarnのuninstall
RUN gem uninstall yarn -aIx

# supervisor
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN sed -i -e "$ a alias rbrestart='supervisorctl restart rails'" ~/.bashrc

EXPOSE 80
CMD /bin/sh -c "rm -f tmp/pids/server.pid && /usr/bin/supervisord"
EOF
else
  echo "Dockerfile already exists"
fi

if [ ! -d "src" ]; then
  mkdir src && cd src

  # Gemfileを作成、編集
  echo "make Gemfile"
  cat <<EOF > Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem 'rails', '~> 6.0.3'
EOF

  echo "make Gemfile.lock"
  touch Gemfile.lock

  cd ../
else
  echo "src already exists"
fi

# docker-compose.ymlを作成、編集
if [ ! -e "docker-compose.yml" ]; then
  echo "make docker-compose.yml"
  cat <<EOF > docker-compose.yml
version: '3.7'
services:
  web:
    build: .
    init: true
    volumes:
      - ./src/:/$APP_NAME
    ports:
      - '3900:3900'
EOF
else
  echo "docker-compose.yml already exists"
fi

# supervisord.confを作成、編集
if [ ! -e "supervisord.conf" ]; then
  echo "make supervisord.conf"
  cat <<EOF > supervisord.conf
[supervisord]
nodaemon=true

[program:rails]
command=bundle exec rails s -p 3900 -b '0.0.0.0'
EOF
else
  echo "supervisord.conf already exists"
fi

# rails newを実行する
echo "docker-compose run web rails new . --force --skip-bundle"
docker-compose run web rails new . --force --skip-bundle

# webpacker install
echo "docker-compose run web rails webpacker:install"
docker-compose run web rails webpacker:install

# コンテナをビルド・起動
echo "docker-compose up -d --build"
docker-compose up -d --build

init.shを実行するとこのようにファイルが配置されてwebコンテナが起動します。

ホームディレクトリ
├── init.sh
├── src
│   ├── Gemfile
│   └── Gemfile.lock
├── docker-compose.yml
├── Dockerfile
└── supervisord.conf

おまけ

少しでも労力を減らすためにaliasを追加してrbrestartと打てば再起動コマンドが実行されるようにしました。
これで楽に再起動ができます🙌

まとめ

これまでDockerを使っていてもプロセスについてはあまり意識することがなかったので、理解を深めることができ良い機会になりました。
また、サクッとコンテナ作成・削除できるDockerの手軽さを改めて感じることもできました🐳

晴れて気軽に再起動できるようになったので、肝心のRubyも理解を深められるように引き続き精進していきたいと思います。
お読みいただきありがとうございました🙇‍♀️


株式会社ウイングドアでは、Ruby on RailsやPHPを活用したwebサービス、webサイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。