Skip to content

About

Build and publish Docker images as Dist::Zilla release artifacts

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Dist::Zilla::Plugin::Docker::API

Build and publish Docker images as Dist::Zilla release artifacts using API::Docker — no docker CLI shell-outs.

Why this plugin?

When releasing a Perl distribution, you often want to ship a Docker image alongside it. This plugin integrates the image build directly into the Dist::Zilla build / release workflow:

  • No CLI dependency — uses API::Docker to talk to the Docker daemon directly.
  • No shell quoting bugs — all parameters passed as structured data.
  • Streaming progress — real-time Docker build output via callbacks.
  • Template expansion — tags, build args and labels use Dist::Zilla template variables (%v, %n, %g, ...).

Installation

cpanm Dist::Zilla::Plugin::Docker::API

Quick start

[Docker::API]
image      = ghcr.io/example/my-app
dockerfile = Dockerfile

tag = latest
tag = %v
  • dzil build builds the image and applies every tag locally.
  • dzil release re-tags the already-built image and (by default) pushes it.

The same tag list is used in both phases — there is no separate build-vs-release list.

Behavior

Dzil command Docker behavior
dzil build Build image, apply every tag. No push.
dzil release Re-tag the already-built image with every tag, push (if release_push=1).

Container engine

Builds and pushes go through API::Docker, which speaks the Docker Engine HTTP API over a socket. No docker binary is involved at any point, so any engine serving that API will do and Docker itself need not be installed. Podman's rootless socket is a tested alternative:

systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"

The socket is located from DOCKER_HOST, falling back to /var/run/docker.sock and nothing else — Docker contexts are not consulted, so a daemon picked with docker context use will not be found. target reaches the engine unchanged, so multi-stage builds behave the same either way.

Since after_build builds an image unconditionally, the plugin verifies in before_build that an engine actually answers, and gives up there instead of after a whole distribution has been assembled:

[Docker::API] Docker::API engine ready: Podman Engine 5.4.2 (API 1.41)

During dzil release (when release_enabled and release_push are both true), the same before_build hook also pre-flights the registry credential the push would use — handed to the engine's POST /auth before anything is built, so an expired or rejected login is caught before the image is built and tagged, not after. A plain dzil build never triggers this. No credential resolved for the registry is not a failure — an anonymous push is legal, so nothing is checked. A failed check is fatal, and its message says only that the check failed, not that the credential was rejected: Podman answers a rejected credential and an unreachable registry with the same 500, so the two can't be told apart from the status, and the engine's own text is included instead.

Variable Effect
DOCKER_HOST Socket or URL of the engine to talk to
DZIL_DOCKER_API_SKIP_PRECHECK=1 Skip both the engine version probe and the release registry-credential check; an unreachable engine or a bad credential then only surfaces once the build (or push) reaches it
DZIL_DOCKER_API_SKIP=1 Skip the image build entirely for one run — no engine contact, no image. dzil release refuses to run with this set

Configuration

Required

  • image — full image repository name, e.g. ghcr.io/user/my-app.

Tags

tag is multi-value and template-enabled. Default is latest, %V and %v (three tags). Setting tag yourself replaces that list, it does not add to it.

tag = latest
tag = %v
tag = v%v
tag = build-%v-%g

Available template variables:

Variable Description Example
%n Distribution name My-App
%v Distribution version 1.234
%V Major version (leading digits) 1
%vmaj Same as %V 1
%vmin Minor version (second field) 234
%t Trial suffix -TRIAL or empty
%g Short git SHA (7 chars) a1b2c3d
%G Full git SHA (40 chars) a1b2c3d4e5f6...
%b Git branch name main
%d Dist::Zilla build root /path/to/build
%o Source repository root /path/to/repo
%a Release archive path /path/to/x.tgz
%p Plugin instance name Docker::API

Release switch

release_push = 1     # push during release (default: 1)

Build options

dockerfile = Dockerfile     # name of the Dockerfile in the build root
pull       = 0
no_cache   = 0
rm         = 1
force_rm   = 1
platform   = linux/amd64    # exactly one; a second value is a fatal error
Context mode Description
build Tar of Dist::Zilla's build_root
source Tar of the source repository
archive The release tarball

Build args and labels

Both build_arg and label are multi-value and template-enabled (KEY=VALUE form).

build_arg = DIST_NAME=%n
build_arg = DIST_VERSION=%v

label = org.opencontainers.image.title=%n
label = org.opencontainers.image.version=%v

Tag-exists check and trials

fail_if_tag_exists   = 0     # abort the release if any tag already exists remotely (default: 0)
skip_latest_on_trial = 1     # omit `latest` for trial releases (default: 1)

fail_if_tag_exists asks the registry, not the local daemon, whether each tag already exists — through API::Docker's distribution->exists (GET /distribution/{name}/json), using the credential resolved for image or an anonymous request when none applies. The check runs before anything is tagged or pushed, and only when release_push is also on.

An engine with no /distribution route — rootless Podman among them — can't answer that question at all, and that is treated as a release-stopping failure rather than as "the tag is free": the release aborts with the engine's own error and a reminder that fail_if_tag_exists = 0 releases without the check. Turning the switch on against such an engine means every release fails until it's turned back off.

Registry auth

Credentials are resolved from an ordered list of auth files — for the target registry, the first file with a usable entry wins, and a file with none falls through to the next, so a Docker config and a Podman auth file can each cover different registries in the same run:

  1. REGISTRY_AUTH_FILE, if set — the path it names, directly.
  2. config.json in the directory named by DOCKER_CONFIG, if set.
  3. $XDG_RUNTIME_DIR/containers/auth.json, if XDG_RUNTIME_DIR is set.
  4. containers/auth.json under XDG_CONFIG_HOME, or under ~/.config when that is unset.
  5. ~/.docker/config.json.

~/.dockercfg (Docker's legacy pre-config.json format) is never read. All five share the same auths block, and within the matching registry's entry the selection is the same regardless of which file supplied it, in this order: an identitytoken, a base64 auth field (username:password), or plain username / password fields. For Docker Hub the usual spellings (https://index.docker.io/v1/ and /v2/, index.docker.io, docker.io) are all tried.

A plain docker login or podman login is picked up with no extra setup: Podman's own default auth file is candidate 3 above. If podman login --authfile PATH was used to write somewhere else, set REGISTRY_AUTH_FILE=PATH in the environment dzil runs in so candidate 1 finds it.

A credsStore or credHelpers entry delegating the secret to an external helper is not supported: the auths entry is then empty, no credential is found, and the push is attempted anonymously. The release precheck says so out loud before building:

[Docker::API] Docker::API: no registry credentials found for ghcr.io/user/my-app - the release push will be anonymous

There is no DOCKER_AUTH_CONFIG support — that variable is read nowhere in this plugin.

Use via @Author::GETTY

In a distribution that uses @Author::GETTY, the bundle exposes a subsection that constructs a Docker::API plugin per occurrence:

[@Author::GETTY]
docker_image = registry/app
docker_tags  = latest %v

[@Author::GETTY::Docker / runtime-root]
target = runtime-root

[@Author::GETTY::Docker / runtime-user]
target = runtime-user
tags   = user
local  = 1

See the bundle docs for the full inheritance model.

Deprecated options

The following config keys still work but emit a deprecation warning:

Deprecated Replacement
build_tag tag
release_tag tag
repository image
push release_push

When build_tag and/or release_tag are given, their values are merged (build first, then release) into tag. If tag is also set explicitly, it wins and the legacy values are ignored.

Removed options

phase, build_load, release_load and load are no longer supported and have no replacement: they are warned about and discarded. The image is built through the engine API, where it lands in the engine's image store anyway.

Result object

build_image returns a Dist::Zilla::Plugin::Docker::API::Result:

my $result = $plugin->client->build_image(...);
$result->image_id;   # Docker image ID
$result->tags;       # ArrayRef of applied tags
$result->pushed;     # ArrayRef of successfully pushed tags
$result->digest;     # SHA256 digest of pushed image
$result->warnings;   # ArrayRef of non-fatal errors

Architecture

Helper classes:

  • Dist::Zilla::Plugin::Docker::API::TagTemplate — template variable expansion
  • Dist::Zilla::Plugin::Docker::API::Client — API::Docker adapter
  • Dist::Zilla::Plugin::Docker::API::Result — build/push result

Roles consumed:

  • Dist::Zilla::Role::Plugin
  • Dist::Zilla::Role::AfterBuild
  • Dist::Zilla::Role::Releaser

See also

License

Copyright (c) 2026 Torsten Raudssus https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as Perl 5 itself.

About

Build and publish Docker images as Dist::Zilla release artifacts

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages