0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker Engine APIを調査する

Posted at

Docker CLI は、内部で Docker Engine API を呼び出して動作しています。
例えば docker ps は、実際には GET /containers/json という API を叩いています。

本記事では、この API 呼び出しの流れを徹底解析し、CLI とデーモンの関係を明らかにします

  1. Docker Engine APIの概要 : CLI とデーモンの関係を整理
  2. ソースコード解析 : CLI がどのように API を呼び出すかを調査
  3. パケットキャプチャ : 実際の API リクエストを可視化
  4. Dockerデーモンのログ解析 : デーモン側の処理を確認
  5. APIの直接呼び出し : CLI を介さず API を叩いてみる

この調査を通じて、Docker CLI の仕組みや API の動作原理を理解し、より高度な Docker の活用方法につなげること を目的とします。

1. Docker Engine APIの概要

普段、私たちは docker rundocker builddocker ps などのコマンドをCLIで実行していますが、各コマンドがそのままDockerデーモンに送信されているわけではありません。
例えばdocker ps と入力すると、docker/cliリポジトリ のコードに基づいて、REST APIである GET /containers/json に変換されます。

docker_API_概要.png

上記の図は、docker ps がどのようにデーモンにリクエストを送り、最終的にデーモンが情報を返すかを示しています。

docker ps の処理は、以下の流れで進みます:

  1. CLI の入力:ユーザーが docker ps を実行
  2. API 変換:CLI 内部 (docker/cli) で GET /containers/json に変換
  3. リクエスト送信:API リクエストが UNIX ドメインソケット /var/run/docker.sock 経由でデーモンに送信される
  4. デーモン側の処理:デーモン (dockerd) がコンテナ情報を取得し、JSON 形式でレスポンスを返す
  5. CLI の出力処理:CLI がレスポンスを受け取り、ユーザーに表示する

参考
Docker Engine API について
Docker概要

2. ソースコード解析

docker/cliリポジトリ に記載されている内容の一部を見ていきます。
概略のみ説明しますが、詳細はソースコードを参照してください。
大きく分けて、cobraコマンドの作成、作成したコマンドの実行の2つに分離されます。

実際のソースコード

1. docker コマンドのエントリーポイント

/cli/cmd/docker/docker.go
func main() {
	err := dockerMain(context.Background())
	if errors.As(err, &errCtxSignalTerminated{}) {
		os.Exit(getExitCode(err))
	}

	if err != nil && !errdefs.IsCancelled(err) {
		_, _ = fmt.Fprintln(os.Stderr, err)
		os.Exit(getExitCode(err))
	}
}

func dockerMain(ctx context.Context) error {
	ctx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
	defer cancelNotify()

	dockerCli, err := command.NewDockerCli(command.WithBaseContext(ctx))
	if err != nil {
		return err
	}
	logrus.SetOutput(dockerCli.Err())
	otel.SetErrorHandler(debug.OTELErrorHandler)

	return runDocker(ctx, dockerCli)
}

func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
	tcmd := newDockerCommand(dockerCli)
    
    

    err = cmd.ExecuteContext(ctx)

    

    return err
}

func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
	var (
		opts    *cliflags.ClientOptions
		helpCmd *cobra.Command
	)

	cmd := &cobra.Command{
		Use:              "docker [OPTIONS] COMMAND [ARG...]",
		Short:            "A self-sufficient runtime for containers",
		SilenceUsage:     true,
		SilenceErrors:    true,
		TraverseChildren: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(args) == 0 {
				return command.ShowHelp(dockerCli.Err())(cmd, args)
			}
			return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", args[0])
		},
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			return isSupported(cmd, dockerCli)
		},
		Version:               fmt.Sprintf("%s, build %s", version.Version, version.GitCommit),
		DisableFlagsInUseLine: true,
		CompletionOptions: cobra.CompletionOptions{
			DisableDefaultCmd:   false,
			HiddenDefaultCmd:    true,
			DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
		},
	}

    
    
	commands.AddCommands(cmd, dockerCli)

	cli.DisableFlagsInUseLine(cmd)
	setValidateArgs(dockerCli, cmd)

	// flags must be the top-level command flags, not cmd.Flags()
	return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
}
  • main() がエントリーポイントになります
  • newDockerCommandcobra でCLIが作成されます
  • cmd.ExecuteContext(ctx)でコマンドが実行されます

2. docker ps のコマンド登録

/cli/cli/command/commands/commands.go
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
	cmd.AddCommand(
		// commonly used shorthands
		container.NewRunCommand(dockerCli),
		container.NewExecCommand(dockerCli),
		container.NewPsCommand(dockerCli),
		image.NewBuildCommand(dockerCli),
		image.NewPullCommand(dockerCli),
		image.NewPushCommand(dockerCli),
		image.NewImagesCommand(dockerCli),
		registry.NewLoginCommand(dockerCli),
		registry.NewLogoutCommand(dockerCli),
		registry.NewSearchCommand(dockerCli),
		system.NewVersionCommand(dockerCli),
		system.NewInfoCommand(dockerCli),

		// management commands
		builder.NewBuilderCommand(dockerCli),
		checkpoint.NewCheckpointCommand(dockerCli),
		container.NewContainerCommand(dockerCli),
		context.NewContextCommand(dockerCli),
		image.NewImageCommand(dockerCli),
		manifest.NewManifestCommand(dockerCli),
		network.NewNetworkCommand(dockerCli),
		plugin.NewPluginCommand(dockerCli),
		system.NewSystemCommand(dockerCli),
		trust.NewTrustCommand(dockerCli),
		volume.NewVolumeCommand(dockerCli),

        
	)
}
/cli/cli/command/container/list.go
// NewPsCommand creates a new cobra.Command for `docker ps`
func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
	options := psOptions{filter: opts.NewFilterOpt()}

	cmd := &cobra.Command{
		Use:   "ps [OPTIONS]",
		Short: "List containers",
		Args:  cli.NoArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			options.sizeChanged = cmd.Flags().Changed("size")
			return runPs(cmd.Context(), dockerCLI, &options)
		},
		Annotations: map[string]string{
			"category-top": "3",
			"aliases":      "docker container ls, docker container list, docker container ps, docker ps",
		},
		ValidArgsFunction: completion.NoComplete,
	}

    
	return cmd    
}

func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error {

    

    listOptions, err := buildContainerListOptions(options)
	if err != nil {
		return err
	}

	containers, err := dockerCLI.Client().ContainerList(ctx, *listOptions)
	if err != nil {
		return err
	}

	containerCtx := formatter.Context{
		Output: dockerCLI.Out(),
		Format: formatter.NewContainerFormat(options.format, options.quiet, listOptions.Size),
		Trunc:  !options.noTrunc,
	}
	return formatter.ContainerWrite(containerCtx, containers)
}
  • NewPsCommand(dockerCLI) により、docker ps コマンドが cobra.Command に登録されます
  • RunE に定義された runPs() が、実際の処理を担当します
  • レスポンス (resp.Body) を json.NewDecoder(resp.Body).Decode(&containers) でデコードし、コンテナの一覧を取得します
docker/client/container_list.go
func (cli *Client) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) {

    

    resp, err := cli.get(ctx, "/containers/json", query, nil)
	defer ensureReaderClosed(resp)
	if err != nil {
		return nil, err
	}

	var containers []container.Summary
	err = json.NewDecoder(resp.Body).Decode(&containers)
	return containers, err
}
  • cli.get(ctx, "/containers/json", query, nil) を実行することで、API GET /containers/jsonを呼び出します
  • そのレスポンス (resp.Body) を json.NewDecoder(resp.Body).Decode(&containers) でデコードし、コンテナの一覧を取得します

3. docker psのパケットキャプチャ

通常、Wireshark や tcpdump は ネットワーク上のパケット をキャプチャするツールです。
しかし、Docker CLI は UNIX ドメインソケット /var/run/docker.sock を使い、ローカルプロセス間で通信します。
この UNIX ドメインソケットの通信はカーネル内で処理されるため、通常のネットワークキャプチャツールでは見えません

そこで、TCPでリクエストするようにしてキャプチャできるようにした後、socat を利用してUNIX ドメインソケットに変換 し、dockerデーモンへリクエストできるようにします。

大まかな手順は次の通りです。

  1. tcpdump で TCP のパケットをキャプチャ
  2. socat で TCPをUNIX ドメインソケットをに変換
  3. 向き先をTCPにした上でdocker ps を実行し、Wireshark でリクエストを確認

3.png
4.png

実際に Wireshark でキャプチャを取得するため、次の手順を実行します。

ターミナル1: socat を起動し、転送を設定
TCP ポート 12345 で通信を待ち受け、/var/run/docker.sock に転送します。

ターミナル1
socat -v TCP-LISTEN:12345,fork UNIX-CONNECT:/var/run/docker.sock

ターミナル2: tcpdump で通信をキャプチャ
TCP ポート 12345 への通信をキャプチャし、docker_capture.pcap に保存します。

ターミナル2
sudo tcpdump -i lo0 -nn -X port 12345 -w docker_capture.pcap

ターミナル3: Docker CLI を実行
Docker CLI の接続先を 12345 ポートに向けた上で、docker ps を実行します。
(※ docker run nginx は省略可)

ターミナル3
docker run nginx
export DOCKER_HOST=tcp://localhost:12345
docker ps

ターミナル2 で tcpdump を停止
docker ps 実行後、ターミナル2で Ctrl + C を押し、tcpdump を終了します。

docker_capture.pcap を Wireshark で開くと、実際の通信内容を確認できます。

リクエスト
request.png

レスポンス
response.png

参考
「UNIX ドメインソケット」と「ソケット」について比較する

4. Dockerデーモンのログの確認

Docker のデーモン側でどのようにリクエストを受け取っているかを確認するには、ログレベルを debug に設定する必要があります。

1. ログレベルを debug に変更

Docker の設定ファイル (Docker Engine の設定) に debug: true を追加します。

設定方法

  1. Docker Desktop を開く
  2. Settings (設定) → Docker Engine を選択
  3. 以下のように debug: true を追加
    {
      "builder": {
        "gc": {
          "defaultKeepStorage": "20GB",
          "enabled": true
        }
      },
      "debug": true,
      "experimental": false
    }
    
  4. 「Apply & Restart」ボタンを押し、Docker を再起動

2. Docker デーモンのログを確認

docker ps を実行すると、デーモン側のログは次のように記録されます。

~/Library/Containers/com.docker.docker/Data/log/vm/dockerd.log
time="2025-02-16T06:47:20.818742722Z" level=debug msg="Calling HEAD /_ping"
time="2025-02-16T06:47:20.820766263Z" level=debug msg="Calling GET /v1.47/containers/json"

このログから、
HEAD /_ping が最初に呼ばれている(Docker デーモンのヘルスチェック)
GET /v1.47/containers/jsondocker ps のリクエストである
ことが確認できます。

また、tail -f を使用すれば、Docker デーモンのログをリアルタイムで確認できます。

tail -f ~/Library/Containers/com.docker.docker/Data/log/vm/dockerd.log

5. APIの直接呼び出し

Docker Engine API には、バージョンごとに利用可能な API の一覧が記載されています。
Docker CLI (docker ps など) は、内部的にこの API を呼び出して動作しています。

curl --unix-socket /var/run/docker.sock http:/v1.41/containers/json
[{"Id":"ec2fd5900c370fa1f3d4fc3bbe21d8b8cb1e4593dbad1dbd06fab0f3ecfdc024","Names":["/gracious_babbage"],"Image":"nginx","ImageID":"sha256:9b1b7be1ffa607d40d545607d3fdf441f08553468adec5588fb58499ad77fe58","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Created":1739616018,"Ports":[{"PrivatePort":80,"Type":"tcp"}],"Labels":{"maintainer":"NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e"},"State":"running","Status":"Up 11 minutes","HostConfig":{"NetworkMode":"bridge"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null,"NetworkID":"d5012daf5e2c6d9da8e5a111297163736f3189e6c4ba79b83aec229db467f679","EndpointID":"e626e73227d777d7878e71bee967d652e5aea86c48e2f4a83ed6fcff8e9600ab","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"DNSNames":null}}},"Mounts":[]}]

もし socat を利用して /var/run/docker.sock を TCP に転送している場合は、
DOCKER_HOST を指定して API を呼び出せます。

ターミナル1
socat -v TCP-LISTEN:12345,fork UNIX-CONNECT:/var/run/docker.sock
ターミナル2
curl -X GET http://localhost:12345/v1.41/containers/json
[{"Id":"ec2fd5900c370fa1f3d4fc3bbe21d8b8cb1e4593dbad1dbd06fab0f3ecfdc024","Names":["/gracious_babbage"],"Image":"nginx","ImageID":"sha256:9b1b7be1ffa607d40d545607d3fdf441f08553468adec5588fb58499ad77fe58","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Created":1739616018,"Ports":[{"PrivatePort":80,"Type":"tcp"}],"Labels":{"maintainer":"NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e"},"State":"running","Status":"Up 16 minutes","HostConfig":{"NetworkMode":"bridge"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null,"NetworkID":"d5012daf5e2c6d9da8e5a111297163736f3189e6c4ba79b83aec229db467f679","EndpointID":"e626e73227d777d7878e71bee967d652e5aea86c48e2f4a83ed6fcff8e9600ab","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"DNSNames":null}}},"Mounts":[]}]

終わりに

docker ps の内部動作を解析することで、Docker CLI がどのように API を利用しているかが分かりました。
この知識を活かすことで、以下のような応用が可能ではないかと思います:

  • API を直接操作し、カスタムツールを開発
  • デバッグ時に API を確認し、異常動作を特定
  • Docker CLI なしでスクリプトからコンテナ管理
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?