magus v0.3.0 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
6 min read

Run magus from a container image

Official images are published to two registries:

  • ghcr.io/egladman/magus on the GitHub Container Registry
  • docker.io/egladman/magus on Docker Hub

Both receive the same tags from the same build, so a given tag is the same image digest in either place. Pick whichever your environment already authenticates against; GHCR is the primary, and the rest of this page names it for brevity. Either way, the images are a drop-in alternative to installing the binary, which is convenient on CI runners and in throwaway environments.

They are ordinary OCI images with nothing Docker-specific about them. The examples below use docker because it is the most familiar, and every one of them works unchanged with podman, nerdctl, or any other OCI runtime. Browse the published tags and digests on the package page.

Each image runs magus as its entrypoint against /workspace, as a non-root user. Mount your repository there and pass a magus command:

docker run --rm -v "$PWD":/workspace ghcr.io/egladman/magus:latest ls
docker run --rm -v "$PWD":/workspace ghcr.io/egladman/magus:latest run ci

The default command is ls (list projects and targets), so a bare docker run --rm -v "$PWD":/workspace ghcr.io/egladman/magus:latest is a quick smoke test.

Variants

Image Base Platforms Notes
ghcr.io/egladman/magus:latest distroless/static linux/amd64, linux/arm64 Fully static, no libc. The default; use this unless you need something below.
ghcr.io/egladman/magus:latest-cgo distroless/cc (glibc) linux/amd64 glibc build that bundles inotify-tools, so magus watch / fs\watch work inside the container.

Use latest unless you know you need the other one. Two things differ beyond the base image, and both follow from the static image carrying no shared libraries at all:

  • magus watch / fs\watch need inotify-tools, which only the cgo image ships.
  • Buzz FFI (zdef()) is unavailable in every static build. That means this image and the unsuffixed release archives, so it applies equally to a binary you extract with docker cp below. FFI opens a shared library at runtime, which is what made those builds need a dynamic loader; they are compiled with -tags noffi, and zdef() reports FFI as unsupported rather than failing at the call. Keeping it would have cost the static property itself: the loader it pulls in is exactly what distroless/static, a scratch image, and a musl host do not provide. Use the cgo image, or a -cgo archive, if a magusfile calls zdef().

Tags

  • latest / latest-cgo follows the most recent release.
  • <version> / <version>-cgo pins one release, for example v0.3.0 or v0.3.0-cgo. Pin a version in CI so a run stays reproducible.
docker pull ghcr.io/egladman/magus:v0.3.0

Per-commit images

Every merge to main also publishes a static image tagged with that commit's short hash, so you can run an exact commit without waiting for a release:

docker run --rm -v "$PWD":/workspace ghcr.io/egladman/magus:a1b2c3d ls

These are not releases. They are GHCR-only (never Docker Hub), static-only (no -cgo variant), have no moving tag to follow, are not pruned on any schedule, and carry no compatibility promise. They are signed, but by cd.yaml rather than the release workflow, so the verify command below deliberately rejects them - see Verify the signature.

Two images, two channels, and one charm tells them apart: magus run image-build:cd publishes to the per-commit channel, magus run image-build:cd,stable to the stable one. Neither word is decoration - cd is what makes a build publish at all, and stable is which stream it lands in.

Build the image yourself

magus run image-build

A local build loads a single architecture into your docker image store, and says which one in the build log. That is a constraint rather than a preference: docker buildx build --load cannot load a multi-platform result into the image store, so a local build has exactly one architecture no matter what. It defaults to your host's, so an Apple Silicon machine builds and runs linux/arm64 natively.

Command Builds
magus run image-build cgo variant, host architecture
magus run image-build:static static variant, host architecture
magus run image-build:static,arm64 static variant, forced linux/arm64
magus run image-build:amd64 cgo variant, forced linux/amd64

The amd64 and arm64 charms exist for reproducing a failure that only happens on the other architecture. They go through QEMU emulation, so expect them to be slow - they are a debugging tool, not a second default.

Multi-architecture images are built only when publishing, where the result is pushed to a registry rather than loaded locally.

Install the binary without running a container

The static image is a single static binary on an empty base, so the image doubles as a distribution channel: copy the binary out and run it on the host, no container runtime involved at run time. This is handy on a machine that already has a runtime but no install script, and it is how several projects distribute cross-platform tools.

id=$(docker create ghcr.io/egladman/magus:v0.3.0)
docker cp "$id":/magus ./magus
docker rm "$id"
./magus version

podman works the same way with podman create / podman cp. Pin a version rather than latest, so what you extract is reproducible.

This only works with the static image. The binary in the -cgo image is linked against that image's glibc and will not run on an arbitrary host. What you extract is the same static build the unsuffixed release archives ship, so it has no Buzz FFI either (see Variants).

Verify the signature

Every pushed image digest is signed with cosign at release time, keyless through Sigstore OIDC, so there is no long-lived key to leak. To confirm an image came from this project's release workflow:

cosign verify ghcr.io/egladman/magus:latest \
  --certificate-identity-regexp '^https://github.com/egladman/magus/.github/workflows/release.yaml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Use cosign v3 or later. Signatures are written in the Sigstore bundle format that v3 made the default. A v3 client verifies both formats, but a v2 client cannot read a v3 signature and reports the image as unverified - which looks exactly like a bad signature. Check with cosign version before concluding anything from a failure.

A missing or mismatched signature means the image is not an official build. Do not run it.

A signature lives in the registry beside the image it covers, so it is pushed to each registry separately even though the digest is identical. Verify the reference you actually pulled - swap ghcr.io for docker.io above if that is where the image came from.

The identity regexp pins the release workflow specifically, which is what makes the command reject a per-commit image: those are signed too, but under cd.yaml, so they fail this check by design rather than by accident. To verify one on purpose, name that workflow instead:

cosign verify ghcr.io/egladman/magus:a1b2c3d \
  --certificate-identity-regexp '^https://github.com/egladman/magus/.github/workflows/cd.yaml@.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Read the SBOM

Every published image carries an SPDX software bill of materials and a max-detail SLSA provenance statement, both attached as in-toto attestations inside the image index. They are generated by BuildKit during the build itself, one per platform, so what they describe is the filesystem that actually shipped rather than a later re-scan of it.

Because they are part of the index, no extra registry lookup is needed:

docker buildx imagetools inspect ghcr.io/egladman/magus:latest --format '{{ json .SBOM }}'
docker buildx imagetools inspect ghcr.io/egladman/magus:latest --format '{{ json .Provenance }}'

On a multi-platform tag, pick a platform to avoid dumping every architecture at once:

docker buildx imagetools inspect ghcr.io/egladman/magus:latest \
  --format '{{ json (index .SBOM "linux/arm64").SPDX }}'

The SPDX document is ordinary JSON, so any SBOM tool reads it. To feed it to a vulnerability scanner:

docker buildx imagetools inspect ghcr.io/egladman/magus:latest \
  --format '{{ json (index .SBOM "linux/amd64").SPDX }}' > magus.spdx.json
grype sbom:magus.spdx.json

The static image is a single Go binary on an empty base, so expect its SBOM to be short: the Go module graph and essentially nothing else. The -cgo image also lists its distroless/cc glibc layer and inotify-tools.

Next steps

containercontainer imageimageocidockerpodmannerdctlghcrdocker hubregistrycosignsbomprovenanceinstall
Last updated (a103255f)
Earlier changes on this page (2)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. See the module reference.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Conventions

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.