Docker コンテキストを切り替えてリモートホスト上で Docker コマンドを実行する

Docker コンテキストとは?

Docker コンテキストは、ひとことで言うと、Docker CLI で入力したコマンド (docker) の操作対象ホストを切り替えるための仕組みです。

Docker コンテキストの切り替え
図: Docker コンテキストの切り替え

例えば、Conoha などの VPS サーバー や、Azure (ACI)AWS (ECS) といったクラウドサービス上の Docker エンジンに対して、ローカルホストから docker コマンドを実行できるようになります。 カレントコンテキストは、docker compose use コマンドで簡単に切り替えることができるので、ローカルでのコンテナのテストが終わったら、コンテキストを Azure (ACI) に切り替えてクラウド環境上で docker compose up するといったことが簡単にできます。

当然、接続先のコンテナサービスごとに接続プロトコルは異なるのですが、Docker は標準で Azure や AWS をターゲットとしたコンテキストの作成に対応しており、実際に docker コマンドを実行するときは接続プロトコルを意識しないで済むようになっています。 ここでは、より汎用的な SSH 接続を用いるコンテキストを作成し、リモートホスト上の Docker エンジンに対して Docker コマンドを実行してみます。

前提条件:

  • リモートホストに Docker がインストールされていること
  • リモートホストに SSH キーで SSH 接続できるようなっていること(参考: SSH の使い方
  • 接続先のユーザーが sudo なしで docker コマンドを実行できるようなっていること(参考: docker グループへの追加

デフォルト・コンテキスト

Docker Desktop をインストールすると、デフォルトのコンテキストとして、default という名前のコンテキストが作成されます。 Docker コンテキストの一覧を表示するには、docker context ls コマンドを使用します。

Docker コンテキストの一覧を確認する
$ docker context ls
NAME        TYPE   DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
default *   moby   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm

default コンテキストのエンドポイントは unix:///var/run/docker.sock となっており、これはローカルホスト上の Docker デーモンに対して Docker コマンドを実行することを示しています。

リモートホストへの SSH 接続設定

SSH 接続に使用するユーザー情報などは、~/.ssh/config ファイルにまとめて記述しておくと便利です。 というか、これを使わないと、細かい接続設定を行うのが困難なので作成しておきましょう。

~/.ssh/config
Host conoha
    Hostname example.com
    Port 22
    User maku
    IdentityFile ~/.ssh/id_ed25519

ここでは、Conoha VPS を使う想定で、設定名を conoha にしましたが、このあたりは自由に決めてください。 SSH キー(秘密鍵)にパスフレーズが設定されている場合は、SSH agent に秘密鍵とそのパスフレーズを設定しておく必要があります。

SSH agent に秘密鍵を登録しておく
$ ssh-add ~/.ssh/id_ed25519
Enter passphrase for /Users/maku/.ssh/id_ed25519: (秘密鍵のパスフレーズを入力)
Identity added: /Users/maku/.ssh/id_ed25519 (maku@macbook.local)

SSH 接続用の Docker コンテキストを作成する

新しい Docker コンテキストを作成するには、docker context create コマンドを使用します。 --docker オプションを次のように指定すると、SSH 接続用のコンテキストを作成できます。

Docker コンテキストを作成する
$ docker context create --docker "host=ssh://conoha" my-context

ssh://conoha という接続 URI では、前述の ~/.ssh/config で設定した名称を使用していることに注意してください。 ssh://maku@example.com:22 のように、明示的な URI を指定することもできます。

次のように実行すると、新しいコンテキスト my-context が作成されていることを確認できます。

$ docker context ls
NAME         TYPE   DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
default *    moby   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm
my-context   moby                                             ssh://conoha

この段階では、まだカレントコンテキストは default のままです。

コンテキストを切り替えて Docker コマンドを実行する

使用する Docker コンテキストを切り替えるには、docker context use コマンドを使用します。

コンテキストを my-context に切り替える
# コンテキストを切り替え
$ docker context use my-context
my-context

# カレントコンテキストを確認(アクティブなコンテキストに * 印が付いている)
$ docker context ls
NAME           ...(省略)...
my-context *   ...(省略)...
default        ...(省略)...

# 次のようにカレントコンテキスト名だけ表示することも可能
$ docker context show
my-context

あとは、いつも通り docker コマンドを実行すると、SSH 接続先の Docker ホストで処理されるようになります。

$ docker container run --rm hello-world

例えば、リモートホスト側で次のような感じで Web サーバーを起動し、ローカルホスト側からアクセスできるようになります(もちろんポートが適切に開放されている必要があります)。

# リモートホスト上の Web サーバー(nginx コンテナ)を起動
$ docker container run --rm -d -p 8000:80 --name web nginx

# ローカルホストからアクセス
$ curl http://example.com:8000

# リモートホスト上の Web サーバーを停止
$ docker container stop web

上記の例では、あらかじめ docker context use コマンドで使用するコンテキストを切り替えておきましたが、docker コマンド実行時に --context オプションでコンテキスト名を指定することもできます。 次のように実行すれば、各コンテキストで使用する Docker ホストの詳細情報を確認できます。

コンテキストを明示して実行
$ docker --context default info     # デフォルトコンテキスト(ローカルホスト)で実行
$ docker --context my-context info  # 今回作成したコンテキスト(リモートホスト)で実行

本番環境用のコンテキストは常に選択していると危険なので、--context オプションでのみ使用した方がよいかもしれません。

$ docker --context production compose up -d

Docker コンテキストを削除する

必要なくなった Docker コンテキストは、docker context rm コマンドで削除できます。

$ docker context use default    # デフォルトコンテキストに戻しておく
$ docker context rm my-context  # 不要なコンテキストを削除
$ docker context ls             # コンテキストの一覧を確認
NAME         ...(省略)...
default *    ...(省略)...

バインドマウント時の注意

Docker コンテキストとバインドマウントを組み合わせて使用するときは、マウント時の source パスの指定方法に注意する必要があります。 source パスを下記例の ./meili_data のように相対パスで記述すると、それは docker コマンドを実行する PC 上のカレントパスとして展開されます。

docker-compose.yml(バインドマウント)
version: "3.9"

services:
  meilisearch:
    image: "getmeili/meilisearch:v1.6"
    container_name: meilisearch
    ports:
      - "7700:7700"
    environment:
      - MEILI_ENV=production
      - MEILI_NO_ANALYTICS=true
      - MEILI_MASTER_KEY
    volumes:
      - type: bind
        source: ./meili_data
        target: /meili_data
        bind:
          create_host_path: true  # ./meili_data が存在しなければ作成

例えば、カレントディレクトリが /Users/maku/myproject の状態で docker --context my-context compose up -d と実行すると、my-context コンテキストが指し示すリモートホスト上に /Users/maku/myproject というディレクトリが生成されてしまいます(存在しなければ)。 このパスは、おそらくリモートホスト上では意味を持ちません。 このような振る舞いを防ぐには、source パスを最初から絶対パスで指定する必要があります。

    volumes:
      - type: bind
        source: /opt/meili_data  # ターゲットホスト上の絶対パスを想定して記述
        target: /meili_data
        bind:
          create_host_path: true

あるいは、バインドマウントではなく、ボリュームマウント を使ってしまうのが手っ取り早いです。 ボリュームマウントを使用すると Docker のシステムがデータを一元管理してくれるため、ファイルシステム上のパスに関連する問題が発生しなくなります。

docker-compose.yml(ボリュームマウント)
version: "3.9"

services:
  meilisearch:
    image: "getmeili/meilisearch:v1.6"
    container_name: meilisearch
    ports:
      - "7700:7700"
    environment:
      - MEILI_ENV=production
      - MEILI_NO_ANALYTICS=true
      - MEILI_MASTER_KEY
    volumes:
      - type: volume  # ボリュームマウントを使用する
        source: meili_data  # 一番下で定義しているボリュームを指定
        target: /meili_data
        volume:
          nocopy: true  # ボリューム生成時にコンテナから内容をコピーしない

# 作成するボリュームの定義
volumes:
  meili_data:
    # name: meili_data  # プロジェクト名のプレフィックスを付けたくないとき