はじめに
こんにちは。
Docker を使って Rails の環境構築を進めていたところ、
AI や教材から「Dockerfileに以下の entrypoint.sh を設定するコードを追加してください」と言われました。
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
言われた通りに書いたのですが、
「そもそもこれって何のためにあるの?」「ないとどうなるの?」が気になり、仕組みを調べてみました。
すると、server.pid というファイルが引き起こす「バグ」と、それを防ぐための仕組みが見えてきたので、
自分への備忘録を兼ねてできるだけ分かりやすくまとめてみたいと思います!
同じように「言われるがままコードを書いたけど、中身が気になる」という初学者の方の参考になれば嬉しいです。
何のためにこのコードを書くのか?
一言でいうと、これは「Docker が強制終了したときに、次回 Rails が起動できなくなるバグを防ぐための安全弁」です。
Rails には「サーバー起動時に server.pid という一時ファイルを作り、終了時に消す」という仕組みがあります。
パソコンが急に固まったり、Docker を無理やり落としたりすると、
Rails の裏側で、本来はサーバー終了に伴い一緒に消える server.pid という一時ファイルが消えずに残ってしまいます。
この残されたファイルのせいで、次回コンテナを立ち上げたときに Rails サーバーの起動処理が「すでにサーバーが起動している!」と判断して、二重起動を防ぐために起動直後に即落ちしてしまう現象が発生します。
=> Booting Puma
=> Rails 6.1.7 application starting in development
=> Run `rails server --help` for more startup options
A server is already running. Check /myapp/tmp/pids/server.pid.
Exiting
これを防ぐために、コンテナが起動した瞬間に最優先で entrypoint.sh を実行し、「Rails が立ち上がる直前に、もし古い server.pid が残っていたら強制削除する」という掃除を行っています。
では、なぜそんな現象が起きるのか、裏側の仕組みを詳しく見ていきましょう!
そもそも server.pid って何?
これは「現在、環境(コンテナ内など)の中で動いている サーバー(Rails)に対して、OS が割り当てた『識別番号』が書かれたメモ帳」です。
server.pid の「PID」とは、Process ID(プロセスID) の略になります。
これがなぜ作られ、なぜ強制終了時に問題を起こすのか、その仕組みを解説します。
1. server.pid の役割:二重起動の防止
Rails サーバーを起動すると、裏側でひとつのプロセス(プログラム)が立ち上がります。
このとき、OS はそのプロセスに対して「君の番号は 1234 ね」という風に番号(これがPID)を割り振ります。
すると Rails は起動した瞬間、アプリのディレクトリ内にある tmp/pids/server.pid というファイルを自動で作って、その中に 1234 という数字だけを書き込みます。
これが server.pid という一時ファイル です。
なぜそんなファイルを作るのか?というと、
理由は、「同じ Rails サーバーを、同じ場所で 2 つ同時に起動させないため(二重起動の防止)」です。
仮にもし、すでに Rails が動いているのに気づかず、もう一度同じコマンドを打ってしまったとします。
そうすると、2つ目の Rails は起動する直前に server.pid を見に行きます。
そこにすでに 1234 と書かれているのを発見すると、
「あ、すでに PID 番号 1234 の先客が起動中だな。じゃあ僕は起動するのをやめよう」
と判断して、エラーを出して安全に身を引きます。
つまり、server.pid に記録された PID のプロセスがまだ動いていると判断した場合は、新しい Rails サーバーは起動しないようになっているのです。
これが正常な「二重起動防止」の仕組みです。
2. 強制終了時に起きるバグの現象
通常、Ctrl + C などで正しく Rails を終了させると、
Rails は終了する間際に「自分が作った server.pid をゴミ箱にポイして消す」という片付けをしてから終了します。
しかし、Docker コンテナが急にクラッシュしたり、PCの電源が落ちたりすると、Rails は片付けをする暇がないまま終了します。
その結果、「PID番号 1234 のプログラム(本体)はもう存在しないのに、server.pid(メモ帳)だけが残される 」という状態になってしまうのです。
server.pid が残ったまま起動するとどうなるか
この状態で再び Docker を立ち上げて Rails を起動しようとします。
新しく起動しようとした Rails は、残された server.pid を見て、
「 server.pid があるということは、すでに PID番号 1234 がどこかで動いているから同じ番号で起動しちゃダメだ。終了しよう」
となってしまいます。
実際には PID 1234番 はもうどこにも存在しないのに、残された server.pid のせいで、Rails が勝手に遠慮して起動直後に即落ちしてしまうことになる、ということです。
これが server.pid による起動エラーの正体です。
だからこその ENTRYPOINT ["entrypoint.sh"]
この「誰もいないのに、メモ帳(server.pid)だけが残って起動できない」という問題を解決するために、entrypoint.sh が活躍します。
Rails が起動してそのメモ帳をチェックする「一歩手前」で、rm -f tmp/pids/server.pid を実行してそのメモ帳を強制的に破り捨てます。
これによって、新しく起動しようとする Rails は「メモ帳はないな!新しくPID番号を登録して起動しよう!」と、毎回スムーズに立ち上がることができるようになるのです。
Dockerfile の記述
上記のことを理解しておくと、Dockerfile に書いた記述がどういうものかわかってきます。
# エントリポイントスクリプトをコピーして実行権限を付与
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
COPY entrypoint.sh /usr/bin/ => ローカル側にある事前準備用のスクリプトを、コンテナのシステムディレクトリ(/usr/bin/)にコピーする。
RUN chmod +x ... => コピーしたファイルはそのままではただのテキストなので、Linuxに「これは実行していいプログラム(ファイル)」と実行権限(+x)を付与する。
ENTRYPOINT ["entrypoint.sh"] => コンテナが起動した瞬間、何よりも最優先でこのスクリプトを実行して!とコンテナに命令する。
まとめると、ローカルで事前準備したスクリプトを最優先で実行させている、ということになります。
事前準備するスクリプトは、前の起動時の server.pid を消去するものになりますね。
よって entrypoint.sh の中のコードは以下になります。
#!/bin/bash
set -e
# もし古いサーバーの pid ファイルが残っていたら、起動前に必ず消去する
rm -f /myapp/tmp/pids/server.pid
# 本来の主役であるコマンド(rails server など)にバトンタッチする
exec "$@"
ちなみに server.pid は Rails を起動すると自動的に生成されますが、
entrypoint.sh は自分で作成する必要があるので、Dockerfile の記述時に作成しておく必要があります。
これで Docker が強制終了しても、次回 Rails が起動できなくなるバグを防ぐことができます!!
いくつかの補足と疑問
補足:PID が生きているか自動で確認する仕組み
実は、server.pid に書かれたPIDが実際に生きているプロセスかどうかを確認する仕組みが、
Puma がラップしている Rack(正確には rackup gem)の check_pid! というメソッドに実装されています。
rackupのソースコード を見ると、
以下のようなコードになっています(該当部分を抜粋・要約)。
- pidファイルに書かれたPID番号に対して
Process.kill(0, pid)という特殊な呼び出しを行う - これは実際にプロセスへシグナルを送るものではなく、「そのPID番号のプロセスが存在するかどうか」を確認する呼び出し(厳密には権限チェックも行う)
- もしプロセスが存在しなければ
Errno::ESRCHというエラーが発生する - エラーが発生した場合は「もう誰もいない」と判断して、古いpidファイルを自動的に削除してから起動を続ける
つまり、通常の環境であれば、たとえ server.pid が残っていても「本当にそのPIDが生きているか」までチェックしてくれるので、賢く自動で対処してくれる仕組みが備わっているわけです。
じゃあ、entrypoint.sh なんていらないのでは? となってしまいそうですが、
Docker 内だとこの賢い挙動が意図通りに働かないケースがあります。
なぜなら、
「Docker コンテナは起動するたびに新しい PID 名前空間が作られ、PID が 1 から順番に割り振られ直すため、以前使われていた小さな PID が再利用される可能性があるから」 です。
例えば以下のようなフローです。
- 前回クラッシュしたとき、Rails の PID が 7 で、
server.pidにも 7 と残っています。 - コンテナを再起動(
docker compose up)。 - コンテナが起動すると、PID は 1 から順番に割り振られます。その結果、先に起動した別のプロセスに、たまたま PID番号 7 が割り振られてしまったとします。
- その後、Rails(Puma経由でRackの
check_pid!)が起動すると、server.pidの 7 を見つけます。 -
Process.kill(0, 7)で「PID 7 番って生きてる?」と確認します。 - OS が答えます。「7 番は生きています!(※ただし全く無関係な別のプログラム)」
-
check_pid!が勘違いします。「エラーが出なかった=まだ動いている=前のRailsがまだ起動中なんだ!じゃあこのプロセスは邪魔しちゃいけないから終了しよう」
このように、PID が再利用された場合には、無関係なプロセスを以前の Rails サーバーと誤認してしまう可能性があります。
つまり、本体の Rails はもう強制停止しているのに、
Docker コンテナでは PID が 1 から振り直されるため、以前使われていた PID が再利用されてしまい、別のプログラムを「前のサーバーだ」と勘違いしてしまう可能性があるのです。
なので、Rack の check_pid! というメソッドに任せるのではなく、entrypoint.sh で強制的に server.pid を削除しておく方法が広く採用されています。
ちなみに、Docker ではなくホストPCなどの通常環境であれば、この仕組みはきちんと機能するケースが多いです。
プロセスIDの母数が多いので、たまたま同じ番号が使い回される可能性は低いためです。
疑問:Rails やコンテナは、すでに同じ PID があるのであれば、番号を自動的にずらして起動する、ということはしないのか?
ここまで読むと、一つの疑問が浮かぶかもしれません。
「もし同じ PID が残っているなら、Rails 側で別の新しい PID を自動で使って起動すればいいのでは?」
結論から言うと、OS の視点から見れば、すでに起動している PID とは別の PID に自動的に割り振るのは簡単です。
しかし、Rails がそれを禁止しています。
理由は、Rails の中で2つ目のプロセスが動いたら危険だからです。
なぜ2つ同時に動くと危険なのか?
同じ Rails アプリケーションサーバーが意図せず複数起動すると、それぞれが同じアプリケーションとして動作してしまいます。
その結果、同じリソースを同時に扱ったり、意図しない競合が発生したりする可能性があります。
そのため、Rails (正確には Puma が利用する Rack) は同じアプリケーションサーバーを誤って二重起動しないよう server.pid という鍵をかけて、起動中のサーバーの二重起動を厳重に防いでいます。
ただし、大規模なアプリだと例外で、一つのサーバーに複数のプロセスが同居することもあります。(Pumaのクラスターモードなど)。
この場合は、マスタープロセスが複数のワーカープロセスの役割分担を管理し、リクエストの奪い合いのような衝突が起きないようになっています。
また、複数台のサーバーで捌く規模になると、その手前にロードバランサーを置いてリクエストを振り分けることも一般的です。
だからこそ、開発環境などの1つのコンテナ内においては、予期せぬ二重起動を防ぐために server.pid による制限が絶対に不可欠なのです。
まとめ
-
server.pidは、Rails サーバーの二重起動を防ぐために作成される一時ファイル - 正常終了時には削除されるが、Docker コンテナの強制終了などでは残ってしまうことがある
-
server.pidが残ると、Rails(正確には Puma が利用する Rack)が「すでにサーバーが起動している」と判断し、起動できなくなる場合がある -
entrypoint.shで起動前に古い server.pid を削除しておくことで、この問題を回避できる - Rack には PID の生存確認を行う仕組みもあるが、Docker では PID の再利用によって誤判定が起こる可能性があるため、entrypoint.sh による削除が広く採用されている
ここまで読んでいただきありがとうございました。
本記事はAI やソースコードなどを読みながら自分なりに内容をまとめたものです。
もし認識違いや補足があれば、ご指摘いただけると嬉しいです。