SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

docker compose execで対話型シェルを開く方法

実行中のComposeサービスへdocker compose execでシェルを開く方法を解説します。bashがない場合のashや、停止中にrun --rmを使う違いも確認できます。

docker compose exec で対話型シェルを取得する

docker compose exec web bash は、web サービスとしてすでに実行中のコンテナ内で対話型シェルを開きます。exec の後に指定する名前は、コンテナ名ではなく compose.yaml に記載されたサービス名です。イメージに bash がない場合は、代わりに sh を指定します。

docker compose ps
docker compose exec web bash

最初に docker compose ps を実行します。webrunning の状態で一覧表示されるはずです。次に2つ目のコマンドを実行すると、コンテナ内のプロンプトが表示されます。exit または Ctrl-D を入力するとホストに戻ります。exec はメインプロセスとは別の2つ目のプロセスを起動するため、シェルを終了してもサービスは実行を続けます。シェルを閉じても PID 1(プロセス ID 1)には影響しません。PID 1 は、コンテナが実行するように構築されたプロセスです。

コンテナに入る方法は2つあり、これはその1つです。exec は、すでに存在するコンテナに接続します。docker compose run は、同じサービス定義から新しいコンテナを作成します。この違い1つから、このガイドのほぼすべての説明が導かれます。

Compose では -it が任意ですが、通常の docker では必須である理由

セッションの対話部分は、2 つのフラグで制御します。-i は標準入力を開いたままにするため、入力した内容がプロセスに渡ります。-t は TTY と呼ばれる疑似端末を割り当てるため、シェルがプロンプトを表示し、矢印キーを処理できます。通常の docker exec では、デフォルトで両方が無効です。そのため、これまでのすべての例で docker exec -it を指定しています。docker compose exec は両方を有効にするため、docker compose exec -it web bashdocker compose exec web bash は同じ動作になります。Compose でも -it を指定できるため、従来の操作方法をそのまま使えます。

TTY がないことは数秒で分かります。シェルは動作しますが、プロンプトが表示されず、Ctrl-C もプロセスに届きません。反対に、Compose に TTY を割り当てないよう指定する必要がある場合は、専用のフラグと、この後の専用セクションで説明します。

bash がないイメージでの対処

Alpine ベースのイメージで bash を指定すると、exec は次のように失敗します。

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

このメッセージは exec の問題を示しているのではありません。指定したバイナリがイメージ内にないという意味です。Alpine には BusyBox が含まれており、ash/bin/sh として提供されますが、bash は含まれていません。そのため、sh を指定します。

docker compose exec web sh

-slim タグを含む Debian ベースおよび Ubuntu ベースのイメージには bash が含まれており、bash ではコマンド履歴とより優れた補完機能を利用できます。そのため、まず bash を試し、失敗したら sh に切り替えます。sh は、ほぼすべての汎用イメージに存在します。

シェルがまったくないイメージもあります。Distroless イメージや FROM scratch 構成されたイメージには、意図的にアプリケーションのバイナリとそのライブラリしか含まれていません。存在しないシェルは攻撃に利用できないためです。このようなイメージでは sh も同じメッセージで失敗し、他に試せる方法はありません。利用できる方法は 2 つあります。Google の distroless イメージには BusyBox シェルを追加した :debug タグが用意されています。一時的にそのタグへ切り替えると、コンテナに入れます。もう 1 つは、対象コンテナの namespace 内で別のコンテナを起動する方法です。

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

これで、netshoot のツールをアプリケーションのネットワークに接続した状態で利用できます。そのため、curl localhost:8080ss -lntp は対象コンテナ内で実行した場合と同じように動作します。表示されるファイルシステムはアプリケーションのものではなく、netshoot のものです。プロセス namespace は共有されているため、root で実行すれば ls /proc/1/root/ から対象コンテナ自身のファイルにアクセスできます。

サービスが起動していない場合は、docker compose run --rm を使用します

exec の実行には、起動中のコンテナが必要です。停止中のサービスを指定すると、拒否されます。

service "web" is not running

自動的に起動することはありません。docker compose run は起動します。

docker compose run --rm web bash

run は、web サービス定義から新しいコンテナを作成します。同じイメージ、環境変数、ボリューム、ネットワークを使用し、入力したコマンドでサービスのコマンドを置き換えます。--rm は、終了時にそのコンテナを削除します。--rm を付けないと、残ったコンテナが myproject-web-run-4f1c2b のような名前で蓄積します。docker compose ps -a で確認できますが、それ以外の処理では削除されません。

run には、注意が必要な動作が 2 つあります。--service-ports を追加しない限り、サービスのポートは公開されません。これは意図した動作です。1 つ目のコンテナがホストの 8080 番ポートを使用中に、2 つ目のコンテナが同じポートをバインドすると、bind: address already in use で失敗するためです。また、シェルが表示される前に、サービスが depends_on に列挙しているすべてのサービスを起動します。そのため、内部を簡単に確認するだけでも、データベースやキャッシュが起動する場合があります。--no-deps を指定すると、この動作を無効にできます。

run はイメージの ENTRYPOINT を経由しますが、exec は経由しません。exec は既存のコンテナ内でコマンドを直接起動するため、entrypoint スクリプトはそのコマンドを受け取りません。run では、bash がそのスクリプトへの引数として渡されます。公式イメージの多くは entrypoint の末尾で exec "$@" を実行するため、引数がそのまま渡され、シェルを使用できます。引数を独自に解釈するスクリプトでは別の処理が行われます。その場合は、その実行に限って entrypoint を置き換えます。

docker compose run --rm --entrypoint sh web

exec では動作するコマンドが run では異なる動作をする最も一般的な理由は、これです。command と entrypoint の違いを確認すると、毎回イメージ設定のどの部分を置き換えているのかが分かります。

exec または run: どちらを選ぶか

  • exec には起動中のコンテナが必要です。run には必要ありません。run は依存関係も起動する場合があります。
  • exec では、実行中のプロセス一覧と現在のファイルを確認できます。アプリケーションの起動後に書き込まれた内容も含まれます。run ではイメージのクリーンなコピーが使われるため、そのような内容はありません。
  • exec では entrypoint を実行しません。run では実行します。
  • --rm を指定しない限り、run の実行後もコンテナが残ります。

実際に何が起きているかを確認するには、exec を使用します。同じ環境の一時的なコピーを使う場合、1 回限りのマイグレーションコマンドを実行する場合、または実際のサービスが exec で接続できるほど長く起動し続けない場合は、run --rm を使用します。

便利な exec フラグ: user、作業ディレクトリ、レプリカ

多くのイメージは root 以外のユーザーに切り替わるため、exec シェル内で診断ツールをインストールする場合は、ここで止まります。

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u rootを指定すると、同じコンテナ内で root シェルを取得できます。

docker compose exec -u root web sh

-w /srv/appは、そのコマンドに限って作業ディレクトリを設定します。-e KEY=valueは、サービスではなくセッションに環境変数を追加します。サービスが複数のレプリカを実行している場合、--index 2によって接続先のコンテナを選択します。マウントしたディレクトリのファイル所有権を調査している場合は、コンテナイメージ内の PUID と PGIDで、書き込み可能なユーザーを決めるのはユーザー名ではなく数値 ID である理由を確認できます。

データベースコンテナ内で psql または mysql シェルを起動する

クライアントはすでにデータベースイメージに含まれています。そのため、ホストにクライアントを用意する必要も、ポートを公開する必要もありません。

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

Postgres イメージには psql、MySQL イメージには mysql、MariaDB イメージには mariadb が含まれています。コンテナ内から接続するため、Compose ファイルでデータベースのポートを一切公開していなくても動作します。これはより安全な構成です。公開していないポートには、インターネット上の何者も接続できません。

1 つ注意すべき点があります。Docker がコマンドを処理する前に、シェルがホスト上で変数を展開します。そのため、その変数がコンテナ内にしか存在しない場合、-U "$POSTGRES_USER" は空の文字列を渡します。単一引用符とコンテナ内のシェルを使うと、適切な場所で変数が展開されます。

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

ここでコマンドを指定せずに docker compose run --rm db を使わないでください。同じデータボリュームに対して 2 つ目の Postgres サーバーを起動することになり、起動に失敗します。

FATAL:  lock file "postmaster.pid" already exists

ロックファイルは正常に機能しています。2 つのサーバーが 1 つのデータディレクトリに書き込むと、データが破損するためです。データベースが起動している間に、実行中のコンテナへ exec で入ってください。データベースを Compose で運用するかどうかは別の判断です。データベースを Docker とホストのどちらで運用するかでは、そのトレードオフを説明しています。

起動時にコンソールを必要とするサービス: stdin_open と tty

exec と run は、手動で開くシェルを対象とします。メインプロセスが本質的に対話型であるサービスには、compose ファイルに次の2つのキーが必要です。

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: truedocker run -i で、tty: truedocker run -t です。これらがない場合、コンテナは起動直後に終了コード 0 で終了し、docker compose ps -a には Exited (0) が表示されます。クラッシュしたわけではありません。stdin にターミナルがない状態で python を実行すると、直ちに end of file を読み取り、正常に終了します。誰も入力しない環境では、これが正しい動作です。

2つのキーを設定したら、実行中のプロセスに接続します。

docker attach $(docker compose ps -q console)

Ctrl-P に続けて Ctrl-Q を押すと、プロセスを実行したまま切断できます。この操作は、コンテナで TTY と stdin の両方が開いている場合にのみ機能します。一方、Ctrl-C は PID 1 に割り込みを送り、サービスを停止します。

通常のサービスでは、両方のキーを設定しないでください。Web サーバーが stdin を読み取ることはありません。また、tty: true によって、人が画面を見ていると判断した多くのプログラムがカラー出力と行バッファリングに切り替わり、docker compose logs がエスケープコードで埋まるためです。

cron と CI でスクリプト化した exec が失敗する理由: -T フラグ

ターミナルでは動作する exec コマンドが、cron ジョブや継続的インテグレーション (CI) ランナーでは失敗します。

the input device is not a TTY

Compose はデフォルトで擬似端末を要求します。一方、cron はジョブに端末を割り当てないため、コマンドが実行される前に要求が失敗します。-T を指定すると、この要求を無効にできます。

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T にはもう 1 つ理由があります。TTY は出力時にバイトストリームを書き換えるため、圧縮ダンプを TTY 経由で渡すと内容が破損します。リダイレクトまたはパイプする出力には -T が必要です。

cron には、さらに 2 つ注意点があります。-f には絶対パスを指定してください。cron は compose ファイルが存在しないホームディレクトリからジョブを実行するため、Compose は no configuration file provided: not found で停止します。また、exec は実行したコマンドの終了コードを返します。そのため、失敗する pg_dumpset -e のもとでスクリプトを失敗させます。空のバックアップを作成して成功を報告することはありません。日常的に使うその他のコマンドは、スクリプトのそばに置いておく価値のあるCompose コマンドチートシートにまとめています。

コンテナ内で行った変更が消える理由

exec でツールをインストールし、設定ファイルを編集して問題を修正しても、1 週間後には修正が消えていることがあります。これは、コンテナの書き込み可能レイヤーが設計どおりに動作しているためです。docker compose up -d は、イメージタグまたはサービス定義を変更すると古いコンテナを削除し、イメージから新しいコンテナを作成します。そのため、手動で行った編集は古いコンテナとともに失われます。

docker compose restart は動作が異なります。同じコンテナを停止して再起動するため、手動で行った編集は保持されます。そのため、手動の修正が数週間維持された後、関係のない更新時に消えることがあります。名前付きボリュームと bind mount は、どちらの操作でも保持されます。データがコンテナの外部に保存されるためです。保持するデータにどちらを選ぶべきかは、bind mount と名前付きボリュームで説明しています。

そのため、exec シェルは内容の確認とテストを行う場所として扱ってください。修正方法が分かったら、変更が保持される場所に記述します。パッケージは Dockerfile に、設定は compose ファイルに記述します。その後、docker compose up -d で適用し、別の exec を実行して、新しいコンテナに実際に反映されていることを確認してください。

FAQ

docker compose exec と docker compose run の違いは何ですか?

exec は、すでに起動しているコンテナ内でメインプロセスとは別にコマンドを実行し、イメージの entrypoint をスキップします。run は、同じサービス定義から同じイメージ、環境変数、ボリューム、ネットワークを使用して新しいコンテナを作成し、コマンドを entrypoint 経由で実行します。また、先に depends_on サービスを起動します。run では、--service-ports を追加しない限り、サービスのポートも公開されません。稼働中のサービスを調べる場合は exec を使用します。サービスが停止している場合や、サービスに影響を与えたくない場合は run --rm を使用します。

docker compose exec でサービスが起動していないと表示されるのはなぜですか?

exec は既存のコンテナに接続するだけで、コンテナを作成できません。そのため、サービスが停止またはクラッシュしていると service "web" is not running になります。docker compose ps -a を確認すると、Exited (1) などの状態で終了したコンテナを一覧できます。停止した理由は docker compose logs web で確認します。シェルを起動するには docker compose run --rm --entrypoint sh web を実行します。これは、同じサービス定義から新しいコンテナを作成し、問題のある起動コマンドを実行せずに起動します。

イメージに bash がない場合、シェルを開くにはどうすればよいですか?

docker compose exec web bashexec: "bash": executable file not found in $PATH で失敗する場合、イメージに bash がありません。Alpine ベースのイメージでは通常の状態です。BusyBox が /bin/sh を提供するため、docker compose exec web sh を使用します。Distroless イメージと scratch イメージにはシェルがまったくないため、exec コマンドは機能しません。イメージの公開元が提供している場合は、イメージの :debug タグに切り替えます。または、docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot で対象の namespace 内にデバッグ用コンテナを起動します。$CIDdocker compose ps -q web から取得します。

cron で exec コマンドを実行すると「入力デバイスは TTY ではありません」と表示されるのはなぜですか?

docker compose exec はデフォルトで擬似端末を要求しますが、cron は擬似端末を提供しないため、コマンドを実行する前に失敗します。-T を追加して無効にします: docker compose exec -T db pg_dump -U postgres app。リダイレクトまたはパイプで出力する場合も -T を使用してください。TTY はバイトストリームを変更するため、バイナリダンプが破損する可能性があります。cron では compose ファイルの絶対パスとともに -f も指定してください。指定しないと Compose は no configuration file provided: not found で終了します。

exec でコンテナ内に加えた変更は、再起動後も残りますか?

同じコンテナを再利用する docker compose restart の場合は残ります。イメージまたは設定を変更した後に docker compose up -d を実行すると、イメージからコンテナが再作成され、書き込み可能レイヤーが破棄されるため、変更は失われます。named volume または bind mount に書き込んだデータはコンテナの外部に保存されるため、どちらの場合も残ります。exec で診断用の変更を行い、恒久的な変更は Dockerfile または compose ファイルに記述してください。