In this page
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.