task(docker): Implement immutable digest pinning and lifecycle management for java-base image
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
The dotcms/java-base image is currently tagged only by Java version (e.g., dotcms/java-base:21.0.8-ms). This tag is mutable — every rebuild overwrites it regardless of what changed. Any consumer referencing this tag silently pulls a different image after a rebuild, with no record in git of what changed or when.
This issue implements a complete lifecycle management strategy: content-addressed immutable tags, image manifest digest pinning in the runtime Dockerfile, a scheduled weekly rebuild for security freshness, a validation gate before any tag is published, and an automated PR flow to update the pinned reference.
This is a follow-on to #34781, which restructures the Dockerfiles. Digest pinning completes the reproducibility story.
The Problem
Mutable tags provide no reproducibility guarantee
Dated tags (21.0.8-ms-20260302) look immutable but are not — a retry or hotfix rebuild on the same day overwrites them. The same tag string can refer to different image content at different times.
The runtime Dockerfile currently takes SDKMAN_JAVA_VERSION and constructs FROM dotcms/java-base:${SDKMAN_JAVA_VERSION}. With no digest pin:
- Two builds from the same commit may use different base images
- A failing build cannot be reproduced if the base image has been rebuilt since
- There is no git-traceable record of which base image a given runtime build consumed
- A breaking upstream change (apt package, pgdg script, SDKMAN availability) silently becomes the floating tag and breaks all downstream builds
Upstream sources that can change without a Dockerfile edit
| Source | Risk |
|---|---|
FROM ubuntu:24.04 |
Canonical pushes new layer to the tag; content changes silently |
apt upgrade -y |
Picks up all package updates at build time; same Dockerfile can produce different images |
wget -O - https://get.sdkman.io | bash |
SDKMAN has retired older Java builds from its archive; script behaviour can change |
| pgdg setup script | Fetched from network; repo key or script could change |
Solution
1. Two-tier immutable tagging
Every successful java-base build pushes two immutable tags plus one floating alias:
| Tag | Type | Purpose |
|---|---|---|
21.0.8-ms-20260302 |
Immutable dated | Human-readable snapshot; the tag recorded in .approved-ref |
21.0.8-ms-{dockerfile-hash} |
Immutable content-addressed | Correlates tag back to exact Dockerfile; derived from sha256sum docker/java-base/Dockerfile (first 12 chars) |
21.0.8-ms |
Mutable floating | Convenience alias for local development only; never used in Dockerfiles or CI pins |
2. Image manifest digest pinning via .approved-ref
The truly immutable reference is the image manifest digest — a SHA256 of the actual image layers that cannot be overwritten regardless of what happens to the tag.
A file docker/java-base/.approved-ref is the single source of truth for which base image the runtime build uses. It stores the full reference combining the human-readable dated tag and the immutable digest:
dotcms/java-base:21.0.8-ms-20260302@sha256:74de5a500b17c8f2a1e3d4b9c7f8e2a1d3b5c9e7f2a4b6d8e1f3a5b7c9e2d4f6
The tag is context. The digest is the actual pin. If they ever diverge (someone pushed different content to the tag), Docker uses the digest and ignores the tag.
Runtime Dockerfile:
ARG BASE_IMAGE_REF=UNSET
FROM ${BASE_IMAGE_REF} AS container-base
CI pipeline reads the file and passes it as a build-arg:
- name: Read approved base image ref
run: echo "BASE_IMAGE_REF=$(cat docker/java-base/.approved-ref)" >> $GITHUB_ENV
- name: Build runtime image
uses: docker/build-push-action@v6
with:
build-args: BASE_IMAGE_REF=${{ env.BASE_IMAGE_REF }}
3. Validation gate before any tag is published
Every build — whether triggered by a Dockerfile change or a scheduled rebuild — runs smoke tests inside the image before any tags are pushed. If validation fails, no tags are updated and CI alerts immediately:
docker run --rm $IMAGE_ID java -version 2>&1 | grep -q "21\."
docker run --rm $IMAGE_ID /usr/bin/pg_dump --version
docker run --rm $IMAGE_ID dpkg -l libmimalloc2.0 libarchive-tools libtcnative-1
docker run --rm $IMAGE_ID env | grep -E "^JAVA_HOME="
This prevents upstream breakage (bad apt package, SDKMAN failure, pgdg script change) from becoming the floating tag and silently breaking all downstream builds.
4. Scheduled weekly rebuild for security freshness
A cron-triggered workflow rebuilds java-base weekly from the current Dockerfile. This picks up:
- apt security patches applied by
apt upgrade -y - Ubuntu 24.04 base layer updates pushed by Canonical
After a successful rebuild and validation:
- Push the dated tag (
21.0.8-ms-{yyyymmdd}) and update the floating tag - Capture the manifest list digest from build output (must use manifest list, not platform-specific digest, for multi-arch compatibility)
- Write the new
image:tag@sha256:digestreference to.approved-ref - Open an automated PR with this single-file change
The PR is the gate — auto-merge if CI passes, or require human review if preferred. If validation failed, no PR is opened and the previous approved ref remains in use.
5. Multi-arch digest
The workflow builds both linux/amd64 and linux/arm64. The .approved-ref file must store the manifest list digest (the multi-arch index), not a platform-specific digest. docker/build-push-action outputs this as steps.build.outputs.digest automatically.
Files
docker/java-base/.approved-ref(new file)docker/java-base/Dockerfile(add Dockerfile hash label to image metadata)dotCMS/src/main/docker/original/Dockerfile(replaceSDKMAN_JAVA_VERSIONARG withBASE_IMAGE_REFARG).github/workflows/cicd_manual_build-java-base.yml(add hash tag, validation gate, digest capture,.approved-refupdate).github/workflows/cicd_scheduled_java-base-rebuild.yml(new scheduled workflow)- Runtime image CI pipeline (read
.approved-refand pass as build-arg)
Acceptance Criteria
-
docker/java-base/.approved-refexists and contains aimage:tag@sha256:digestreference -
cicd_manual_build-java-base.ymlpushes both the dated immutable tag and the Dockerfile hash tag in a single run - Validation gate runs before any tags are pushed; build fails loudly if validation fails and no tags are updated
- The manifest list digest (not a platform-specific digest) is captured and written to
.approved-ref - A scheduled weekly rebuild workflow exists and produces a PR updating
.approved-refon success - Runtime Dockerfile uses
ARG BASE_IMAGE_REFandFROM ${BASE_IMAGE_REF} - CI pipeline reads
.approved-refand passes it as--build-arg BASE_IMAGE_REF - Two builds from the same git commit always use the same base image regardless of when they run
- A Dockerfile change that has not been built and pushed causes the runtime CI build to fail at
FROMwith a clear missing-digest error - Rebuilding the same dated tag does not affect builds already pinned via digest
Priority
Medium
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with docker/java-base/Dockerfile, .github/workflows/cicd_manual_build-java-base.yml, and dotCMS/src/main/docker/original/Dockerfile; then trace the runtime image CI pipeline that consumes the base image. Check how the existing workflow builds and tags images before adding the scheduled workflow and validation flow. Done means the acceptance criteria pass, including digest-pinned runtime builds and automated .approved-ref updates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, github-actions, java
- Domain
- build-system, ci-cd, devops, infrastructure
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100