DockerコンテナのPID 1問題を解決する方法|--initフラグとtiniでシグナル処理とゾンビプロセスを防ぐ設計

運営:リナックスマスター.JP(代表 宮崎智広)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > Docker > DockerコンテナのPID 1問題を解決する方法|--initフラグとtiniでシグナル処理とゾンビプロセスを防ぐ設計
「docker stopを実行してもコンテナがすぐに止まらず、10秒後に強制終了(SIGKILL)される」「長時間稼働させるとdocker top コンテナ名で<defunct>という謎のプロセスが増え続ける」——本番でDockerコンテナを動かしていると、こんなトラブルに出会うことがある。

これらはどちらも「PID 1問題」と呼ばれる、Dockerコンテナに固有のプロセス管理の落とし穴だ。アプリをそのままPID 1として起動すると、シグナルの伝達もゾンビプロセスの回収も正常に機能しない。

この記事では、PID 1問題の仕組みを整理した上で、--initフラグとtiniを使った解決策を実機で確認しながら解説する。実行環境はUbuntu 24.04 LTS(Docker 27.3.1)で動作確認済みだ。

この記事のポイント

・アプリをPID 1で起動するとSIGTERMが届かずdocker stopが10秒後にSIGKILLになる
・子プロセスのゾンビが溜まるのはPID 1がwait()を呼ばないのが原因
・--initフラグでtiniをPID 1に差し込むと両問題を解決できる
・本番イメージに焼き込むにはDockerfileにtiniをインストールしてENTRYPOINTに指定する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

DockerコンテナがPID 1で起動する仕組みと問題の本質

通常のLinuxサーバーでは、OS起動時に最初に立ち上がるプロセスのPIDは1になる。現代のサーバーではsystemdがPID 1として動き、全プロセスの先祖になる。systemdはシグナルを適切に処理し、孤立した子プロセスの終了を回収する「リーピング」も担う。

Dockerコンテナは、VMとは異なり新しいPID名前空間を作成することでプロセスを分離する。この名前空間の中ではプロセス番号が1から振り直されるため、ENTRYPOINTやCMDに指定したプロセスがそのままPID 1になる。

# sleepコマンドをPID 1として起動した例 $ docker run --rm -d --name pid1-demo ubuntu:24.04 sleep 600 $ docker exec pid1-demo ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 2788 952 ? Ss 03:21 0:00 sleep 600

sleep 600がPID 1になっている。通常のサーバーではsystemdが担うPID 1の座に、コンテナでは直接アプリが座る。これがすべての問題の出発点だ。

シグナル処理の問題|なぜdocker stopが10秒かかるのか

docker stop コンテナ名を実行すると、DockerはコンテナのPID 1にSIGTERMを送る。アプリがSIGTERMを受け取って正常終了すれば問題ない。しかし多くの場合、これが意図どおりに動かない。

典型的な落とし穴がシェル形式のCMD/ENTRYPOINTだ。

# シェル形式(PID 1がshになってしまう) CMD sh -c "node server.js" # または CMD ["sh", "-c", "node server.js"] # exec形式(PID 1がnodeになる。こちらが正しい) CMD ["node", "server.js"]

シェル形式で起動すると/bin/sh -c "node server.js"という形でshがPID 1になり、nodeはその子プロセスとして動く。docker stopでSIGTERMがshに届いても、shはデフォルトではSIGTERMに対して何もしない(LinuxのシグナルはPID 1のデフォルトハンドラーを登録しない仕様で、shにはSIGTERMのハンドラーがない)。子プロセスのnodeにもシグナルは転送されない。

結果として--stop-timeout(デフォルト10秒)が経過してからSIGKILLで強制終了される。実機で確認してみよう。

# シェル形式でsleepを起動(shがPID 1でSIGTERMを無視する) $ docker run --rm -d --name test-signal ubuntu:24.04 sh -c 'exec sleep 600' # stopにかかる時間を計測 $ time docker stop test-signal test-signal real 0m10.394s ← 10秒以上かかっている(SIGKILLで強制終了)

exec形式に変えるとアプリ自体がPID 1になってSIGTERMを直接受け取れる。ただしexec形式に変えても、アプリがSIGTERMを適切に処理しない場合や、シグナルを子プロセスに転送する必要がある複数プロセス構成では問題が残る。

ゾンビプロセスが発生する仕組み

Linuxでは子プロセスが終了すると、カーネルはその終了情報を保持する「ゾンビ状態」(<defunct>)にする。親プロセスがwait()システムコールを呼んで終了情報を回収して初めて、ゾンビは消える。

通常のサーバーではPID 1(systemd)が孤立した子プロセスを引き取り、終了時にwait()してゾンビを回収する。DockerコンテナでアプリがPID 1になっている場合、そのアプリがwait()を自分で呼ばない限りゾンビが蓄積する。実機で確認してみよう。

# PID 1にシェルを置いて子プロセスを次々に起動・終了させる $ docker run --rm -d --name zombie-test ubuntu:24.04 sh -c 'while true; do sleep 0.1 & done' # 少し待ってからプロセスを確認 $ docker exec zombie-test ps aux | grep defunct | head -5 root 312 0.0 0.0 0 0 ? Z 04:15 0:00 [sleep] root 313 0.0 0.0 0 0 ? Z 04:15 0:00 [sleep] root 314 0.0 0.0 0 0 ? Z 04:15 0:00 [sleep] root 315 0.0 0.0 0 0 ? Z 04:15 0:00 [sleep] root 316 0.0 0.0 0 0 ? Z 04:15 0:00 [sleep] # ゾンビの数を確認(放置すると増え続ける) $ docker exec zombie-test ps aux | grep -c defunct 47

<defunct>と表示されているのがゾンビプロセスだ。コンテナが長時間稼働すると、このゾンビが蓄積してシステムのプロセステーブルを圧迫し、やがて新しいプロセスを起動できなくなる。

--initフラグで解決する方法

1. docker runでの--initフラグの使い方

--initフラグを付けるだけで、DockerはコンテナのPID 1としてtiniという軽量なinit処理系を自動的に挿入する。アプリはPID 2以降で動き、tiniがSIGTERMの転送とゾンビ回収を引き受ける。

# --initフラグを付けて起動 $ docker run --rm -d --name with-init --init ubuntu:24.04 sh -c 'while true; do sleep 0.1 & done' # PID 1がtiniになっている $ docker exec with-init ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 876 708 ? Ss 04:30 0:00 /sbin/docker-init -- sh -c while true; do sleep ... root 7 0.0 0.0 2788 952 ? S 04:30 0:00 sh -c while true; do sleep 0.1 & done # ゾンビが溜まらないことを確認 $ docker exec with-init ps aux | grep defunct | wc -l 0

PID 1が/sbin/docker-init(Dockerが組み込んだtiniの別名)になり、アプリのshは子プロセスとして動いている。ゾンビも溜まっていない。docker stopの速度も改善する。

# --initあり:tiniがSIGTERMをアプリに転送して即座に終了 $ time docker stop with-init with-init real 0m0.481s ← ほぼ即時終了

2. Docker Composeでのinit設定(init: true)

Docker Composeでは、サービスの定義にinit: trueを追加するだけで同じ効果が得られる。docker runの--initに相当する設定だ。

# compose.yml services: app: image: myapp:latest init: true # tiniをPID 1として挿入 ports: - "3000:3000" db: image: mysql:8.4 init: true environment: MYSQL_ROOT_PASSWORD: secret

コンテナの本番設計についてはDocker実践講座でも詳しく取り上げている。シグナル処理やリソース制限など、運用で欠かせないポイントを体系的に学べる。

DockerfileにtiniをENTRYPOINTとして組み込む方法

--initフラグやinit: trueはコンテナ実行時に指定する方式だ。イメージ自体にtiniを焼き込んでおけば、フラグを付け忘れてもPID 1問題が発生しない。特にKubernetesやAWS ECSなど、--initオプションを使えない環境では必須になる。

1. Alpineベースイメージでのtini組み込み

Alpineの公式パッケージにtiniが含まれているのでapk addで追加できる。

FROM node:22-alpine # tiniをパッケージマネージャーで追加 RUN apk add --no-cache tini WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . # ENTRYPOINTの先頭にtiniを置く(exec形式必須) ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "server.js"]

tiniを挟んだ後、アプリはtiniの子プロセス(PID 2以降)として起動する。tini --の--は「以降の引数がtiniへのオプションではなくコマンド」であることを示すセパレータだ。

2. Debian・Ubuntuベースイメージでのtini組み込み

Debian・Ubuntuではapt-getでtiniをインストールする。

FROM python:3.13-slim RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["python", "app.py"]

3. tiniのインストールパスを確認する

tiniのインストールパスはディストリビューションによって異なる。ビルド後に確認しておくとよい。

# Alpine のパス $ docker run --rm node:22-alpine which tini /sbin/tini # Debian/Ubuntu のパス $ docker run --rm python:3.13-slim sh -c 'apt-get install -y tini -qq && which tini' /usr/bin/tini # Dockerfileを正しく書けていればビルド後のコンテナで確認可能 $ docker run --rm myapp:latest ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 884 708 ? Ss 05:00 0:00 /sbin/tini -- node server.js root 6 0.2 1.2 681420 49152 ? Sl 05:00 0:00 node server.js

--initフラグとDockerfile組み込みの使い分け

どちらの方法も技術的には同等で、tiniがPID 1に入るという点では同じだ。選択基準を整理する。

観点 --initフラグ(init: true) Dockerfile組み込み
設定の場所 実行時(docker run / compose.yml) イメージビルド時(Dockerfile)
付け忘れリスク あり(実行者に依存する) なし(イメージに焼き込む)
Kubernetes・ECS対応 不可(--initはDocker固有) 可(tini自体はどこでも動く)
イメージのポータビリティ 低い(実行環境依存) 高い(イメージが自律)
導入コスト 低い(既存Dockerfileを変えない) 中程度(Dockerfile修正が必要)

本番・ステージング・KubernetesなどDockerランタイムに依存しない環境で運用する場合は、Dockerfile組み込みが推奨だ。開発環境やローカルで手軽に試したい場合は--initフラグで十分だ。

なお、exec形式のENTRYPOINTを使っていてアプリ自体がSIGTERMを適切に処理し、かつ子プロセスをフォークしない場合は、tiniなしでも問題が起きないケースもある。ただし確認コストを考えると、tiniを組み込んでおく方が安全だ。

よくあるエラーと対処法

1. tiniのパスが見つからず起動に失敗する

DockerfileにENTRYPOINT ["/sbin/tini", "--"]と書いたのに起動時に次のエラーが出る場合は、tiniのインストールパスが異なることが原因だ。

# 起動時のエラー例 docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "/sbin/tini": stat /sbin/tini: no such file or directory: unknown.

Debian・Ubuntuベースのイメージでは/usr/bin/tiniにインストールされる。Alpine系は/sbin/tiniだ。which tiniでパスを確認してからDockerfileに反映する。

# パスを確認してからDockerfileに書く $ docker run --rm python:3.13-slim sh -c 'apt-get install -y tini -qq && which tini' /usr/bin/tini # Dockerfileは確認したパスで記述 ENTRYPOINT ["/usr/bin/tini", "--"]

2. --initを指定してもdocker stopが遅い

【注意】 tiniがPID 1に入っても、アプリ自体がSIGTERMを受け取って自分でシャットダウン処理を行わなければ意味がない。アプリのコードでSIGTERMハンドラーを実装していない場合は、tiniがシグナルを転送しても無視されて10秒後にSIGKILLになる。

Node.jsならprocess.on('SIGTERM', ...)、Pythonならsignal.signal(signal.SIGTERM, ...)でハンドラーを登録する。アプリのSIGTERM対応と--initはセットで考えること。

3. Kubernetesで--initに相当する設定を行う方法

KubernetesのPodSpecには--initフラグに相当するオプションがない。KubernetesでPID 1問題を回避するには、DockerfileにtiniをINSTALLしてENTRYPOINTに指定する方法が唯一の解だ。shareProcessNamespace: trueはPID名前空間をサイドカーと共有する別機能なので混同しないよう注意。

本記事のまとめ

問題 原因 解決策
docker stopが10秒後にSIGKILL PID 1がSIGTERMを無視・転送しない --initフラグまたはtini組み込み
ゾンビプロセスが増え続ける PID 1がwait()でゾンビを回収しない --initフラグまたはtini組み込み
KubernetesやECSでも安全に動かしたい --initはDocker固有オプション DockerfileにtiniをINSTALL・ENTRYPOINT設定
シェル形式CMDでシグナルが届かない shがPID 1でSIGTERMを子に転送しない exec形式CMD + --initまたはtini

PID 1問題は見落とされがちだが、本番コンテナの安定運用には欠かせない知識だ。まずは--initフラグで動作を確認し、本番イメージにはDockerfileへのtini組み込みを採用するとよい。

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、

Dockerを使ったコンテナ設計から本番運用まで、実務で通用するスキルを2日間のハンズオンで学べる講座があります。
>> Docker実践講座の詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録

運営

リナックスマスター.JP(代表 宮崎智広)

株式会社イーネットマーキュリーが運営するLinux教育サイトです。代表の宮崎智広は現役のLinuxサーバー管理者として20年以上の実務経験を持ち、累計3,100名以上のエンジニアを指導してきました。実践型のLinuxセミナーや無料マニュアルの配布を通じてLinux人材の育成に取り組んでいます。