A monorepo of purpose-built OCI/container images used across our CI/CD build pipelines and lab deployments. Each top-level directory is one independent image project that shares a common build convention.
app-container/
├── base_images.sh # Central pin of every base image (single source of truth)
├── include.mk # Shared make targets used by every project
├── Makefile # Repo-wide lint aggregator (shellcheck + hadolint)
├── <project>/ # One directory per image, e.g. activemq, net-snmp, frrouting
│ ├── Dockerfile.tpl # Template; `${VAR}` values are filled from version-lock.sh
│ ├── Makefile # One line: `include ../include.mk`
│ ├── version-lock.sh # Pinned versions/vars for this image (sourced at build)
│ ├── release.tag # Published image name + tag, e.g. `activemq:6.1.3.20240919`
│ └── build/ # Local OCI build artifacts (git-ignored)
└── .github/
├── workflows/build.yml # CI: lint, discover changed projects, build/publish
└── renovate.json # Automated base-image + GitHub Actions updates
The Dockerfile in each project is generated from Dockerfile.tpl by
envsubst and is git-ignored — never edit or commit it.
From inside a project directory:
cd net-snmp
make oci # generate Dockerfile, lint, build a local OCI image
make oci SINGLE_ARCH=linux/arm64
make clean # remove generated Dockerfile, artifacts, builder
make help # full target listmake oci writes the image archive to build/<project>_<arch>.oci.
Required tooling: docker (with buildx), envsubst (gettext), shellcheck,
hadolint.
Publishing is normally done by CI, but can be run manually:
make publish \
CONTAINER_REGISTRY=quay.io \
CONTAINER_REGISTRY_REPO=labmonkeys \
CONTAINER_REGISTRY_LOGIN=... \
CONTAINER_REGISTRY_PASS=...Published tags are treated as immutable. make publish refuses to overwrite
an existing tag; make publish-force overrides that (use with care — it can
break downstream CI/CD).
- Create a directory named after the image and add:
Makefilecontaining onlyinclude ../include.mkDockerfile.tplstarting withFROM "${BASE_IMAGE}"and using${VAR}placeholders for anything version-specificversion-lock.shthatsource ../base_images.sh, exports the VCS/date labels, setsBASE_IMAGE, and pins the image-specific versions (copy an existing project such asnet-snmp/as a starting point)release.tagwith thename:tagto publishbuild/.gitkeep
- That's it — CI discovers the new directory automatically (any directory with
a
Dockerfile.tpl); no workflow edit is needed.
Prefer pinning base images in base_images.sh rather than hard-coding a tag in
a Dockerfile.tpl, so Renovate can keep them current.
.github/workflows/build.yml runs on every push to main and on pull requests:
- lint —
make shellcheckandmake hadolintacross the whole repo. - discover — computes which project directories changed. A change to a
shared file (
base_images.sh,include.mk, rootMakefile, or the workflow) rebuilds every project. - build — a matrix over the changed projects. Pull requests run
make oci(build only); pushes tomainrunmake publish(multi-arch, per the project'sPLATFORMS) followed bymake sign, with a.b<run-number>suffix.
Registry credentials are provided as repository secrets: QUAY_USERNAME,
QUAY_PASSWORD, and QUAY_LABMONKEYS_ORG (the registry host quay.io is a
literal in the workflow). The build job requests id-token: write so cosign can
sign keylessly via GitHub OIDC.
Each project declares the platforms it supports via PLATFORMS in its
version-lock.sh (default linux/amd64):
export PLATFORMS="linux/amd64,linux/arm64"make publish builds and pushes exactly those platforms as a single
multi-architecture image index. Images that hardcode an amd64 artifact select
the right asset from the Docker build arg $TARGETARCH; only the variables
version-lock.sh exports are substituted at render time, so $TARGETARCH
survives into the generated Dockerfile.
Published images are signed with cosign using keyless signing (no stored key) — the signer identity is the GitHub Actions workflow. Verify a published image with:
cosign verify \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity-regexp='^https://github.com/labmonkeys-space/app-container/' \
quay.io/labmonkeys/<project>:<tag>A single signature over the image index covers all its architectures.
.github/renovate.json keeps dependencies current:
- Base images pinned in
base_images.sh(via a custom regex manager). - GitHub Actions, pinned to immutable SHAs with the semver retained in a comment.
MIT — see LICENSE.