Build & publish a monorepo of co-dependent, multiarchitecture container images.
Avoid rebuilding already-built images, automatically rebuild dependencies of descendent images if necessary, and handle multiarchitecture building woes. Let users easily shift between remote and local registries. Automatically cascade version increments to all descendent images when patching a root base image.
Assume you have a "project directory" that contains a contexts
subdirectory. That subdirectory, in turn, has a list of further
subdirectories, each representing a Docker build
context. Each contains a
Dockerfile and files that are used throughout the building of that
Dockerfile. They also contain an EDN file called info.edn:
{:version "1.2.3"}
Each context subdirectory additionally has a numeric prefix that determines
the ordering of its build; 00-debian-slim is guaranteed to be built before
its dependent image specified in 01-base-dev.
It ends up looking like this:
.
├── contexts
│ ├── 00-debian-slim
│ │ ├── Dockerfile
│ │ └── info.edn
│ ├── 01-jq
│ │ ├── Dockerfile
│ │ └── info.edn
│ ├── 01-base-dev
│ │ ├── Dockerfile
│ │ ├── info.edn
│ │ ├── README.md
│ ├── 01-pass-env
│ │ ├── Dockerfile
│ │ ├── info.edn
│ │ └── README.md
│ ├── 02-java-dev
│ │ ├── Dockerfile
│ │ └── info.edn
│ ├── 03-clojure-dev
│ │ ├── Dockerfile
│ │ └── info.edn
│ └── README.md
└── README.md
That ordering, combined with versioning metadata in info.edn, gives enough
information to fully manage multiarchitecture builds of codependent images
with versioning and automatic version-bumping on base image updates.
This tool does all of that.
Get the container-image-pipelines executable from the
releases.
Alternatively, get a platform-agnostic JAR if you already have a Java runtime
installed. You can also run it from source using clj -M -m louis-jackman.container-image-pipelines after installing
Clojure.
$ ./container-image-pipelines
Build & publish a monorepo of co-dependent, multiarchitecture container images.
Avoid rebuilding already-built images, automatically rebuild dependencies of
descendent images if necessary, and handle multiarchitecture building
woes. Let users easily shift between remote and local
registries. Automatically cascade version increments to all descendent
images when patching a root base image.
Usage:
container-image-pipelines help
container-image-pipelines build
container-image-pipelines publish-locally
container-image-pipelines publish
container-image-pipelines upload-all-to-local-registry
container-image-pipelines pull-latest-from-local-registry
container-image-pipelines pull-latest-from-remote-registry
container-image-pipelines cascade-version-updates
Pass `-h` or `--help` to one of those subcommands to discover
their options.
An error occured: invalid subcommand.
More context: valid subcommands: build, cascade-version-updates, help, publish, publish-locally, pull-latest-from-local-registry, pull-latest-from-remote-registry, upload-all-to-local-registry
To see the options available for a specific subcommand, pass -h or --help to it:
$ clj -M -m louis-jackman.container-image-pipelines pull-latest-from-remote-registry -h
container-image-pipelines pull-latest-from-remote-registry
--project-dir [.] — The project directory containing the `contexts` directory.
--remote-registry — The remote registry.
Several operations are available. They include pulling all prebuilt multiarchitecture images from a remote registry to your local registry, building them all from scratch locally with additional customisations, automatically cascading version bumps throughout derived images when a base image is updated, and more.
This repository is currently hosted on GitLab.com. An official mirror exists on GitHub. GitLab is still the official hub for contributions such as PRs and issues.
See my dockerfiles repository for an example project directory, against which this tool can be run.
When building images, version tags are used but latest is also set for
convenience.
The last path component of the image name is based off the directory structure
of the provided project directory; to illustrate, the built image of the
Dockerfile found within the contexts/04-go-dev subdirectory can be found at
$REGISTRY/go-dev. The 04- within that path is a numeric prefix to control
build ordering, which is explained later in this document.
To build all images locally for just the current architecture and load the
result into the local Docker store, run the build subcommand. Run the
publish subcommand to emulate the same building and pushing steps taken by
a CD pipeline, building for all supported platforms and publishing to a
remote registry.
Alternatively, the images can be built locally for just the current
architecture and pushed to a local registry. Run the publish-locally
subcommand to do so against a local registry at localhost:5000.
The project directory's images can be built in a specific order,
e.g. debian-slim before its dependent
images. src/louis_jackman/container_image_pipelines/image_building.clj
manages such dependencies while building, and that's encapsulated behind
the build and publish* subcommands. This is implemented using numeric
ordering prefixes on each context subdirectory within a project directory,
e.g. debian-slim being prefixed with 00- to represent it being in the
first batch of images to build, and its dependent image base-dev coming long
after it due to having a 03- prefix.
The publish-locally subcommand also solves the problem of BuildKit having
poor support for dependencies on images stored in the local Docker images
store by relying on a local
registry rather than the local store.
The default configuration will attempt builds on both AMD64 and ARM64, but
requires a modern BuildKit-supporting Docker installation and QEMU binaries
for both architectures. If that isn't available, use the
--only-local-platform argument, or override the default platforms with
--default-platforms.
To deploy to a local registry, first spin up a local Docker registry:
docker run -d -e REGISTRY_STORAGE_DELETE_ENABLED=true --restart=always --name registry -p 5000:5000 registry:2.8.3That sets up a registry that auto-restarts when it ends, and allows deletions
when necessary. It exposed the expected 5000 port. For manipulating the
registry locally, such as listing and deleting images, consider the reg
tool.
That command spins up an unauthenticated registry without TLS, with the ability to erase and replace images for anyone who can reach the port. That is only secure if the port is firewalled for just localhost or the local network has other mitigating controls.
Then set up a builder instance:
docker buildx create --platform=linux/arm64,linux/amd64 --driver-opt=network=host --useDoing it with these additional flags ensures it can cross-compile to both ARM64 and AMD64, while putting the builder on the host network. Without that networking change, it can't see the registry service that was just spun up. A more elegant solution would be to give them both the same dedicated Docker network.
Ensure the QEMU binaries for all desired platforms exist by invoking this one-shot container. This may not work on all architectures, meaning some will only be capable of doing "local" image builds rather than building for multiple architectures.
docker run --rm --privileged multiarch/qemu-user-static --reset -p yesFinally, build the images and push them to the local registry:
clj -M -m louis-jackman.container-image-pipelines publish-locallySee the publish-locally subcommand's --help for more details.
Potential future improvements are enumerated in the TODO document.