Dockerでrunserverを起動する際にPIDを1にする

よくありがちなことなのですが、Dockerでアプリを動かす場合は、PID 1 のプロセスにしておいた方が良いという話です。

背景

Dockerコンテナ内でDjangoアプリを開発するときに、 migraterunserver を実行するために、以下のようなDockerfileを作成することがあります。

Dockerfile:

FROM python:3.13

# 環境変数設定
ENV PYTHONUNBUFFERED=1 \
    WORKDIR=/app

# 作業ディレクトリの作成
RUN mkdir -p $WORKDIR

# 作業ディレクトリの設定
WORKDIR $WORKDIR

# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Djangoのmigrate実行後にrunserverを起動
CMD python manage.py migrate && python manage.py runserver

このDockerfileを使用してコンテナを起動すると、 migrate が実行された後に runserver が起動します。

これを以下のような compose.yaml で実行し、PIDの状態を確認してみます。

compose.yaml:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - .:/app
docker compose up -d
docker compose exec app ps -ef

結果は以下のようになりました。

UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  1 16:50 ?        00:00:00 /bin/sh -c python manage.py migrate && python manage.py runserver
root           8       1 18 16:50 ?        00:00:00 python manage.py runserver
root           9       8 41 16:50 ?        00:00:00 /usr/local/bin/python manage.py runserver
root          11       0 80 16:50 pts/0    00:00:00 ps -ef

さて、ここでPID 1 のプロセスは /bin/sh になっていて、shからrunserverを起動しています。 runserverを実行しているPythonのPIDは 8 です。runserverは内部でDjangoのアプリをスレッドで実行しているので、PID 9Pythonのプロセスです。

この状態で、 docker compose down を実行すると、コンテナが停止して破棄されるのですが、コマンドを実行してから停止するまでに時間がかかります。10秒ぐらい。

なぜか?

この時間がかかっているのは、PID 1にSIGTERMシグナルを送信してもプロセスが停止しないからです。 Docker composeのFAQに記載があります。

docs.docker.com

デフォルトではタイムアウトが10秒になっていて、10秒待ってもPID 1 のプロセスが終了しない場合は、強制終了されます。

少し時間が経った後に終了するのは、この強制終了によって終了されているからですね。

解決策

これをきれいに解決するには、 runserver をPID 1 で実行するとよいです。

Dockerfileを以下のように変更します。

FROM python:3.13

# 環境変数設定
ENV PYTHONUNBUFFERED=1 \
    WORKDIR=/app

# 作業ディレクトリの作成
RUN mkdir -p $WORKDIR

# 作業ディレクトリの設定
WORKDIR $WORKDIR

# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Djangoのmigrate実行後にrunserverをexecで起動
CMD python manage.py migrate && exec python manage.py runserver

変更したのは最後のCMDの部分です。execコマンドでrunserverを実行するようにしました。 こうすると、PID 1 のプロセスがrunserverになります。

docker compose buildでイメージをビルドしなおし、composeを起動して、PIDを確認してみます。

docker compose build
docker compose up -d
docker compose exec app ps -ef

結果は以下のようになりました。

UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0 20 17:10 ?        00:00:00 python manage.py runserver
root           8       1 67 17:10 ?        00:00:00 /usr/local/bin/python manage.py runserver
root          10       0 66 17:10 pts/0    00:00:00 ps -ef

PID 1 のプロセスが python manage.py runserver になっています。

これで、 docker compose down を実行すると、すぐにコンテナが停止、破棄されます。

まとめ

Dockerでアプリを動かすときは、PID 1 のプロセスにしておくと、コンテナの停止が早くなります。

exec コマンドを使うことで、起動したプロセスのPIDを起動元のPIDにできます。

補足

本番環境だとgunicornなどでアプリを起動することが多いですが、gunicornの場合も同様にしてPID 1 で起動しておかないと、graceful shutdownにならなかったりするので、注意が必要です。

参考

tokibito.hatenablog.com