Docker CLI は、内部で Docker Engine API を呼び出して動作しています。
例えば docker ps
は、実際には GET /containers/json
という API を叩いています。
本記事では、この API 呼び出しの流れを徹底解析し、CLI とデーモンの関係を明らかにします。
- Docker Engine APIの概要 : CLI とデーモンの関係を整理
- ソースコード解析 : CLI がどのように API を呼び出すかを調査
- パケットキャプチャ : 実際の API リクエストを可視化
- Dockerデーモンのログ解析 : デーモン側の処理を確認
- APIの直接呼び出し : CLI を介さず API を叩いてみる
この調査を通じて、Docker CLI の仕組みや API の動作原理を理解し、より高度な Docker の活用方法につなげること を目的とします。
1. Docker Engine APIの概要
普段、私たちは docker run
や docker build
、docker ps
などのコマンドをCLIで実行していますが、各コマンドがそのままDockerデーモンに送信されているわけではありません。
例えばdocker ps
と入力すると、docker/cliリポジトリ のコードに基づいて、REST APIである GET /containers/json
に変換されます。
上記の図は、docker ps
がどのようにデーモンにリクエストを送り、最終的にデーモンが情報を返すかを示しています。
docker ps の処理は、以下の流れで進みます:
-
CLI の入力:ユーザーが
docker ps
を実行 -
API 変換:CLI 内部 (
docker/cli
) でGET /containers/json
に変換 -
リクエスト送信:API リクエストが UNIX ドメインソケット
/var/run/docker.sock
経由でデーモンに送信される - デーモン側の処理:デーモン (dockerd) がコンテナ情報を取得し、JSON 形式でレスポンスを返す
- CLI の出力処理:CLI がレスポンスを受け取り、ユーザーに表示する
参考
Docker Engine API について
Docker概要
2. ソースコード解析
docker/cliリポジトリ に記載されている内容の一部を見ていきます。
概略のみ説明しますが、詳細はソースコードを参照してください。
大きく分けて、cobraコマンドの作成、作成したコマンドの実行の2つに分離されます。
実際のソースコード
1. docker コマンドのエントリーポイント
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()
がエントリーポイントになります -
newDockerCommand
でcobra
でCLIが作成されます -
cmd.ExecuteContext(ctx)
でコマンドが実行されます
2. docker ps のコマンド登録
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),
略
)
}
// 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)
でデコードし、コンテナの一覧を取得します
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)
を実行することで、APIGET /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デーモンへリクエストできるようにします。
大まかな手順は次の通りです。
-
tcpdump
で TCP のパケットをキャプチャ -
socat
で TCPをUNIX ドメインソケットをに変換 - 向き先をTCPにした上で
docker ps
を実行し、Wireshark でリクエストを確認
実際に Wireshark でキャプチャを取得するため、次の手順を実行します。
ターミナル1: socat を起動し、転送を設定
TCP ポート 12345 で通信を待ち受け、/var/run/docker.sock
に転送します。
socat -v TCP-LISTEN:12345,fork UNIX-CONNECT:/var/run/docker.sock
ターミナル2: tcpdump で通信をキャプチャ
TCP ポート 12345 への通信をキャプチャし、docker_capture.pcap
に保存します。
sudo tcpdump -i lo0 -nn -X port 12345 -w docker_capture.pcap
ターミナル3: Docker CLI を実行
Docker CLI の接続先を 12345 ポートに向けた上で、docker ps
を実行します。
(※ docker run nginx
は省略可)
docker run nginx
export DOCKER_HOST=tcp://localhost:12345
docker ps
ターミナル2 で tcpdump
を停止
docker ps
実行後、ターミナル2で Ctrl + C
を押し、tcpdump
を終了します。
docker_capture.pcap
を Wireshark で開くと、実際の通信内容を確認できます。
参考
「UNIX ドメインソケット」と「ソケット」について比較する
4. Dockerデーモンのログの確認
Docker のデーモン側でどのようにリクエストを受け取っているかを確認するには、ログレベルを debug
に設定する必要があります。
1. ログレベルを debug に変更
Docker の設定ファイル (Docker Engine の設定) に debug: true
を追加します。
設定方法
- Docker Desktop を開く
- Settings (設定) → Docker Engine を選択
- 以下のように debug: true を追加
例
{ "builder": { "gc": { "defaultKeepStorage": "20GB", "enabled": true } }, "debug": true, "experimental": false }
- 「Apply & Restart」ボタンを押し、Docker を再起動
2. Docker デーモンのログを確認
docker ps
を実行すると、デーモン側のログは次のように記録されます。
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/json
が docker 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 を呼び出せます。
socat -v TCP-LISTEN:12345,fork UNIX-CONNECT:/var/run/docker.sock
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 なしでスクリプトからコンテナ管理