🐳

docker runで-itオプションをつけてもコンテナが終了する理由

2024/09/16に公開

はじめに

Dockerコンテナ内でコマンドを打ちたい時は、コンテナを立ち上げるときのdocker container rundocker run)コマンドに-itオプションをつけると思います。

ではこの-itとは何をしているのか?
そして-itをつけてもコンテナが終了してしまい、コンテナに接続できないことがあるのはなぜ?
というのが気になったので、ChatGPTに聞いてみました。

この記事はChatGPTの解答をまとめ、それにいくつか補足をしたものになります。
少しでも理解の助けになれば幸いです。

環境

  • Docker 27.2.0
  • MacOS

参考

https://wa3.i-3-i.info/word11668.html
https://zenn.dev/suzuki_hoge/books/2022-03-docker-practice-8ae36c33424b59

結論

そもそも-itオプションをつけるとコンテナが持続するようになるケースは結構限られているみたいです。
それがどんなケースなのかというと、実行されるコマンドが対話型のときだと思われます。
対話型のコマンドはたくさんあるのですが、その一つにbashshなどのシェルを立ち上げる系のコマンドがあります。

もっと言うならば、docker container runbashを入れると終了しなくなります。
例えばこんな感じになります。

docker container run -it node:latest bash

これを実行すると、コンテナ内で通常のLinuxコマンドが打てると思います。

コンテナが終了するタイミング

そもそも、Dockerのコンテナはいつ終了するのでしょうか?
そのあたりはこの本が非常にわかりやすかったのでぜひ読んで欲しいのですが、この記事でも触れておこうと思います。

上の本によると、コンテナは一つのプロセスを起動するためにあります。
ここでは便宜上それをメインプロセスと呼ぶことにします。

コンテナはメインプロセスを実行するために起動する という点を押さえておくと、コンテナのことをよりスムーズに理解できるようになります。

ではメインプロセスとはどのように指定するのでしょうか。
それはDockerfileCMD句だったり、docker container rundocker run)コマンドの引数だったりします。

そして、メインプロセスが終了すると、コンテナも自動で停止します。
--rmオプションをつけていた場合は削除もされます。

また、メインプロセスが終了したコンテナは自動で停止する という重要な点を理解できるようになります。

コンテナでプロセスを実行する

では、一度メインプロセスの指定方法を確認しておこうと思います。
前述した通り、メインプロセスの指定方法はいくつかあります。

  • DockerfileCMD句に書く
  • docker container runの引数に書く
  • (Docker Composeの場合、compose.yamlcommandフィールドに書く)

ではここで、Ubuntuをベースとしたコンテナを使って、以下のコマンドを実行してみようと思います。

echo 'Hello World!'

DockerfileCMD句に書く

Dockerfileを使う場合、Ubuntuのイメージをベースとして、新しくイメージを作ることになります。
そしてそのイメージでCMDを指定し、メインプロセスを上書きします。

例えばこのようになります。

Dockerfile
# ubuntuのイメージをベースとして使う
FROM ubuntu:24.04

# メインプロセスを上書きする
CMD ["echo", "Hello World!"]

このイメージをビルドし、コンテナを起動してみます。

ターミナル
docker image build --tag my-ubuntu:latest
docker container run my-ubuntu:latest

すると、ターミナルにHello World!と表示され、コンテナが停止すると思います。
コンテナ一覧を見て確認してみてください。

ターミナル
docker container ls --all

# 実行結果
CONTAINER ID   IMAGE              COMMAND                 CREATED              STATUS                          PORTS     NAMES
d174f2a3bbf8   my-ubuntu:latest   "echo 'Hello World!'"   About a minute ago   Exited (0) About a minute ago             jolly_jackson

STATUSExited (0)になっているのが確認できると思います。
Exitedなので停止していて、0なのでメインプロセスは正常に終了したみたいです。

docker container runの引数に書く

runコマンドの引数にコンテナのメインプロセスとなるコマンドを書くこともできます。
その場合は、例えばこのようになります。

ターミナル
docker container run \
    ubuntu:24.04 \
    echo Hello World!

# 実行結果
Hello World!

無事にechoコマンドが実行できたようです。
そして、コンテナが停止したのが確認できると思います。

docker container ls --all

# 実行結果
CONTAINER ID   IMAGE          COMMAND               CREATED         STATUS                     PORTS     NAMES
a76ded1d33a2   ubuntu:24.04   "echo Hello World!"   2 minutes ago   Exited (0) 2 minutes ago             relaxed_leakey

Dockerfileの時と同じく、StatusExited (0)のようになっているはずです。


ここでコンテナが停止したのは、メインプロセスの実行が終了したからです。
先ほども言った通りコンテナはプロセスを実行するためにあり、そのプロセスが終了すると停止します。
そのため、与えられたコマンドを実行し終わって役目を終えたコンテナは、自動的に停止されます。

また、今回実行したechoコマンドは即座に実行が終了するコマンドです。
そのため、コンテナが起動するとechoコマンドが実行されて終了し、同時にコンテナも停止してしまいました。

-itオプションとは

docker container rundocker run)コマンドの-itとは、主にコンテナをシェルで操作するときに使われるオプションです。
これは-iオプションと-tオプションを組み合わせたものであり、それぞれに役割があります。

--interactive-i

--interactive-iのロング版)オプションは、コンテナの標準入力に接続します。
コンテナの標準入力とホストの標準入力を接続することで、ホストのターミナルに入力した内容がコンテナに反映されるようになります。

コンテナの標準入力は、後述する「対話型のコマンド」が終了するタイミングに影響を与えます。

--tty-t

--tty-tのロング版)オプションは、コンテナに仮想端末(TTY)を割り当てます。
仮想端末を割り当てることで、ターミナルのような環境を提供するそうです。
ただし、仮想端末が割り当てられていなくてもコマンドを実行することはできます。

仮想端末とは仮想的にターミナルを作ること?みたいです。
詳しくはこちらこちらが参考になると思います。

仮想端末の有無は、後述する「対話型のコマンド」が終了するタイミングに影響を与えることがあります。
ChatGPTによると与えないこともあるみたいです。

--itをつけるとコンテナが永続する例

では、--itオプションをつけることによってコンテナが永続する例を見てみたいと思います。
ここでは先ほどに続き、Ubuntuのイメージを使おうと思います。

まずはオプションなしで普通にコンテナを起動してみます。

ターミナル
docker container run ubuntu:24.04

すると起動はできるものの、すぐにコンテナが停止するのがわかると思います。

では次に、-itオプションをつけて実行してみます。

ターミナル
docker container run -it ubuntu:24.04

するとプロンプトが変わり、コンテナ内でコマンドが打てるようになったと思います。
先ほどはコンテナがすぐに停止してしまいましたが、これは一体どういうことなのでしょうか?

それを理解するには、まず先にUbuntuのデフォルトコマンドであるbash、そして対話型のコマンドの特性について知る必要があります。

対話型のコマンドとは

ここで言う「対話型のコマンド」とは、標準入力を待機するコマンドです。
もっと言うなら、起動すると何か入力できるコマンドです。

対話型のコマンドはたくさんありますが、ここではそのうちのいくつかを紹介します。

  • bashshなどのシェルを立ち上げるコマンド
  • vimなどのテキストエディタ
  • nodepython(オプションや引数なし)コマンドで立ち上がるREPL
  • mysqlpsqlなどの対話型のツール

なお、対話で使うことはほとんどないと思われるgrepも、標準入力を受け取れるので対話型のコマンドです。

コマンドの終了タイミング

コンテナ起動時に実行されるコマンドの終了タイミングは、そのままコンテナの生存期間に影響します。
では、対話型のコマンドはいつどのように終了するのでしょうか?

それは、コマンドをどのように実行したかで変わります。
ここではbashコマンドを例に挙げて解説します。

引数なしで実行する

まずは特に引数やオプションをつけず、通常通り実行してみようと思います。
ターミナル(ホストでもUbuntuのコンテナ内でも可)で、以下のようにコマンドを実行しました。

ターミナル
host$ bash

# プロンプトが変わる
bash-3.2$ echo Hello World!
Hello World! # コマンドが実行できる

見ての通りプロンプトが変わり、bashのプロセス内で(?)コマンドが実行できるようになりました。
この時exitコマンドを入力するまで bashのプロセスは終了していない 点に注目してください。

標準入力でコマンドを入れて実行する

bashコマンドは、標準入力にコマンドを入れることでそのコマンドを実行してくれます。
ということで、echoコマンドを使って、echo Hello World!というコマンドをbashの標準入力に与えてみようと思います。

ターミナル
echo echo Hello World! | bash

# 実行結果
Hello World!

この時、bashコマンドは echo Hello World!を実行し終わった時点で終了している点に注目してください。

また、bashコマンドは引数にファイルパスを入れることで、そのファイルを実行することができますが、ここでは扱いません。

標準入力に何も入れずに実行する

ChatGPTに聞いたところ、どうやら/dev/nullというのを使うと「空」が表現できるみたいです。
ということで、これをbashコマンドの標準入力に渡してみます。

ターミナル
bash < /dev/null

すると、bashコマンドは何もせず終了してしまいました。
プロンプトも変わらず、すぐに終了したみたいです。

対話型のコマンドとDockerコンテナ

前述した通り、対話型のコマンドはユーザーからの入力を受け取るのに標準入力を使用します。
では、Dockerコンテナにおいて標準入力の扱いはどうなっているのでしょうか。

対話型のコマンドは標準入力が空だと終了してしまうので、コンテナのメインプロセスが対話型のコマンドである場合は標準入力の有無がコンテナの停止に直結することになります。

つまり、メインプロセスが対話型のコマンドであるコンテナに、永続するタイプの標準入力を与えることができれば、そのコンテナは永続するということになります。
そんな方法は果たしてあるのでしょうか。いや待て、どこかで似たような言葉を聞いたような...

そう、それがdocker container runコマンドの-i(と-t)オプションです。
流れはこのようになります。

  1. -itオプションによってホストターミナルの標準入力とコンテナの標準入力を結びつける
  2. 対話型のコマンドのプロセスが終了しなくなる
  3. プロセスを手動で終了するまでコンテナが停止しなくなる

つまり、-itオプションそのものにコンテナを永続させる効果はなく、対話型のコマンドを永続させることによって間接的にコンテナを永続させることがある、ということになります。

対話型以外ののコマンド

では、対話型以外のコマンドはいつ終了するのでしょうか?
これは標準入力に左右されないため、元々のコマンド固有の動きをすることになります。

例えばlsコマンドを見てみます。
このコマンドは指定されたディレクトリの中身を表示し、すぐに終了します。

lsのようにすぐに終了するコマンドをコンテナのメインプロセスとして指定した場合、そのコンテナはすぐに終了します。
例えば以下のようなDockerfileが該当します。

Dockerfile
FROM ubuntu:24.04
CMD ["ls"]

これをビルドし、コンテナとして起動してみました。
すると、以下のようにlsコマンドの結果が表示され、コンテナはすぐに終了すると思います。

ターミナル
docker image build --tag my-ubuntu:latest .
docker container run my-ubuntu:latest

# 実行結果
bin
boot
# 略
usr
var

では、ここで-itオプションをつけてコンテナを起動してみます。

ターミナル
docker container run -it my-ubuntu:latest

# 実行結果
bin  boot  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

-iによってlsコマンドに標準入力が与えられるのですが、lsは標準入力を受け取らず、標準入力はプロセスの終了タイミングに影響しません。
そのため、-itオプションをつけてもコンテナはすぐに停止するはずです。

出力が変わる理由

-itオプションをつけたときとつけなかったときで、lsコマンドの出力結果が少し変わったことにお気づきでしょうか。
違いは区切りが改行かスペースかの些細なものですが、スペース区切りの方が読みやすいと思います。
これは-itのうち-t--tty)オプションに由来されるものです。

--ttyオプションの効果は、コンテナに仮想端末を割り当てるというものでした。
そして、仮想端末が割り当てられていると、コマンドの出力がフォーマットされることがあります。
(コマンドが内部で仮想端末を使っている?)
今回は仮想端末によってlsコマンドの結果がフォーマットされ、スペース区切りで表示されるようになったのだと思われます。

結局どうすればいいの?

少し長くなってしまったので、コンテナを停止させず中に入って作業する方法をまとめていきます。

対話型のコマンドを使う

まずは、この記事で紹介した対話型のコマンドを使う方法です。
対話型のコマンドはいろいろありますが、内部でコマンドを叩いて作業をする場合はbashを使っておけばいいと思います。
また、対話型のコマンドで永続化させる場合は起動時に-itオプションが必要になります。

例えば、oven/bunというイメージをそのまま起動した場合、-itをつけてもコンテナはすぐに終了してしまいます。
これはCMDに指定されているコマンドがすぐに終了するものであることが原因です。

対処方法は簡単で、メインプロセスをbashに上書きするだけです。
bash以外の対話型のコマンドでもいけますが、コンテナ内でコマンドを打ちたい場合はシェルを起動するコマンドを使います。

ターミナル
docker container run -it oven/bun:latest bash

やったことは最後にbashをつけただけです。

一生終わらないコマンドを使う

上で紹介した方法は、標準入力があるか=-itオプションがあるかどうかでコンテナが永続するかどうかが変わっていました。
それでもいいですが、そもそも対話型のコマンドを使わなくても一生終わらないプロセスをメインプロセスに設定すればいいだけの話でもあります。

今回は一生終わらないプロセスとして、以下のコマンドを使います。(こちらを参考にしました)

tail -F /dev/null

このコマンドをメインプロセスに設定するには、DockerfileCMDにこれを設定します。

Dockerfile
FROM oven/bun:latest

# 上書き
CMD ["tail", "-F", "/dev/null"]

そしてこれをビルドします。

ターミナル
docker image build --tag my-bun .

そしたらコンテナを起動するのですが、この手順には注意が必要です。

  • デタッチモード(-d)で実行すること
    • 最悪ターミナルが操作できなくなり、別の場所からコンテナを終了する羽目になる
  • -itはつけなくてOK
    • この何もしないプロセスに標準入力は不要
    • -itをつけるとCtrl+C * 3で停止できなくなる

ということで、早速起動してみます。

ターミナル
docker container run -d my-bun:latest

# 実行結果
97fc7483127fdc1なんたら

デタッチモードで起動したので、コンテナは今バックグラウンドで動いています。
そのためプロンプトは変わっていませんが、これは正しい動作です。

コンテナが停止しなくなったので、次はこのコンテナに接続してみます。
接続にはdocker container execという、コンテナ内でコマンドを実行するコマンドを使います。
コマンドを実行する前に、先ほど出力されたコンテナIDをコピーしておいてください。

コマンドはこちらです。

ターミナル
docker container exec -it <コピーしたコンテナID> bash

実行した結果、プロンプトが変わってbunコマンドが打てるようになれば成功です。


ちなみに、exec-itrun-itとだいたい同じです。
コンテナに接続するというのはつまり、コンテナ内でbashコマンドを実行し、そこに

  • 現在のターミナルの標準入力をコンテナの標準入力と結びつける
  • 仮想端末を割り当てる

ということになります。

結局対話型のコマンド...というかbashを使っている時点で1つ目の方法と何も変わっていないような気はしますが、一応書いてみました。

Discussion