Skip to main content

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 buildx required: multi-architecture builds require Docker Buildx. Ensure the buildx plugin is installed and a builder with multi-arch support is configured. In GHA, use docker/setup-qemu-action@v3 + docker/setup-buildx-action@v3.
  • No legacy dockers:: the top-level GoReleaser V1 dockers: block is rejected at config-load time with a clear migration error. Port to dockers_v2: (this page).
  • skip: true (or disable: true via 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_args in image history by default. Prefer {{ Env.VAR }} over raw user-config strings for secrets.
  • use: podman is Linux-only: configs setting use: podman on 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

FieldTypeDefaultDescription
idstringUnique handle for this entry (for --id filters)
idslistBuild-ID filter: only include artifacts whose id is in this list
dockerfilestringPath to Dockerfile (required)
imageslistghcr.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.
tagslistTag suffixes — one full image ref per (image × tag)
labelsmapnoneOCI labels via --label key=value. Merged over the auto-injected org.opencontainers.image.* set; user keys win.
oci_labelsbool/templatetrueAuto-inject the predefined org.opencontainers.image.* labels. Set false to opt out. See Auto-injected OCI labels.
annotationsmapnoneOCI annotations via --annotation key=value
extra_fileslistnoneExtra files copied into the build context
platformslisthostTarget platforms (linux/amd64, linux/arm64, ...)
build_argsmapnone--build-arg KEY=VALUE pairs
retryobjecttop-level retry:Per-pipe retry config (deprecated; prefer top-level)
flagslistnoneArbitrary extra docker buildx build flags
skipbool/templatefalseSkip the build. Accepts deprecation-warned disable: alias
sbombool/templatefalseAdd --sbom=true to buildx
hooksobjectnonepre: / post: hooks; see Hooks below
usestringbuildxBackend: 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:

LabelSource
org.opencontainers.image.createdRFC-3339 date from the resolved SOURCE_DATE_EPOCHnever wall-clock, so the image stays byte-reproducible
org.opencontainers.image.sourcerelease repo URL (SSH remotes normalized to their https:// web base)
org.opencontainers.image.revisionreleased commit SHA
org.opencontainers.image.versionreleased version (matches the image tag)
org.opencontainers.image.titlethe crate's name
org.opencontainers.image.descriptionmeta.description (Cargo-derived)
org.opencontainers.image.licensesmeta.license
org.opencontainers.image.urlmeta.homepage
org.opencontainers.image.documentationmetadata.documentation
org.opencontainers.image.vendorthe 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:

VariableAvailable inDescription
{{ Images }}pre + postList of image:tag references for this build
{{ Dockerfile }}pre + postPath to the rendered Dockerfile
{{ ContextDir }}pre + postPath to the buildx context staging directory
{{ Digest }}post onlyImage manifest digest
{{ BaseImage }} / {{ BaseImageDigest }}post onlyFinal-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