Skip to content

Publish Docker images on merge to main, not just on tags - #27

Merged
rethab merged 3 commits into
mainfrom
feat/docker-publish
Jul 12, 2026
Merged

Publish Docker images on merge to main, not just on tags#27
rethab merged 3 commits into
mainfrom
feat/docker-publish

Conversation

@rethab

@rethab rethab commented Jul 12, 2026

Copy link
Copy Markdown
Owner

The fork inherited upstream's Docker workflow, which publishes to ghcr.io only when a release is cut. Between releases there was nothing to pull, even though every merge to main already produces a snapshot release and a Homebrew snapshot cask. Merges to main now publish an image too.

latest follows main, and releases publish version tags (1.2.3, 1.2). That is the opposite of the Docker default, where latest means "newest release", so it is called out in the README: pin a version tag if you want a stable image. Merges additionally get an immutable main-<sha> tag.

Building four platforms on every merge is only affordable if the build is fast, so the Dockerfile now cross-compiles with Go from the build platform instead of running anything under QEMU emulation. The runtime stage is kept free of RUN instructions to make that complete — its apk install of ca-certificates was redundant anyway, since Alpine ships the certificate bundle in the base image since 3.9 — so nothing ever executes on the target platform and the workflow needs no QEMU/binfmt setup at all. Cross-compiling meant swapping go install for go build, since go install refuses to cross-compile into GOBIN; the linker flags still come from the Makefile (new ldflags target), with the tag's v prefix stripped, so the version stamped into an image matches a release binary (1.2.3, not v1.2.3).

Smaller things that fall out of that:

  • linux/arm/v8 is dropped from the platform list. No client asks for it — ARMv8 hardware reports linux/arm64, which we already build — so it was a redundant entry in the manifest.
  • The runtime base moves from alpine:3.19 to 3.23, the release the builder already uses.
  • The image now carries a signed provenance attestation, matching what the release and snapshot workflows already do for their artifacts. There is deliberately no build cache: a cache that any branch can write to is a way into the images we publish, which is the same reasoning that already keeps the Go cache off the release builds.

rethab added 3 commits July 12, 2026 08:30
The image was only built on tag pushes, so there was nothing to pull
between releases. Merges to main now publish latest, and cross-compiling
replaces QEMU emulation so building four platforms per merge is viable.
A RUN in this stage executes on the target platform, which is exactly
the QEMU emulation the cross-compiling builder eliminated -- and the
workflow no longer registers binfmt handlers, so the arm stages would
have failed outright. The certificate bundle it installed ships with
the base image (ca-certificates-bundle) since Alpine 3.9. The base
also moves to the Alpine release the builder already uses.
GoReleaser stamps release binaries without the prefix and the image
tags are unprefixed semver, so a tag build would have reported v1.2.3
while everything around it says 1.2.3.
@rethab
rethab merged commit d58aa9c into main Jul 12, 2026
1 check passed
@rethab
rethab deleted the feat/docker-publish branch July 12, 2026 06:48
rethab added a commit that referenced this pull request Aug 5, 2026
The fork inherited upstream's Docker workflow, which publishes to
`ghcr.io` only when a release is cut. Between releases there was nothing
to pull, even though every merge to `main` already produces a snapshot
release and a Homebrew snapshot cask. Merges to `main` now publish an
image too.

`latest` follows `main`, and releases publish version tags (`1.2.3`,
`1.2`). That is the opposite of the Docker default, where `latest` means
"newest release", so it is called out in the README: pin a version tag
if you want a stable image. Merges additionally get an immutable
`main-<sha>` tag.

Building four platforms on every merge is only affordable if the build
is fast, so the Dockerfile now cross-compiles with Go from the build
platform instead of running anything under QEMU emulation. The runtime
stage is kept free of `RUN` instructions to make that complete — its
`apk` install of ca-certificates was redundant anyway, since Alpine
ships the certificate bundle in the base image since 3.9 — so nothing
ever executes on the target platform and the workflow needs no
QEMU/binfmt setup at all. Cross-compiling meant swapping `go install`
for `go build`, since `go install` refuses to cross-compile into
`GOBIN`; the linker flags still come from the Makefile (new `ldflags`
target), with the tag's `v` prefix stripped, so the version stamped into
an image matches a release binary (`1.2.3`, not `v1.2.3`).

Smaller things that fall out of that:

- `linux/arm/v8` is dropped from the platform list. No client asks for
it — ARMv8 hardware reports `linux/arm64`, which we already build — so
it was a redundant entry in the manifest.
- The runtime base moves from `alpine:3.19` to `3.23`, the release the
builder already uses.
- The image now carries a signed provenance attestation, matching what
the release and snapshot workflows already do for their artifacts. There
is deliberately no build cache: a cache that any branch can write to is
a way into the images we publish, which is the same reasoning that
already keeps the Go cache off the release builds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant