Skip to content

feat(build): publish multi-arch container image to GHCR on release - #631

Open
stevapple wants to merge 1 commit into
modelpack:mainfrom
stevapple:feat/publish-container-image
Open

feat(build): publish multi-arch container image to GHCR on release#631
stevapple wants to merge 1 commit into
modelpack:mainfrom
stevapple:feat/publish-container-image

Conversation

@stevapple

Copy link
Copy Markdown

Closes #630

Publishes modctl as a multi-arch container image so it can be used in CI/CD without installing the Go toolchain.

Changes

  • build/Dockerfile — cross-compiling builder (golang:1.25 on $BUILDPLATFORM) plus a gcr.io/distroless/static-debian13:nonroot runtime, matching the layout used by model-csi-driver. The version ldflags mirror the ones in the existing release build, so modctl version reports the same fields from the image as from the released tarballs.
  • .github/workflows/release.yaml — a publish-image job that pushes ghcr.io/modelpack/modctl:<tag> and :latest for linux/amd64 and linux/arm64 on every v* tag, using GITHUB_TOKEN with packages: write.

The job is independent of the existing binary matrix, and because the builder cross-compiles it needs no QEMU — the whole job takes about as long as a single go build.

Why CGO_ENABLED=0

The released Linux tarball binaries are built with CGO_ENABLED=1 and a statically linked glibc, which breaks NSS-backed os/user lookups. Dropping such a binary into a distroless base panics before any command runs:

$ docker run --rm modctl:repackaged version
panic: user: unknown userid 65532

goroutine 1 [running]:
github.com/modelpack/modctl/cmd.init.12()
	cmd/root.go:117 +0x444

config.NewRoot() calls user.Current() to derive the default storage and log directories, and panics if it fails. This is the same class of failure as #285 / #433 (getgrgid_r segfault in the static binaries).

Building with CGO_ENABLED=0 avoids it entirely: os/user uses the pure-Go implementation that reads /etc/passwd (where distroless does have an entry for uid 65532), and the build selects the pure-Go go-git backend via the existing //go:build !enable_libgit2 constraint in pkg/source/git_gogit.go.

Worth flagging for reviewers: this means the image binary uses go-git while the tarball binaries use libgit2. model-csi-driver already builds its release binaries with CGO_ENABLED=0, so there is precedent in the org, but if you would rather the image match the tarballs exactly, the alternative is to keep CGO and add the osusergo/netgo tags proposed in #433 — happy to switch.

Testing

Built locally with docker buildx for both platforms:

$ docker buildx build --platform linux/amd64,linux/arm64 \
    --build-arg GITVERSION=v0.0.0-poc --build-arg GITCOMMIT=bd2c9f7 \
    -f build/Dockerfile -t modctl:multi .

The resulting manifest list carries linux/amd64 and linux/arm64 (42–46 MB per architecture). Both run as the non-root user:

$ docker run --rm modctl:arm64 version
Version:    v0.0.0-poc
Commit:     bd2c9f7
Platform:   linux
BuildTime:  2026-08-12T14:16:45Z

End-to-end check against Qwen2.5-0.5B-Instruct mounted into the container:

$ docker run --rm -v ./qwen2.5-0.5b-instruct:/model:ro modctl:arm64 \
    modelfile generate /model --name qwen2.5-0.5b-instruct \
    --format safetensors --param-size 0.5b -O /tmp
Generating modelfile for /model
Successfully generated modelfile:
NAME qwen2.5-0.5b-instruct
ARCH transformer
FAMILY qwen2
FORMAT safetensors
PARAMSIZE 0.5b
PRECISION bfloat16
CONFIG config.json
...
MODEL model.safetensors
DOC LICENSE
DOC README.md

Notes

  • The image has no shell, so docker run ... modctl <args> works but sh -c style invocations do not. If you would prefer a shell for CI systems that inject commands: blocks, ubuntu:24.04 (as in model-csi-driver) is a drop-in change to the final stage.

Add build/Dockerfile and a publish-image job so each tag pushes
ghcr.io/modelpack/modctl:<tag> and :latest for linux/amd64 and
linux/arm64, letting users run modctl in CI/CD without installing
the Go toolchain.

The image is built with CGO_ENABLED=0, which selects the pure-Go
go-git backend (pkg/source/git_gogit.go) and pure-Go os/user. The
released tarball binaries link glibc statically with CGO enabled,
so their NSS-backed os/user lookups fail on a distroless base --
user.Current() in config.NewRoot() panics with "unknown userid
65532" before any command runs. This is the same class of failure
as modelpack#285.

The builder cross-compiles from $BUILDPLATFORM, so the job needs
no QEMU and runs independently of the existing binary matrix.

Closes modelpack#630

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: YR Chen <stevapple@icloud.com>
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.

Publish modctl as a container image

1 participant