NAME
API::Docker::API::Images - Docker Engine Images API
VERSION
version 0.004
SYNOPSIS
my $docker = API::Docker->new;
# Build an image from a tar context
use Path::Tiny;
my $tar = path('context.tar')->slurp_raw;
$docker->images->build(context => $tar, t => 'myapp:latest');
# Pull an image
$docker->images->pull(fromImage => 'nginx', tag => 'latest');
# List images
my $images = $docker->images->list;
for my $image (@$images) {
say $image->id;
say join ', ', @{$image->repo_tags};
}
# Inspect image details
my $image = $docker->images->inspect('nginx:latest');
# Tag and push
$docker->images->tag('nginx:latest', repo => 'myrepo/nginx', tag => 'v1');
$docker->images->push('myrepo/nginx', tag => 'v1');
# Remove image
$docker->images->remove('nginx:latest', force => 1);
# Snapshot a container into an image
my $new = $docker->images->commit(container => $id, repo => 'myapp', tag => 'snap');
# Air-gapped roundtrip: export here, carry the tar over, load there
my $export = $docker->images->get('myapp:snap'); # raw tar bytes
$docker->images->load($export);
# Reclaim the build cache (not the same thing as prune)
$docker->images->build_prune(all => 1);
DESCRIPTION
This module provides methods for managing Docker images including pulling, listing, tagging, pushing to registries, and removal.
list and inspect return generated API::Docker::Type objects carrying the convenience methods of API::Docker::Role::Entity::Image, so $image->tag and $image->remove work on either. Which class each returns, and where the two disagree, is below.
Accessed via $docker->images, or through "using" in API::Docker::Role::Using for a run of calls that needs its own transport bound: $docker->images->using(read_timeout => 5).
The two image shapes
The daemon describes an image two ways and the swagger has two definitions for it, so this class returns two classes:
"list" returns API::Docker::Type::ImageSummary objects -- one per entry of
GET /images/json."inspect" returns an API::Docker::Type::ImageInspect -- the body of
GET /images/{name}/json.
They overlap but do not line up, and the field names are the swagger's own spelling in snake_case (Id is ->id, RepoTags is ->repo_tags, SharedSize is ->shared_size). The differences worth knowing before reading a value off the wrong one:
->createdis an integer Unix epoch on a summary and an RFC 3339 string on an inspect. Same field name, two types --IntandStrin the model, which is the swagger's own answer, not a normalisation this client applies. The same split a container has, see "The two container shapes" in API::Docker::API::Containers.The parent layer is
->parent_idon a summary and->parenton an inspect. Both are empty for an image pulled from a registry rather than built locally, and the swagger marks the inspect one deprecated.->labelsis top-level on a summary only. An inspect carries the labels under->config->labels, where->configis the API::Docker::Type::ImageConfig the image runs containers with --->cmd,->env,->entrypoint,->exposed_portsand the rest.->containers(how many containers use the image) and->shared_sizecome from a summary only. The swagger says of both that-1means the value was not calculated, and ofSharedSizethat it is not calculated by default -- so treat-1as "unknown", not as a count.->architecture,->os,->os_version,->variant,->author,->comment,->docker_version,->config,->root_fs,->graph_driverand->metadatacome from an inspect only.->id,->repo_tags,->repo_digests,->size,->descriptorand->manifestsare on both and mean the same thing. The swagger declares every field of a summary required and no field of an inspect, which the model records but does not enforce -- see "sinceis documentation" in API::Docker::Type.
There is no ->virtual_size: the swagger dropped VirtualSize from both definitions after v1.44, and engines that still send it -- the Podman on this machine does -- have it kept verbatim in ->unknown_fields->{VirtualSize}, where TO_JSON writes it back unchanged. t/type_fixture_passthrough.t pins that.
client
Reference to API::Docker client. Weak reference to avoid circular dependencies.
list
my $images = $images->list(all => 1);
List images. Returns an ArrayRef of API::Docker::Type::ImageSummary objects, each carrying the methods of API::Docker::Role::Entity::Image.
Options:
all- Show all images (default hides intermediate images)digests- Include digest informationfilters- HashRef of filter name to ArrayRef of string values, e.g.{ dangling => ['true'] }. Shape-checked and normalised by API::Docker::Role::Filters
build
# Build from a tar archive
my $tar_data = path('context.tar')->slurp_raw;
my $events = $docker->images->build(
context => $tar_data,
t => 'myimage:latest',
dockerfile => 'Dockerfile',
);
# Build with build args
my $events = $docker->images->build(
context => $tar_data,
t => 'myapp:v1',
buildargs => { APP_VERSION => '1.0' },
nocache => 1,
);
Build an image from a tar archive containing a Dockerfile and build context.
The context parameter is required and must contain the raw bytes of a tar archive (or a scalar reference to one).
Returns an ArrayRef of build events, one per object in the engine's newline-delimited JSON stream, even when the stream carried a single object (q => 1 produces exactly one). A successful build returns; a failed one croaks.
my $events = $images->build(context => $tar, t => 'myapp:latest');
my ($aux) = grep { $_->{aux} } @$events;
my $image_id = $aux->{aux}{ID};
The engine answers a failed build with HTTP 200 and reports the failure as an errorDetail object inside the stream, so nothing about the response status says the build broke. This method used to return that stream like any other and leave the scan to the caller, which meant a caller who did not know to scan reported a broken build as a success. It now croaks with an API::Docker::Error::Stream instead:
my $events = eval { $images->build(context => $tar, t => 'myapp:latest') };
if (my $err = $@) {
warn "$err"; # the reason, with Carp's location suffix
for my $event (@{ $err->events }) { # the build output up to the failure
print $event->{stream} if defined $event->{stream};
}
}
The exception stringifies to what a plain croak would have produced, so existing eval-and-inspect-$@ code needs no change.
Options:
context- Tar archive bytes (required)dockerfile- Path to Dockerfile within the archive (default:Dockerfile)t- Tag for the image (e.g.name:tag)q- Suppress verbose build outputnocache- Do not use cache when buildingpull- Always pull base imagerm- Remove intermediate containers (default: true)forcerm- Always remove intermediate containersbuildargs- HashRef of build-time variableslabels- HashRef of labels to set on the imagememory- Memory limit in bytesmemswap- Total memory (memory + swap), -1 to disable swapcpushares- CPU shares (relative weight)cpusetcpus- CPUs to use (e.g.0-3,0,1)cpuperiod- CPU CFS period (microseconds)cpuquota- CPU CFS quota (microseconds)shmsize- Size of /dev/shm in bytesnetworkmode- Network mode during buildplatform- Platform (e.g.linux/amd64)target- Multi-stage build targetregistry_config- Registry credentials for the base images the build pulls, sent asX-Registry-Config. A HashRef mapping each registry hostname to its AuthConfig --{ 'registry.example:5000' => { username => 'me', password => 'secret' } }-- so aFROM private.registry/...can authenticate, and a build drawing from several registries can carry all of them at once. A pre-encoded base64 string is also accepted. Sent only when given. This is notauth/X-Registry-Auth, which carries a single AuthConfig;/builduses the map form. See API::Docker::Role::RegistryAuthon_event- CodeRef called with each build event as it arrives, instead of the ArrayRef being collected and returned; see below
Progress as it arrives
Without a callback the whole stream is read before anything is parsed, so a build that takes two minutes is two minutes of silence followed by all of its output at once. Pass on_event and the events are handed over as the daemon sends them:
my $summary = $images->build(
context => $tar,
t => 'myapp:latest',
on_event => sub {
my ($event, $stop) = @_;
print $event->{stream} if defined $event->{stream};
},
);
$summary; # { delivered => 41, stopped => 0 }
With a callback the return value is that summary HashRef, not the events: delivered is how many went to the callback, stopped is 1 when the callback ended the stream and 0 when the daemon did. Nothing is accumulated, so a caller that wants the aux event with the image id in it must keep that event itself as it goes by. See "Streaming a response as it arrives" in API::Docker::Role::HTTP.
The same section applies to "pull", "push" and "load", which take on_event on the same terms.
A failed build still croaks, one event earlier
The errorDetail check runs either way, so a failed build croaks with an API::Docker::Error::Stream on both paths. What differs is when, and what the exception carries:
Buffered, the stream is scanned once it is complete, and
$err->eventsis the whole event list -- all the build output that led up to the failure.Streamed, the check runs per event, so the croak happens at the event that reports the failure rather than when the daemon eventually closes. The exception then carries that one event alone: a callback stream keeps no history, having handed every earlier event to the callback already. The failing event itself is not delivered.
So a caller that reads the progress out of $err->events must, on this path, collect it in the callback instead:
my @output;
my $summary = eval {
$images->build(context => $tar, t => 'myapp:latest',
on_event => sub { push @output, $_[0] });
};
if (my $err = $@) {
warn "$err"; # the reason, as before
# $err->events is the failing event; @output is what preceded it
}
pull
my $events = $images->pull(fromImage => 'nginx', tag => 'latest');
my $events = $images->pull(fromImage => 'nginx:1.25'); # tag rides in the name
my $events = $images->pull(fromImage => 'alpine@sha256:...'); # by digest
Pull an image from a registry.
tag defaults to latest only when fromImage carries no tag or digest of its own. The engine appends tag to the reference rather than treating it as a fallback, so defaulting it onto an already-qualified name breaks the pull: measured against Docker 29.7.2 (API 1.55) pull(fromImage => 'nginx:1.25') would silently fetch nginx:latest and report success, and against Podman 5.8.4 (compat API 1.44) the same request answers 500 invalid reference format for nginx:1.25:latest. A digest reference breaks the same way on both. So tag is sent only if given explicitly, or defaulted to latest when the name carries neither a :tag (in the segment after the last /) nor an @digest. A registry host:port/ prefix is not mistaken for a tag.
Returns an ArrayRef of progress events, one per object in the engine's newline-delimited JSON stream, even when the stream carried a single object.
A failed pull croaks either way, but which way depends on the engine, so do not write code that expects one of them:
Docker reports it in the stream. The response is HTTP 200 and the failure is an
errorDetailobject among the progress events; this method croaks with an API::Docker::Error::Stream, whose->eventsholds the progress that preceded the failure.Podman reports it in the status line. Measured against the rootless socket (5.4.2, API 1.41): pulling a repository that does not exist answers
403 Forbiddenwith{"message":"denied: requested access to the resource is denied"}, and an existing repository with a missing tag answers404 Not Foundwith{"message":"manifest unknown: manifest unknown"}. Neither reaches the stream at all -- the transport's own status handling croaks with an API::Docker::Error::HTTP -- which is that same string to anything inspecting$@as text -- first.
Catching API::Docker::Error::Stream specifically is therefore not a reliable way to catch a failed pull. eval and inspect $@ as a string, which both cases satisfy.
Options:
fromImage- Image name to pull (required)tag- Tag to pull. Defaulted tolatestonly whenfromImagecarries no tag or digest of its own; see aboveauth- Registry credentials for pulling from a private registry, sent asX-Registry-Auth. A HashRef of the usual keys (username,password,serveraddress, oridentitytoken) or a pre-encoded base64 string, exactly as "push" takes it. Unlikepush, the header is sent only whenauthis given -- an anonymous pull carries none, which the engine reads as the anonymous case. See API::Docker::Role::RegistryAuthon_event- CodeRef called with each progress event as it arrives, instead of the ArrayRef being collected and returned. The return value is then the summary HashRef and a stream failure croaks one event in, exactly as for "build"; see "Progress as it arrives"
inspect
my $image = $images->inspect('nginx:latest');
Get detailed information about an image. Returns an API::Docker::Type::ImageInspect, which is not the class "list" returns -- see "The two image shapes".
history
my $history = $images->history('nginx:latest');
Get image history (layers). Returns ArrayRef of layer information.
push
my $events = $images->push('myrepo/nginx', tag => 'v1');
$images->push('myrepo/nginx', auth => {
username => 'me',
password => 'secret',
serveraddress => 'https://index.docker.io/v1/',
});
Push an image to a registry. Optionally specify tag.
Returns an ArrayRef of progress events, one per object in the engine's newline-delimited JSON stream, even when the stream carried a single object.
A failed push croaks, by one of two routes depending on the engine -- an unauthorised push to a private registry is the common case, and it is exactly the one that must not be reported as a success.
Docker reports it inside a 200 stream as an errorDetail object, which croaks with an API::Docker::Error::Stream carrying the progress events. Podman puts an errorDetail body behind a real error status instead: measured against the rootless socket (5.4.2, API 1.41), a push to an unreachable registry answers 500 Internal Server Error with {"errorDetail":{"message":"... connection refused"},"error":"..."}, so the transport's status handling croaks with an API::Docker::Error::HTTP -- which is that same string to anything inspecting $@ as text -- before the stream is ever decoded. That body carries no message key, so the whole JSON object ends up as the croak text.
Either way the failure is loud. Inspect $@ as a string rather than testing for the exception class, which only the first route produces.
The Docker Engine requires an X-Registry-Auth header on every push, even for anonymous attempts; the header is always sent. Pass auth as a hashref of credentials (typical keys: username, password, serveraddress, or identitytoken), or as a pre-encoded base64 string. Without auth the header carries an empty JSON object.
Options:
tag- Tag to pushauth- Registry credentials, as aboveon_event- CodeRef called with each progress event as it arrives -- layer by layer, rather than the whole upload in one silence -- instead of the ArrayRef being collected and returned. The return value is then the summary HashRef and a stream failure croaks one event in, exactly as for "build"; see "Progress as it arrives"
tag
$images->tag('nginx:latest', repo => 'myrepo/nginx', tag => 'v1');
Tag an image with a new repository and/or tag name.
remove
$images->remove('nginx:latest', force => 1);
Remove an image.
Options:
force- Force removalnoprune- Do not delete untagged parents
search
my $results = $images->search('nginx', limit => 25);
Search Docker Hub for images. Returns ArrayRef of search results.
Options:
limit- Maximum number of resultsfilters- HashRef of filter name to ArrayRef of string values; the engine acceptsis-official,is-automatedandstarshere. The boolean ones want the string,{ 'is-official' => ['true'] }, andstarsa number written as one -- API::Docker::Role::Filters takes care of both and croaks on a shape the daemon would refuse
prune
my $result = $images->prune(filters => { dangling => ['true'] });
Delete unused images. Returns hashref with ImagesDeleted and SpaceReclaimed.
Options:
filters- HashRef of filter name to ArrayRef of string values; the engine acceptsdangling,untilandlabelhere. Shape-checked and normalised by API::Docker::Role::Filters
get
use Path::Tiny;
my $tar = $images->get('alpine:3');
path('alpine.tar')->spew_raw($tar);
Export one image, and the history behind it, as a tar archive -- the endpoint behind docker image save. Together with "load" it is the only way in or out of a daemon that does not go through a registry.
The return value is raw bytes, not a decoded structure. The engine answers with the tar stream itself, and the transport is told to hand it back untouched (raw => 1), so what arrives is byte for byte what the daemon wrote. Write it with a binary-safe file handle -- path(...)->spew_raw, or binmode on a handle of your own. Treating it as text corrupts it, and nothing about the value announces that it is binary.
The archive holds one tarball per layer, a config JSON per image, manifest.json and repositories. Measured against Podman 5.4.2 (API 1.41): the response is chunked with Content-Type: application/octet; charset=us-ascii, where Docker sends application/x-tar -- the transport looks at neither, so the difference does not reach the caller. Exporting alpine:3 through this method produced bytes md5-identical to what curl --unix-socket wrote for the same request, all 8705536 of them, so the chunked reader is binary-clean.
An unknown image croaks. On the same engine that is 404 Not Found with {"message":"failed to find image ...: image not known"}.
Exporting without buffering the archive
The whole archive is buffered in memory before it is returned, so exporting a large image costs its full size in RAM. Pass on_chunk and the bytes are handed over as they arrive instead, and nothing is kept:
use Path::Tiny;
my $out = path('alpine.tar')->openw_raw;
my $summary = $images->get('alpine:3',
on_chunk => sub { print {$out} $_[0] });
close $out;
$summary; # { delivered => 266, stopped => 0 }
The units are whatever the transport read, not a fixed size: a chunk boundary carries no meaning in a tar stream, and the only guarantee is that concatenating them in order gives the same bytes the buffered call returns. Write them to a binary-safe handle, exactly as for the buffered value.
Measured against Podman 5.4.2 (API 1.41): exporting alpine:3 this way delivered its 8705536 bytes in 266 pieces, md5-identical to what the buffered call returns for the same request, with no more than one piece held at a time.
With a callback the return value is the summary HashRef { delivered => N, stopped => 0|1 }, not the archive: delivered is how many pieces went to the callback, stopped is 1 when the callback ended the transfer. Stopping leaves a truncated archive behind -- the export is one tar stream, not a sequence of independent records -- so stop only to abandon it. See "Streaming a response as it arrives" in API::Docker::Role::HTTP.
Options:
on_chunk- CodeRef called with each piece of the archive as it arrives, instead of the whole thing being returned
get_all
my $tar = $images->get_all('alpine:3', 'registry:2');
my $tar = $images->get_all([ 'alpine:3', 'registry:2' ]);
Export several images into one tar archive. Takes the names as a list or as a single ArrayRef; at least one is required.
The return value is raw bytes, exactly as for "get" -- see there for what that means for writing it out.
manifest.json inside the archive carries one entry per image, so a single tar can be carried to another host and loaded in one "load" call. Measured against Podman 5.4.2 (API 1.41): asking for no names at all answers 400 Bad Request with {"message":"no images to download"}.
on_chunk works here exactly as it does for "get" -- several images make a bigger archive, so this is where not buffering it matters most -- but it can only be passed with the ArrayRef form:
my $summary = $images->get_all([ 'alpine:3', 'registry:2' ],
on_chunk => sub { print {$out} $_[0] });
The list form takes names and nothing else: a trailing option pair in it would be indistinguishable from two more image names. Options after the ArrayRef must come in pairs; an odd number croaks.
A transport bound is not one of those options: it goes on the resource class, which works with either form -- $docker->images->using(read_timeout => 5) ->get_all('alpine:3'), see API::Docker::Role::Using.
Measured against Podman 5.4.2 (API 1.41): alpine:3 and registry:2 together came to 34725888 bytes in 1060 pieces, none of which had to be held.
load
use Path::Tiny;
my $events = $images->load(path('alpine.tar')->slurp_raw);
for my $event (@$events) {
print $event->{stream} if defined $event->{stream};
}
Import a tar archive produced by "get" or "get_all" -- the endpoint behind docker image load. The archive is the request body; pass it as raw bytes or as a scalar reference to them, the way "build" takes its context.
Returns an ArrayRef of progress events, one per object in the engine's newline-delimited JSON stream, even when the stream carried a single object. The last of them names what was imported:
my ($loaded) = grep { ($_->{stream} // '') =~ /^Loaded image/ } @$events;
Options:
quiet- Suppress the per-layer progress detail in the response streamon_event- CodeRef called with each progress event as it arrives, instead of the ArrayRef being collected and returned. The return value is then the summary HashRef and a stream failure croaks one event in, exactly as for "build"; see "Progress as it arrives"
quiet changes how much the engine says, not what this method returns: the body stays newline-delimited JSON and the return stays an ArrayRef either way. Podman ignores it entirely -- measured against 5.4.2 (API 1.41), quiet unset, 0 and 1 all produce the identical single {"stream":"Loaded image: ..."} object. Should an engine answer a quiet load with a body of no bytes at all, the transport still returns [], not undef -- the ndjson branch in "_request" in API::Docker::Role::HTTP runs before the empty-body check that would return undef, so a caller that iterates the result unconditionally needs no guard for this case.
A failed load croaks, but by which route depends on the engine, the same split "pull" and "push" have. Docker reports it as an errorDetail object inside a 200 stream, which croaks with an API::Docker::Error::Stream carrying the events. Podman reports it in the status line instead: measured against 5.4.2, a body that is not an image archive answers 500 Internal Server Error with {"message":"failed to load image: payload does not match any of the supported image formats: ..."}, and the transport's status handling croaks with an API::Docker::Error::HTTP -- which is that same string to anything inspecting $@ as text -- before any stream is decoded. Inspect $@ as a string rather than testing for the exception class.
The archive is sent as one buffered request body, so loading a large image costs its full size in RAM.
commit
my $result = $images->commit(
container => $container_id,
repo => 'myapp',
tag => 'snapshot',
comment => 'after the migration ran',
);
my $image_id = $result->{Id};
# With a config override and Dockerfile instructions
$images->commit(
container => $container_id,
repo => 'myapp',
tag => 'v2',
config => { Cmd => [ '/bin/sh' ], Labels => { built => 'here' } },
changes => [ 'EXPOSE 8080', 'LABEL stage=release' ],
);
Create an image from a container's current filesystem. This is the one image-producing path that does not go through a build context, and it is how a caller snapshots a container it has been exec-ing into.
Returns the raw daemon response, a HashRef with an Id key. Measured against Podman 5.4.2 (API 1.41) the status is 201 Created and Id is a bare hex digest with no sha256: prefix; Docker prefixes it. Do not compare it literally against an id from inspect without normalising.
Options:
container- Container id or name to commit (required)repo- Repository for the new image, e.g.myapptag- Tag for the new imagecomment- Commit message stored in the image historyauthor- Author, e.g.Jane <jane@example.com>pause- Pause the container while committing (engine default is true)changes- Dockerfile instructions to apply to the new image, as a single string or an ArrayRef of them; an ArrayRef is joined with newlines, which is what the engine's parser expectsconfig- HashRef of container configuration to override on the new image (Cmd,Env,Labels,ExposedPorts, ...), sent as the request body. Measured against Podman 5.4.2:Cmdreplaces the container's,Envis merged onto the environment the container inherited, and aLabelshere lands alongside aLABELgiven inchanges-- the two are applied together, not one instead of the other
build_prune
my $result = $images->build_prune(all => 1);
my $freed = $result->{SpaceReclaimed};
# Keep 5 GB of cache
$images->build_prune(keep_storage => 5 * 1024 * 1024 * 1024);
Clear the BuildKit build cache. This is not "prune", and the two are not interchangeable: "prune" deletes unused images, this deletes the intermediate build cache that "build" writes. Neither touches the other's storage, and on a machine that builds often the build cache is usually the larger of the two.
Returns the raw daemon response, a HashRef with CachesDeleted and SpaceReclaimed.
Podman does not implement this endpoint. Measured against 5.4.2 (API 1.41): POST /build/prune answers 404 Not Found with a text/plain body of Not Found -- not the JSON {"message":...} shape its other errors use -- at every version prefix tried, and there is no libpod equivalent either. The transport croaks with Docker API error (404): Not Found, the plain body verbatim, because it is not JSON to unwrap. A caller that must work on both engines has to treat that 404 as "no build cache to clear here" rather than as a transport fault.
Options:
keep_storage- Bytes of cache to keep. Sent as the engine'skeep-storage, which is also accepted as the option name; the underscore form exists because the hyphenated one has to be quoted in a Perl hashall- Remove all cache, not just the dangling entriesfilters- HashRef of filters, e.g.{ until => ['24h'] }; values are ArrayRefs of strings, shape-checked and normalised by API::Docker::Role::Filters, and passed to the transport unencoded because it JSON-encodes a HashRef params value itself
SEE ALSO
API::Docker - Main Docker client
API::Docker::Role::Entity::Image - the convenience methods the returned objects carry
API::Docker::Type::ImageSummary - the fields
listreturnsAPI::Docker::Type::ImageInspect - the fields
inspectreturnsAPI::Docker::Role::RegistryAuth - the
X-Registry-Authencodingpushuses, shared with the other registry-facing endpointsAPI::Docker::Error::Stream - Raised by
build,pull,pushandload
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-api-docker/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.