8.2.165-stable Switch to dev

docker

A Docker Engine API client. Pure GCL on http, which comes along with it and does not need declaring.

@library("docker", "0.0.0");

Talks to the daemon’s REST API directly, over its Unix socket or over TCP. It does not shell out, so it needs no docker binary on the path – only a socket it can reach.

var client = docker::Client::local();

var id = client.run(null, docker::CreateContainerRequest {
    Image: "postgres:16",
    Env: Array<String> { "POSTGRES_PASSWORD=secret" },
    HostConfig: docker::HostConfig { PublishAllPorts: true, AutoRemove: true },
});
client.wait_running(id, 30s);
println("postgres on ${client.host_port(id, "5432/tcp")}");

Every type is private, so it is reached by its qualified name: docker::Client, docker::CreateContainerRequest.

Connecting

docker::Client::local();                          // unix:///var/run/docker.sock
docker::Client::from_env();                       // DOCKER_HOST, or the default socket
docker::Client::tcp("http://127.0.0.1:2375");     // a daemon over TCP

from_env reads DOCKER_HOST and DOCKER_TLS_VERIFY together, because tcp:// says nothing about TLS on its own: it resolves to https:// only when the second variable is set.

api_version pins the path prefix, so the daemon answers the wire format this library was written against rather than whatever its newest is:

docker::Client { host: "unix:///var/run/docker.sock", api_version: "v1.43" };

Left null, the daemon’s own default applies. version() reports what it serves and the oldest it still accepts.

Field names are the daemon’s

CreateContainerRequest.Image, ContainerState.ExitCode, SystemVersion.ApiVersion. GreyCat maps a JSON key to the field spelled the same way and has no annotation to rename one, so the wire’s capitals are carried through. A type declares only the keys it needs; the rest of the daemon’s answer is ignored, which is what lets these types describe one version of the API and still parse a newer daemon’s reply.

Containers

client.list_containers(docker::ListContainersRequest { all: true });
client.create_container("api", request);
client.inspect_container(id);
client.start_container(id);
client.stop_container(id, 10s);
client.remove_container(id, true, true);
client.wait_container(id, null, 5min);

run is create plus start, pulling the image first if it is absent. wait_running polls until the container is up, and fails early rather than waiting out its deadline when the container has already exited.

host_port(id, "5432/tcp") reads the published port, and is null until the container runs and the binding actually exists.

PublishAllPorts is -P: every exposed port on a random host port. To pick the ports, give each a binding with an empty HostPort:

PortBindings: Map<String, Array<docker::PortBinding>?> {
    "5432/tcp": Array<docker::PortBinding> { docker::PortBinding { HostPort: "" } },
},

An empty binding array is not that. Docker accepts it as a random port, podman publishes nothing at all, and host_port then answers null.

Logs and exec

var logs = client.logs(id, null);
println(logs.stdout);
println(logs.stderr);

var ran = client.exec(id, docker::ExecRequest { Cmd: Array<String> { "sh", "-c", "ls /data" } });
println("${ran.stdout} (exit ${ran.exit_code})");

Both streams come back separated. Without a TTY the daemon interleaves them as frames – a byte naming the stream, three of padding, four holding the payload length – and this library pulls them apart. With a TTY there is one stream and stderr is empty.

exec is three round trips: the daemon creates the exec, streams its output as the body of the start, and only afterwards knows the exit code.

Images

client.pull_image("alpine:3.19");
client.image_exists("alpine:3.19");
client.tag_image("alpine:3.19", "registry.local/alpine", "3.19");
client.remove_image("registry.local/alpine:3.19", true);

An image reference is split on its tag, and the colon of a registry port is not one: localhost:5000/redis is untagged and resolves to latest.

legacy_build

var built = client.legacy_build(docker::LegacyBuildRequest {
    context_tar: "build/context.tar",
    tag: "myapp:1.0",
    build_args: Map<String, String> { "VERSION": "1.0" },
});
println(built.image_id);

The name is a warning. POST /build is the daemon’s legacy builder: no cache mounts, no secrets, no multi-stage parallelism. docker build has meant BuildKit since Docker 23, and BuildKit is a gRPC session over a hijacked connection this library does not open, so the same Dockerfile can build differently here than at the CLI.

context_tar is a path to a tar of the build context, its Dockerfile included; archive::Archive::create writes one from a directory. The tar is streamed off the disk rather than held in memory. One tag only – the daemon accepts the parameter repeatedly and a query map cannot – so tag_image adds the rest.

Errors

A non-2xx throws, carrying the daemon’s own message:

docker: inspect_container api failed, HTTP 404 No such container: api

Three calls deliberately do not throw:

Call Instead
ping false when the daemon cannot be reached at all
container_exists a bool
image_exists a bool

container_exists returning a bool is what makes it the way to test for a container; inspect_container on a missing one is an error.

A pull and a build report failure inside a 200. The daemon writes the status before it knows the outcome and reports the error partway down the progress stream. Both calls read the whole stream and throw on it, rather than trusting the status.

Reaching an endpoint this library has no method for

request applies the host, the API version prefix, the query encoding, the socket and the headers, leaving only the generic parameter to choose:

var req = client.request(http::HttpMethod::GET, "/tasks", null, null);
var res = http::Http<MyResult> {}.send(req);
client.check("tasks", res.status_code, res.error_msg);
var tasks = res.content!!;

send is the untyped version of the same thing and never throws, for treating a 404 as an outcome. call sends and throws unless it succeeded.

Not covered

attach and an interactive exec need HTTP hijacking, a bidirectional upgrade this library does not open. exec covers running a command and reading what it printed.