Docker 入門

Docker API とクライアントライブラリ

本チュートリアルで一貫して使用してきた Docker コマンドラインインターフェース(CLI)は、コンテナの実行からイメージのビルド、ネットワークの管理に至るまで、極めて強力なツールです。しかし、これは Docker とインタラクト(対話)するための一つの手段に過ぎません。その内部構造(Under the hood)において、Docker CLI は RESTful API を介して Docker Daemon と通信を行っています。このアプリケーションプログラミングインターフェース(API)こそが Docker のプログラマブルなインターフェースであり、よりきめ細かいコントロール、オペレーションのオートメーション(自動化)、そして他のシステムとのディープなインテグレーション(統合)を可能にします。

Docker API を直接理解し活用すること(通常は SDK とも呼ばれるクライアントライブラリ経由)は、無限の可能性を秘めた新しい世界への扉を開きます。カスタムツールの開発、複雑なオーケストレーションの実行、自動化されたデプロイメントパイプラインの構築、あるいは Docker の機能を独自のアプリケーションへ直接インテグレートすることが可能になります。本章では、Docker API のベールを脱ぎ、クライアントライブラリを活用してコードベースで Docker 環境とインタラクトする方法を解説します。

1. Docker Daemon API

コアなレベルにおいて、Docker はクライアント・サーバー(Client-Server)モデルを採用しています。Docker Daemon(通常は単に「Docker エンジン」と呼ばれます)は、ホストマシンのバックグラウンドで稼働するサーバーコンポーネントです。イメージのビルド、コンテナの実行、ネットワーク管理、そして永続化ストレージの処理といった、あらゆる重い処理(Heavy lifting)を引き受けます。一方、Docker CLI はクライアントの役割を果たし、Daemon に対してコマンドを送信します。

この通信こそが Docker API を介して行われているものです。これは RESTful API であり、標準的な HTTP リクエスト(GET、POST、PUT、DELETE)を利用して特定のエンドポイントへデータを送信し、通常は JSON フォーマットで結果を返します。あなたが実行するすべての docker コマンドは、1つまたは複数の API コールへと変換されます。例えば、docker run hello-world と入力すると、CLI はこれを Docker Daemon への API リクエストに変換し、hello-world イメージから新しいコンテナを作成して起動するように要求します。

1.1 API エンドポイントのエクスポーズ方式

Docker Daemon は、いくつかの異なるメカニズムを通じてその API をエクスポーズ(公開)します:

  • Unix ソケット (Unix Socket, Linux/macOS): Linux および macOS では、Docker Daemon は通常 /var/run/docker.sock に配置された Unix ソケットを通じて API をエクスポーズします。これはローカルで Daemon とインタラクトするためのデフォルトかつ最もセキュアな方法です。なぜなら、ソケットへのアクセスはファイルシステムパーミッションによって制御されるためです。通常、docker ユーザーグループのメンバー(または root ユーザー)のみがアクセス可能です。
  • 名前付きパイプ (Named Pipe, Windows): Windows では、Docker Daemon はローカル通信に名前付きパイプ(通常は \\.\pipe\docker_engine)を使用します。
  • TCP ソケット (TCP Socket): Docker Daemon を TCP ポート(通常は非暗号化の 2375 または TLS 暗号化された 2376)をリッスンするように設定することも可能です。これにより Docker API へのリモートアクセスが許可され、他のマシン上の Docker Daemon を管理できるようになります。セキュリティ上の理由から、TLS 暗号化なしで TCP 経由で Docker API をエクスポーズすることは 絶対に推奨されません。そのポートにアクセスできる者であれば誰でも、あなたの Docker ホストを完全にコントロールできてしまうからです。

2. なぜ直接 API とインタラクトするのか?

Docker CLI は手動でのオペレーションやシンプルなシェルスクリプトの記述には非常に適していますが、クライアントライブラリを通じて直接 API とインタラクトすることは、以下のシナリオにおいて不可欠です:

  • オートメーション (Automation): カスタムスクリプトやアプリケーションをビルドし、特定のトリガーやスケジュールに基づいて、コンテナの動的なスケーリング、サービスのデプロイ、または複雑な管理タスクを自動実行します。
  • システムインテグレーション (System Integration): Docker の機能を既存のモニタリングシステム、CI/CD パイプライン、またはプロプライエタリなプラットフォームに組み込みます。例えば、ダッシュボードアプリケーションが各 Docker ホストの API にクエリを実行し、クロスホストでのコンテナステータスを表示するといったことが可能です。
  • カスタムツール (Custom Tools): CLI の機能を超える専用ツールを開発し、ユニークなユーザーインターフェース(UI)を提供したり、Docker のオペレーションを他のシステム機能と組み合わせたりします。
  • 動的ワークフロー (Dynamic Workflows): Docker リソースの動的なプロビジョニング(構成)、管理、および破棄をコアロジックの一部として組み込んだアプリケーションを作成します。

2.1 直接インタラクトの実際のユースケース

  • クラウドサービスプロバイダーのインテグレーション: AWS ECS、Google Kubernetes Engine (GKE)、または Azure Container Instances (ACI) などのクラウドサービスを使用する場合、これらのサービスは通常、基礎となるコンテナランタイム(Docker または互換性のある containerd)の API を使用してインタラクトします。例えば、クラウド環境のオートスケーリンググループは CPU 使用率をモニタリングし、閾値を超えた場合に API をコールして仮想マシン上に新しい Docker コンテナをプロビジョニングします。
  • CI/CD パイプラインのカスタマイズ: Jenkins や GitLab CI などの CI/CD サーバーは、コードのコミット成功後にプラグインやカスタムスクリプトを通じて直接 Docker API とインタラクトし、イメージのビルド、レジストリ(Registry)へのプッシュ、テスト環境へのデプロイを行います。これにより、シンプルな CLI コマンドでは実現不可能な、高度にカスタマイズされたデプロイメント戦略が可能になります。

想定シナリオ:スマートホームシステム

様々なマイクロサービスが Docker コンテナ内で稼働するスマートホーム・オートメーションシステムを構築していると想像してください。このシステムには以下の要件があります:

  • あなたが外出している時にのみ、「セキュリティカメラ映像」コンテナを起動する。
  • あなたが就寝している間は、リソースを節約するために「ミュージックサーバー」コンテナを停止する。
  • 新しいバージョンが利用可能になった際、「ウェザーステーション(気象観測)」コンテナのイメージを動的にアップデートする。

独自のスマートホームハブアプリケーションは、Docker クライアントライブラリを使用してステータスをチェックし、特定のコンテナを起動/停止するための API コールを実行したり、手動で docker コマンドを入力することなく、アップデートされたイメージをプルしてコンテナを再ビルドしたりすることができます。

3. Docker クライアントライブラリ (Client Libraries)

理論上は生の HTTP リクエストを Docker API に送信することも可能ですが、クライアントライブラリを使用する方がはるかに実用的で効率的です。クライアントライブラリ(または SDK - Software Development Kit)は、生の HTTP API の上に言語固有のアブストラクションレイヤー(抽象化層)を提供し、より便利でプログラマティックな方法で Docker とインタラクトできるようにします。HTTP リクエスト、JSON パース、エラーハンドリング、および認証の細部を処理してくれるため、あなたはアプリケーションのビジネスロジックに集中することができます。

多くのプログラミング言語向けに、公式またはコミュニティ主導の Docker クライアントライブラリが存在します。代表的なものは以下の通りです:

  • Python: docker-py (コード内では通常 docker としてインポートされます)
  • Go: [github.com/docker/docker/client](https://github.com/docker/docker/client)
  • Java: docker-java
  • Node.js: dockerode
  • Ruby: docker-api
  • PHP: docker-php

本章では、広く利用されており、ドキュメントが充実していてコアなコンセプトを実証するのに非常に効果的な Python の docker-py ライブラリに焦点を当てます。

3.1 docker-py のインストール

このライブラリを使用する前に、まずインストールする必要があります:

pip install docker

4. docker-py の実践ケース

実践に入りましょう。Docker Daemon が稼働していることを確認してください。

4.1 Docker Daemon への接続

最初のステップは常に、あなたの Docker Daemon に接続できるクライアントオブジェクトを作成することです。docker-py は非常にスマートで、デフォルトの Unix ソケット (/var/run/docker.sock) または名前付きパイプ (\\.\pipe\docker_engine) への自動接続を試みます。

import docker

# Docker クライアントの初期化
# これはデフォルトの Docker ソケットへの接続を試みます
try:
    client = docker.from_env()
    print("Docker Daemon への接続に成功しました。")
except docker.errors.DockerException as e:
    print(f"Docker Daemon への接続に失敗しました: {e}")
    print("Docker が稼働しており、アクセス可能であることを確認してください。")
    exit(1)

# 特定の URL に接続することも可能です(例:リモート Daemon への接続)
# from docker import DockerClient
# client = DockerClient(base_url='tcp://192.168.1.100:2375') # 非暗号化 TCP
# client = DockerClient(base_url='tcp://192.168.1.100:2376', tls=True) # TLS 暗号化 TCP

4.2 コンテナのリスト表示(docker ps のシミュレート)

docker ps -a と同様に、稼働中および停止中のすべてのコンテナをリストアップできます。

import docker

client = docker.from_env()

print("--- すべてのコンテナをリストアップ (稼働中および停止中) ---")
# 'all=True' パラメータは 'docker ps -a' と同等です
containers = client.containers.list(all=True)

if not containers:
    print("コンテナが見つかりませんでした。")
else:
    for container in containers:
        print(f"ID: {container.short_id}, 名前: {container.name}, ステータス: {container.status}, イメージ: {container.image.tags}")

# コンテナ属性へのアクセス方法:
# container.id (完全な ID)
# container.short_id (短縮された ID)
# container.name (コンテナ名)
# container.status (コンテナステータス)
# container.image.tags (イメージタグのリスト)
# container.attrs (生の API 属性をすべて含む辞書)

4.3 新規コンテナの実行(docker run のシミュレート)

シンプルな hello-world コンテナを実行してみましょう。

import docker
import time

client = docker.from_env()

print("--- 新しい 'hello-world' コンテナの実行 ---")
try:
    # 'hello-world' イメージを実行します。
    # 'remove=True' パラメータは、コンテナが終了した後に自動的に削除されることを保証します。
    container = client.containers.run('hello-world', remove=True)
    print(f"コンテナ '{container.name}' が起動し、終了しました。")
    
    # コンテナのログを取得して出力します
    logs = container.logs().decode('utf-8')
    print("\nコンテナログ:")
    print(logs)
except docker.errors.ImageNotFound:
    print("'hello-world' イメージがローカルに見つかりません。プルしています...")
    client.images.pull('hello-world')
    print("イメージのプルが完了しました。コンテナの実行を再試行します。")
    container = client.containers.run('hello-world', remove=True)
    logs = container.logs().decode('utf-8')
    print("\nコンテナログ:")
    print(logs)
except docker.errors.ContainerError as e:
    print(f"コンテナがエラーで終了しました: {e}")
except docker.errors.APIError as e:
    print(f"Docker API エラー: {e}")

print("\n--- Nginx コンテナをバックグラウンドモードで実行 ---")
try:
    # Nginx コンテナをバックグラウンドモードで実行 (-d)
    # 名前を付け、コンテナのポート 80 をホストのポート 8080 にマッピング (-p 8080:80)
    # 'detach=True' パラメータは -d に似ています
    # 'ports' パラメータは host_port:container_port をマッピングします
    nginx_container = client.containers.run(
        'nginx:latest',
        name='my-nginx-webserver',
        ports={'80/tcp': 8080},
        detach=True,
        remove=False # 後でインタラクトするために稼働したままにします
    )
    print(f"Nginx コンテナ '{nginx_container.name}' が起動しました (ID: {nginx_container.short_id})。")
    print("http://localhost:8080 にアクセスしてください")
    print("稼働状態を示すために 5 秒間待機します...")
    time.sleep(5)
    print(f"Nginx コンテナのステータス: {nginx_container.status}")
except docker.errors.ContainerError as e:
    print(f"Nginx コンテナがエラーで終了しました: {e}")
except docker.errors.APIError as e:
    print(f"Docker API エラー: {e}")

# Nginx コンテナのクリーンアップ
if 'nginx_container' in locals() and nginx_container.status == 'running':
    print(f"\n--- Nginx コンテナ '{nginx_container.name}' の停止と削除 ---")
    nginx_container.stop()
    nginx_container.remove()
    print("Nginx コンテナが停止し、削除されました。")

4.4 コンテナライフサイクルの管理(起動、停止、再起動、強制停止、削除)

ID または名前で既存のコンテナの参照を取得し、それに対してオペレーションを実行できます。

import docker
import time

client = docker.from_env()

# 管理テスト用のコンテナが確実に存在するようにする
print("--- 管理用のテストコンテナの存在確認 ---")
try:
    test_container = client.containers.run('alpine', command='sleep 3600', name='my-test-container', detach=True)
    print(f"テストコンテナ '{test_container.name}' が起動しました (ID: {test_container.short_id})。ステータス: {test_container.status}")
except docker.errors.ContainerError as e:
    print(f"コンテナを起動できませんでした: {e}。既存のコンテナの取得を試みます。")
    try:
        test_container = client.containers.get('my-test-container')
        print(f"既存のコンテナ '{test_container.name}' を見つけました。ステータス: {test_container.status}")
        if test_container.status == 'exited':
            print("コンテナは終了しています。起動します。")
            test_container.start()
            # コンテナオブジェクトをリロードして更新後のステータスを取得
            test_container.reload()
            print(f"コンテナが起動しました。新ステータス: {test_container.status}")
    except docker.errors.NotFound:
        print("'my-test-container' を起動または発見できません。プログラムを終了します。")
        exit(1)

# コンテナの停止
print(f"\n--- コンテナ '{test_container.name}' の停止 ---")
test_container.stop()
test_container.reload() # 属性を再読み込みして最新のステータスを取得
print(f"停止後のコンテナステータス: {test_container.status}")
time.sleep(2)

# コンテナの起動
print(f"\n--- コンテナ '{test_container.name}' の起動 ---")
test_container.start()
test_container.reload()
print(f"起動後のコンテナステータス: {test_container.status}")
time.sleep(2)

# コンテナの再起動
print(f"\n--- コンテナ '{test_container.name}' の再起動 ---")
test_container.restart()
test_container.reload()
print(f"再起動後のコンテナステータス: {test_container.status}")
time.sleep(2)

# コンテナの強制停止 (Kill)
print(f"\n--- コンテナ '{test_container.name}' の強制停止 (Kill) ---")
test_container.kill()
test_container.reload()
print(f"Kill 後のコンテナステータス: {test_container.status}")
time.sleep(2)

# コンテナの削除
print(f"\n--- コンテナ '{test_container.name}' の削除 ---")
test_container.remove()
try:
    client.containers.get('my-test-container')
    print("コンテナがまだ存在しています。削除に失敗しました。")
except docker.errors.NotFound:
    print("コンテナが正常に削除されました。")

4.5 イメージのリスト表示と管理(docker images, docker pull, docker rmi のシミュレート)

コードを通じてインタラクティブに Docker イメージを管理できます。

import docker

client = docker.from_env()

print("--- すべてのイメージをリストアップ ---")
images = client.images.list()
if not images:
    print("イメージが見つかりませんでした。")
else:
    for image in images:
        print(f"ID: {image.short_id}, タグ: {image.tags}")

print("\n--- イメージのプル (例: 'ubuntu:latest') ---")
try:
    # イメージをプルします。これは 'docker pull ubuntu:latest' と同等です
    ubuntu_image = client.images.pull('ubuntu:latest')
    print(f"イメージ 'ubuntu:latest' がプルされました。タグ: {ubuntu_image.tags}")
except docker.errors.APIError as e:
    print(f"イメージのプルでエラー発生: {e}")

print("\n--- イメージの削除 (例: 'ubuntu:latest') ---")
# 削除を試みる前に、そのイメージがどのコンテナにも使用されていないことを確認してください
try:
    # タグ経由でイメージオブジェクトを取得
    image_to_remove = client.images.get('ubuntu:latest')
    # イメージを削除します。'docker rmi ubuntu:latest' と同等です
    client.images.remove(image_to_remove.id)
    print("イメージ 'ubuntu:latest' が正常に削除されました。")
except docker.errors.ImageNotFound:
    print("イメージ 'ubuntu:latest' が見つからないため、削除をスキップします。")
except docker.errors.APIError as e:
    print(f"イメージの削除でエラー発生: {e}")
    if "conflict" in str(e).lower():
        print("ヒント: イメージがコンテナによって使用中の可能性があります。先にこのイメージを使用しているコンテナを削除してください。")

4.6 イメージのビルド(docker build のシミュレート)

CI/CD パイプラインにとって、ここは強大さが発揮される部分です。Dockerfile からイメージをビルドすることができます。

まず、シンプルな Dockerfile とアプリケーションファイルを作成します。my_app というディレクトリを作成し、以下のファイルを含めます:

my_app/Dockerfile:

# シンプルな Python Flask アプリ用の Dockerfile
FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]

my_app/requirements.txt:

Flask

my_app/app.py:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from Flask inside a Docker container!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

次に、docker-py を使用してこのイメージをビルドし、実行します。

import docker
import os
import time

client = docker.from_env()

# Dockerfile が含まれるディレクトリのパスを定義
dockerfile_path = './my_app' # my_app ディレクトリがこのスクリプトと同じ場所にあると仮定
image_tag = 'my-flask-app:1.0'

if not os.path.exists(dockerfile_path):
    print(f"エラー: ディレクトリ '{dockerfile_path}' が見つかりません。ディレクトリを作成し、Dockerfile, app.py, requirements.txt を配置してください。")
    exit(1)

print(f"--- '{dockerfile_path}' からイメージ '{image_tag}' をビルドしています ---")
try:
    # イメージをビルドします。'path' はビルドコンテキスト、'tag' はイメージタグです。
    # 'build' メソッドはタプル (image_object, build_logs_generator) を返します。
    image, build_logs = client.images.build(path=dockerfile_path, tag=image_tag)
    
    print("ビルドログ:")
    for chunk in build_logs:
        if 'stream' in chunk:
            print(chunk['stream'], end='')
        if 'error' in chunk:
            print(f"ビルド処理中にエラーが発生しました: {chunk['error']}")
            
    print(f"\nイメージ '{image_tag}' のビルドが成功しました (ID: {image.short_id})。")
    
    print(f"\n--- イメージ '{image_tag}' からコンテナを実行しています ---")
    flask_container = client.containers.run(
        image_tag,
        name='my-flask-web-app',
        ports={'5000/tcp': 5000},
        detach=True
    )
    print(f"コンテナ '{flask_container.name}' が起動しました (ID: {flask_container.short_id})。http://localhost:5000 にアクセスしてください")
    print("アプリの起動を待つため 5 秒間待機します...")
    time.sleep(5)
    print(f"コンテナステータス: {flask_container.status}")
    
    # ポートマッピングを検証するためにコンテナをインスペクトすることができます
    # print("\nコンテナのインスペクト情報:")
    # print(flask_container.attrs['NetworkSettings']['Ports'])

except docker.errors.BuildError as e:
    print(f"イメージのビルドでエラーが発生しました: {e}")
    # 必要に応じて、ビルドエラーに関する詳細情報にアクセスできます
    for line in e.build_log:
        if 'stream' in line:
            print(line['stream'], end='')
except docker.errors.APIError as e:
    print(f"ビルドまたは実行中に Docker API エラーが発生しました: {e}")
finally:
    # コンテナのクリーンアップ
    if 'flask_container' in locals():
        print(f"\n--- コンテナ '{flask_container.name}' の停止と削除 ---")
        flask_container.stop()
        flask_container.remove()
        print("コンテナが停止し、削除されました。")
    
    # イメージのクリーンアップ
    try:
        print(f"--- イメージ '{image_tag}' の削除 ---")
        client.images.remove(image_tag)
        print("イメージが削除されました。")
    except docker.errors.ImageNotFound:
        print(f"イメージ '{image_tag}' が見つからないため、削除をスキップします。")
    except docker.errors.APIError as e:
        print(f"イメージ '{image_tag}' の削除でエラー発生: {e}")

4.7 コンテナログの取得(docker logs のシミュレート)

ログのストリーミング(ストリーム伝送)はモニタリングにとって不可欠です。

import docker
import time

client = docker.from_env()

print("--- ログのデモンストレーション用コンテナの実行 ---")
# 1秒ごとにメッセージを出力する alpine コンテナを実行
log_container = client.containers.run(
    'alpine/git', # 'sh' と 'sleep' が含まれているため alpine/git を使用
    command='sh -c "i=0; while true; do echo \'Hello from container: $i\'; i=$((i+1)); sleep 1; done"',
    name='log-demo-container',
    detach=True
)
print(f"コンテナ '{log_container.name}' が起動しました。ログを蓄積するため 3 秒間待機します。")
time.sleep(3)

print("\n--- コンテナからのログストリーミング ---")
# ログをストリーミングします。'stream=True' は、ログが生成された際に1行ずつ出力されることを意味します。
# 'follow=True' は 'tail -f' のようにデータストリームを開いたままにします。
# 'decode=True' はバイトを文字列にデコードします。
for line in log_container.logs(stream=True, follow=False, decode=True):
    # デモンストレーションの目的で、最初の数行のみを取得します
    if line.strip().startswith('Hello'):
        print(f"ログ: {line.strip()}")
        # ここにログのパース、アラートのトリガーなどのロジックを追加できます。
        
# 'follow=True' をデモするには、コンテナを停止するための別のスレッドまたはプロセスが必要です。
# シンプルな例として、ここでは継続してフォロー(追跡)せず現在のログのみを取得しています。

# 停止可能な制御されたデータストリームを取得するには:
# log_generator = log_container.logs(stream=True, follow=True, decode=True)
# try:
#     for _ in range(5): # 5行取得して停止
#         line = next(log_generator)
#         print(f"ストリームログ: {line.strip()}")
# except StopIteration:
#     pass # ストリームが終了したか、一時的にログがありません

print(f"\n--- コンテナ '{log_container.name}' の停止と削除 ---")
log_container.stop()
log_container.remove()
print("コンテナが停止し、削除されました。")

4.8 Docker オブジェクトのインスペクト(コンテナ、イメージ、ネットワーク、ボリューム)

inspect 機能は、docker inspect と同様に Docker オブジェクトに関する詳細なローレベル情報を提供します。これは、コードを通じてコンフィギュレーション、ランタイム状態、およびネットワークの詳細を収集するのに非常に役立ちます。

import docker

client = docker.from_env()

# インスペクト用のコンテナが確実に存在するようにする
try:
    container = client.containers.get('my-nginx-webserver')
    if container.status == 'exited':
        container.start()
        container.reload()
except docker.errors.NotFound:
    print("Nginx コンテナが見つかりません。インスペクト用に実行します...")
    container = client.containers.run('nginx:latest', name='my-nginx-webserver', ports={'80/tcp': 8080}, detach=True)
    print(f"Nginx コンテナ '{container.name}' が起動しました。")

print(f"\n--- コンテナ '{container.name}' のインスペクト ---")
# .attrs 属性には、API の inspect 呼び出しで返された生の辞書が保持されています
container_info = container.attrs
print(f"コンテナ名: {container_info['Name']}")
print(f"コンテナステータス: {container_info['State']['Status']}")
print(f"IP アドレス: {container_info['NetworkSettings']['IPAddress']}")
print(f"80/tcp に対応するホストポート: {container_info['NetworkSettings']['Ports']['80/tcp'][0]['HostPort']}")

# イメージのインスペクト
print("\n--- イメージ 'nginx:latest' のインスペクト ---")
try:
    image = client.images.get('nginx:latest')
    image_info = image.attrs
    print(f"イメージ ID: {image_info['Id']}")
    print(f"作成日時: {image_info['Created']}")
    print(f"イメージサイズ: {image_info['Size']} バイト")
    print(f"システムアーキテクチャ: {image_info['Architecture']}")
except docker.errors.ImageNotFound:
    print("イメージ 'nginx:latest' が見つかりません。先にプルします...")
    client.images.pull('nginx:latest')
    image = client.images.get('nginx:latest')
    image_info = image.attrs
    print(f"イメージ ID: {image_info['Id']}")
    print(f"作成日時: {image_info['Created']}")
    print(f"イメージサイズ: {image_info['Size']} バイト")
    print(f"システムアーキテクチャ: {image_info['Architecture']}")

# クリーンアップ作業
if 'container' in locals():
    print(f"\n--- コンテナ '{container.name}' の停止と削除 ---")
    container.stop()
    container.remove()
    print("コンテナが停止し、削除されました。")