Docker
Build and push multi-arch Docker images via dockers_v2
Anodizer builds Docker images via docker buildx, producing multi-arch OCI
image indexes in a single push. The canonical (and only) surface is per-crate
dockers_v2:. The legacy GoReleaser V1 dockers: block is rejected at
config-load time with a migration error pointing here.
The
docker_v2:spelling is still accepted as a back-compat serde alias, so existing configs keep working.
Classification
Packager — builds and pushes Docker images. Required: false (optional packager stage).
Placement
dockers_v2: is a per-crate field — it lives under crates[].dockers_v2,
not at the top level:
crates:
- name: myapp
dockers_v2:
- id: myapp
dockerfile: Dockerfile
images:
- ghcr.io/myorg/myapp
tags:
- "{{ Version }}"
- latest
platforms:
- linux/amd64
- linux/arm64
Workspace-wide defaults can be set under defaults.dockers_v2: (single struct,
deep-merged into each crate's first dockers_v2[] entry).
Minimal config
images defaults to ghcr.io/<owner>/<crate> — the owner is resolved from
release.github or the origin git remote, and each crate gets its own name.
So a typical project only declares the dockerfile and tags:
crates:
- name: myapp
dockers_v2:
- dockerfile: Dockerfile
tags: ["{{ Version }}", "latest"]
Set images explicitly to publish under a different registry/name (e.g.
docker.io/<owner>/<name>), or when the owner can't be resolved (no GitHub
remote and no release.github) — an unresolvable owner leaves images empty
and the pipe emits no tags.
crates:
- name: myapp
dockers_v2:
- dockerfile: Dockerfile
images: ["docker.io/myorg/myapp"] # override the ghcr.io default
tags: ["{{ Version }}", "latest"]Full config reference
crates:
- name: myapp
dockers_v2:
- id: myapp # optional; unique handle (for --id filters)
ids: [myapp] # optional; build-ID filter
dockerfile: Dockerfile # required; path to Dockerfile
images: # optional; default ghcr.io/<owner>/<crate>
- ghcr.io/myorg/myapp
tags: # required; one image:tag per (image × tag)
- "{{ Version }}"
- "v{{ Version }}"
- latest
labels: # optional; --label key=value (merged over auto OCI labels)
org.opencontainers.image.source: "https://github.com/myorg/myapp"
oci_labels: true # optional; auto-inject org.opencontainers.image.* (default true)
annotations: # optional; --annotation key=value
org.opencontainers.image.licenses: "MIT"
extra_files: # optional; copied into build context
- LICENSE
- README.md
platforms: # optional; --platform list
- linux/amd64
- linux/arm64
build_args: # optional; --build-arg KEY=VALUE
VERSION: "{{ Version }}"
BIN: anodizer
retry: # optional; per-pipe retry
attempts: 10
delay: "10s"
max_delay: "5m"
flags: # optional; raw extra buildx flags
- --provenance=false
skip: false # optional; bool or template (accepts deprecation-warned `disable:` alias)
sbom: true # optional; --sbom=true
hooks: # optional; pre/post hooks
pre:
- cmd: ./scripts/prepare-build-context.sh
dir: .
post:
- cmd: echo "built {{ Images | join(sep=',') }}"
use: buildx # optional; "buildx" (default) | "podman" (Linux-only)Authentication
Docker registry credentials are resolved from the host Docker configuration
(~/.docker/config.json). Run docker login (or use
docker/login-action@v3 in CI)
before the release step. In anodizer-action, set docker-registry /
docker-username / docker-password and the action logs in for you.
Common gotchas
docker buildxrequired: multi-architecture builds require Docker Buildx. Ensure the buildx plugin is installed and a builder with multi-arch support is configured. In GHA, usedocker/setup-qemu-action@v3+docker/setup-buildx-action@v3.- No legacy
dockers:: the top-level GoReleaser V1dockers:block is rejected at config-load time with a clear migration error. Port todockers_v2:(this page). skip: true(ordisable: truevia deprecation-warned back-compat alias) builds the image locally but does not push.- Platform strings: use Docker platform notation (
linux/amd64,linux/arm64), not Rust target triples. - Build-args leak: buildx records
build_argsin image history by default. Prefer{{ Env.VAR }}over raw user-config strings for secrets. use: podmanis Linux-only: configs settinguse: podmanon macOS or Windows fail at config-validation time. The podman backend disables buildx-only flags (--rewrite-timestamp,--provenance,--attest,--cache-from/to,--output,--sbom).
Republish / update behavior
Pushing the same image:tag to a registry overwrites the previous push.
Re-running the docker stage with the same images / tags re-pushes the
image. There is no replace_existing_* flag — registry semantics handle it.
dockers_v2 config fields
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique handle for this entry (for --id filters) |
ids | list | — | Build-ID filter: only include artifacts whose id is in this list |
dockerfile | string | — | Path to Dockerfile (required) |
images | list | ghcr.io/<owner>/<crate> | Base image names. Defaults to the per-crate ghcr.io image — owner from release.github or the origin remote. Empty (no tags emitted) when the owner can't be resolved. Set to override. |
tags | list | — | Tag suffixes — one full image ref per (image × tag) |
labels | map | none | OCI labels via --label key=value. Merged over the auto-injected org.opencontainers.image.* set; user keys win. |
oci_labels | bool/template | true | Auto-inject the predefined org.opencontainers.image.* labels. Set false to opt out. See Auto-injected OCI labels. |
annotations | map | none | OCI annotations via --annotation key=value |
extra_files | list | none | Extra files copied into the build context |
platforms | list | host | Target platforms (linux/amd64, linux/arm64, ...) |
build_args | map | none | --build-arg KEY=VALUE pairs |
retry | object | top-level retry: | Per-pipe retry config (deprecated; prefer top-level) |
flags | list | none | Arbitrary extra docker buildx build flags |
skip | bool/template | false | Skip the build. Accepts deprecation-warned disable: alias |
sbom | bool/template | false | Add --sbom=true to buildx |
hooks | object | none | pre: / post: hooks; see Hooks below |
use | string | buildx | Backend: buildx or podman (Linux-only) |
Auto-injected OCI labels
anodizer auto-injects the standard predefined org.opencontainers.image.*
labels on every dockers_v2 build — you do not declare them by hand. Each
label is emitted only when its source value resolves:
| Label | Source |
|---|---|
org.opencontainers.image.created | RFC-3339 date from the resolved SOURCE_DATE_EPOCH — never wall-clock, so the image stays byte-reproducible |
org.opencontainers.image.source | release repo URL (SSH remotes normalized to their https:// web base) |
org.opencontainers.image.revision | released commit SHA |
org.opencontainers.image.version | released version (matches the image tag) |
org.opencontainers.image.title | the crate's name |
org.opencontainers.image.description | meta.description (Cargo-derived) |
org.opencontainers.image.licenses | meta.license |
org.opencontainers.image.url | meta.homepage |
org.opencontainers.image.documentation | metadata.documentation |
org.opencontainers.image.vendor | the crate's first author, with any <email> suffix stripped |
The documentation label is driven by the top-level metadata.documentation
field:
metadata:
documentation: "https://tj-smith47.github.io/anodizer"
→ --label org.opencontainers.image.documentation=https://tj-smith47.github.io/anodizer.
User labels always win. Any key you set under labels: overrides the
auto-derived value for that key (the auto value never clobbers an explicit
user label). To suppress the whole auto-label set, set oci_labels: false:
crates:
- name: myapp
dockers_v2:
- dockerfile: Dockerfile
tags: ["{{ Version }}"]
oci_labels: false # opt out of the predefined org.opencontainers.image.* labels
labels:
org.opencontainers.image.source: "https://github.com/myorg/myapp" # set your own
created deriving from SOURCE_DATE_EPOCH (seeded by the build stage) is what
keeps two builds of the same commit byte-identical — a wall-clock created
would defeat reproducibility, so anodizer omits the label entirely when no
source date is resolvable rather than stamping the current time.
Multi-arch builds
crates:
- name: myapp
dockers_v2:
- dockerfile: Dockerfile
images: ["ghcr.io/myorg/myapp"]
tags: ["{{ Version }}"]
platforms:
- linux/amd64
- linux/arm64
A single docker buildx build --platform=linux/amd64,linux/arm64 --push ...
emits one multi-arch OCI image index — no separate
docker_manifests[] entry is required.
docker_manifests[] is retained only for the niche case of stitching together
manifest lists from images that were not built by dockers_v2 in the same run.
Hooks
pre: runs after the staging context is prepared but before
docker buildx build; post: runs after the image digest is captured. Hook
commands, working directories, and env values are template-expanded; in
addition to the standard template surface, hooks see:
| Variable | Available in | Description |
|---|---|---|
{{ Images }} | pre + post | List of image:tag references for this build |
{{ Dockerfile }} | pre + post | Path to the rendered Dockerfile |
{{ ContextDir }} | pre + post | Path to the buildx context staging directory |
{{ Digest }} | post only | Image manifest digest |
{{ BaseImage }} / {{ BaseImageDigest }} | post only | Final-stage base image (mirrors GoReleaser's overlay) |
Dockerfile pattern (distroless + dist-tree binary)
The recommended pattern: a multi-stage-free Dockerfile that copies the
pre-built release binary from anodize's dist tree. The BIN build-arg names
the per-arch binary path.
# syntax=docker/dockerfile:1.7
FROM --platform=$TARGETPLATFORM gcr.io/distroless/cc-debian12:nonroot
ARG BIN=myapp
COPY ${BIN} /usr/local/bin/myapp
USER nonroot
ENTRYPOINT ["/usr/local/bin/myapp"]
When this image doubles as an MCP server (the dogfood case), set
ENTRYPOINT to the binary and CMD to the MCP subcommand:
ENTRYPOINT ["/usr/local/bin/anodizer"]
CMD ["mcp"]
Consumers then run docker run --rm -i ghcr.io/myorg/myapp:<ver> and the
container speaks MCP over stdio out of the box. See
MCP registry for the manifest that points at
this image.
Build flags
Pass additional flags to docker buildx build:
dockers_v2:
- dockerfile: Dockerfile
images: ["ghcr.io/myorg/myapp"]
tags: ["{{ Version }}"]
flags:
- --provenance=false
- "--label=org.opencontainers.image.version={{ Version }}"
Prefer the labels: / annotations: / build_args: maps over inline
--label= / --build-arg= in flags: — the maps are template-expanded per
entry and cleaner to read.
Full example
crates:
- name: myapp
dockers_v2:
- id: myapp
dockerfile: Dockerfile
images:
- ghcr.io/myorg/myapp
tags:
- "{{ Version }}"
- "v{{ Version }}"
- latest
platforms:
- linux/amd64
- linux/arm64
build_args:
BIN: myapp
VERSION: "{{ Version }}"
labels:
org.opencontainers.image.source: "https://github.com/myorg/myapp"
org.opencontainers.image.version: "{{ Version }}"
annotations:
org.opencontainers.image.licenses: "MIT"
extra_files:
- LICENSE
- README.md
sbom: true