⭐️tini

https://github.com/krallin/tini

  • コンテナ用の超軽量initプロセス
  • 一言でいうと「コンテナの中に本物のPID 1(init)を立ててあげるツール」

前提: なぜコンテナはPID 1問題を抱えるのか

  • 通常のLinuxマシンではPID 1はinit(systemdなど)で、以下を専門に担当している
    • シグナルの正しい転送・処理
    • 親を失った孤児プロセスを引き取り、終了時にwait()で回収する
  • コンテナではこのinitを省略し、アプリ自身がPID 1になるケースが多い
    • しかしアプリは「自分がinitになる」ことを想定して作られていないため、上記の役割を果たせない

役割(何を代行するか)

  • シグナルの転送: docker stop(SIGTERM)などを受け取り、子プロセス(実際のアプリ)へ正しく転送する
  • ゾンビプロセスの回収(reap): 孤児化した子プロセスを引き取り、wait()して片付ける

よくある使い方

  • Dockerにtiniが組み込み済みなので、--initフラグを付けるだけでも良い
    docker run --init myimage
    
    • このフラグを付けると、Dockerエンジンがdocker-init(静的リンクされたtini相当)を自動的に注入してPID 1にする
    • イメージ側(myimage)は何も変更しなくてよい。Docker側が「docker-init → アプリ」という親子関係を作ってくれる
      docker run --init myimage
              │
              ▼
        Dockerが docker-init(tini相当) を注入し、PID 1として起動
              │  fork + exec
              ▼
        docker-init(PID 1) ──▶ your-app(子プロセス)
              │
              └─ SIGTERM転送・ゾンビ回収を代行
      
    • ただしこれはdocker runのフラグなので、起動コマンドを自前で指定できないCloud Run等では使えない(→ 後述のENTRYPOINT方式が必要)
  • Dockerfileで明示的にENTRYPOINTに指定する方法もよく使われる(アプリはCMD側に書く)
    ENTRYPOINT ["/tini", "--"]
    CMD ["your-app"]
    

Cloud Runなどdocker runできない環境の場合

  • Cloud RunやKubernetesなどは起動コマンドを自前で指定できず、--initフラグは使えない
  • その場合はENTRYPOINT方式一択。イメージにtiniを焼き込んでおけば、実行環境側の起動方法によらず常にPID 1として動く
    RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
    ENTRYPOINT ["/usr/bin/tini", "--"]
    CMD ["your-app"]
    

このコマンドの意味

  • ENTRYPOINTとCMDはexec形式(配列表記)だと連結され、実際には /usr/bin/tini -- your-app というコマンドラインが実行される
    • /usr/bin/tini: tini実行ファイルのフルパス。tiniだけ(フルパスなし)でもPATHが通っていれば動くが、ENTRYPOINTは生死を握る最重要プロセスなのでPATH依存を避けてフルパス指定するのが公式推奨
    • --: 「オプションはここまで」を示す区切り。以降のyour-app側の引数をtini自身のオプションと誤解釈させないための安全策
    • your-app: tiniが実際に起動する対象プロセス(CMD側)

Cloud Runでの内部動作

① Cloud Runがインスタンスを起動
        │
        ▼
② コンテナ起動: /usr/bin/tini -- your-app が実行される
        │
        ▼
③ tini が PID 1 として起動
        │  fork + exec
        ▼
④ your-app が子プロセスとして起動(リクエスト処理を開始)
        │
        ▼
⑤ Cloud Runがインスタンスを停止する時
     Cloud Run ──SIGTERM──▶ tini(PID 1) ──転送──▶ your-app
                                                        │ graceful shutdown
                                                        ▼
                                                       exit
                                                        │
                                          tini が wait() で回収
                                                        ▼
                                        tini も同じ終了コードでexit
                                                        ▼
                                                 コンテナ終了 ✅
  • tiniはアプリをexec置換するのではなくforkで子として起動し、自分はPID 1に居座り続ける
  • これによりSIGTERMを正しく中継でき、your-appが生成する孫プロセスのゾンビ化も防げる

実際に確認してみる

1. --initの有無でプロセスを比較

docker run -d --name til-no-init alpine sleep 300
docker exec til-no-init ps
# PID 1 = sleep 300 (initなし=アプリ自身がPID 1)

docker run -d --init --name til-with-init alpine sleep 300
docker exec til-with-init ps
# PID 1 = /sbin/docker-init -- sleep 300
# PID 7 = sleep 300      (initあり=docker-initが挟まる)
PID 1子プロセス
--initなしsleep 300なし
--initあり/sbin/docker-init -- sleep 300sleep 300