Migrate a Shared CI Base Image

Migrate shared Docker base images with consumer tests, patching, and rollback.

Introduction

30 Second Summary

Teams often reuse the same foundation across many internal tools because consistency saves time. An unnoticed assumption in that foundation can turn a routine update into a chain of failures.

In this project, you will safely migrate two working container images from a legacy Docker base to a patched CI base. A compatibility test proves every consumer still works before you rehearse a versioned rollout with a preserved rollback target.

What You'll Build

You run one verification script to watch both rebuilt tools pass with non-root execution while earlier image versions stay ready for rollback.

By the end of this project, you'll have:

  • A working legacy baseline where two local tools print consumer-a-ok and consumer-b-ok before migration.
  • A controlled failure that reveals an undocumented writable-directory runtime contract in a downstream consumer.
  • A compatibility gate you can run after a shared-base update to confirm every rebuilt consumer passes on cloudeng-ci:1.0.1.
  • Secret Mission: Pin Alpine 3.24.1 by an immutable digest to prove both consumers still work with an exact upstream image.

Are there any prerequisites?

You need a Mac running a supported macOS release with at least 4 GB of RAM plus Visual Studio Code. Step 1 includes the complete Docker Desktop 4.89.0 setup for beginners.

Before We Start

A safe shared-image migration starts with a clear target and proof that every dependent consumer still behaves correctly. This commitment keeps the project focused on preserving observable output while replacing cloudeng-serverless:latest.

Install and Verify Docker Desktop

Reproducing image inheritance on your Mac needs a consistent local Linux container engine. Docker Desktop provides that environment for every build and runtime check in this project.

This step installs Docker Desktop 4.89.0 on a supported Mac. You will prove that the Docker client can reach the local Docker Engine before creating any images.

In this step, get ready to:
  • Confirm your Mac can run Docker Desktop.
  • Install Docker Desktop 4.89.0 with the recommended settings.
  • Verify the Docker client can communicate with the local Docker Engine.
Check your Mac and download the installer

Docker Desktop supports the current major macOS release plus the previous two major releases. It requires at least 4 GB of memory.

  • Open your Mac's system information from the Apple menu in the top-left corner.
  • Compare your macOS release with the supported-release policy in the Docker Desktop Mac installation guide.
  • Confirm the listed memory is at least 4 GB.
  • Identify the chip type as Apple silicon or Intel.
  • Open the official Docker Desktop release notes.
  • Download Docker Desktop 4.89.0 for Mac Apple chip or Mac Intel chip to match your Mac.

Mac below the requirements?

  • Update to a supported macOS release before continuing.
  • Use a supported Mac with at least 4 GB RAM if an update is unavailable.
  • Ask for help choosing a compatible path.

That compatibility check clears the first hurdle. You now have the installer that matches your Mac's processor.

Install and start Docker Desktop

A version check separates a current installation from an older installation or a missing command. Each result leads to a clear setup path.

  • Press Cmd+Space to open macOS search.
  • Type Terminal into the search bar.
  • Press Enter to open Terminal.
  • Check the current Docker installation by running this command:
docker version

What does this command check?

The command asks the Docker client for information about itself and the local engine.

  • The Client section identifies the installed command-line component.
  • The Server section confirms that the local Docker Engine is running and reachable.
  • An unavailable command indicates that Docker Desktop still needs to be installed.

Your result fits one of these setup states.

✔️ I see the required version

Your Docker Desktop installation already reports version 4.89.0 with both component sections. That means the required version is ready.

  • Keep Docker Desktop running for the rest of the project.

ⓧ I see an older version

You can continue confidently for this personal lab. Docker Desktop is free for personal use and education.

Commercial use at a larger enterprise may require a paid subscription.

  • Open the downloaded Docker.dmg file.
  • Drag Docker to Applications.
  • Approve replacing the older application if macOS asks.
  • Launch /Applications/Docker.app.
  • Accept the agreement.
  • Use the recommended settings.

ⓧ Command not found

You can continue confidently for this personal lab. Docker Desktop is free for personal use and education.

Commercial use at a larger enterprise may require a paid subscription.

  • Open the downloaded Docker.dmg file.
  • Drag Docker to Applications.
  • Launch /Applications/Docker.app.
  • Accept the agreement.
  • Use the recommended settings.

Before you check again, do you expect the terminal to reach both Docker components now?

  • Return to the Terminal window from earlier.
  • Check the completed installation by running the version command again:
docker version

What does the second check prove?

The repeated command tests the completed installation. Seeing both sections proves that the client can communicate with the running engine.

You should now see Client and Server sections. That result confirms Docker Desktop has started its local engine.

Still missing the Server section?

  • Keep Docker Desktop open while its local engine starts.
  • Rerun the version check after startup completes.
  • Help me diagnose the missing Server section.
Verify Docker from the project folder

The integrated terminal in Visual Studio Code runs commands from the folder currently open as your workspace. Verifying Docker there prepares the exact environment you will use for the image builds.

  • Create a new folder named cloudeng-image-migration on your Desktop with Finder.

You should see the empty cloudeng-image-migration folder on your Desktop.

  • Press Cmd+Space to open macOS search.
  • Type Visual Studio Code and press Enter to open it.

You should see the Visual Studio Code editor ready for a folder.

  • Open cloudeng-image-migration from your Desktop in Visual Studio Code.
  • Open Visual Studio Code's integrated terminal for the cloudeng-image-migration folder.

Before you run the final check, which two sections would prove the client and engine can communicate?

  • Verify Docker from the integrated terminal by running this command:
docker version

What does the final check prove?

This check runs through the same integrated terminal you will use for every project build. The Client and Server sections confirm that the command-line client reaches the local engine.

That's your local foundation in place. Your integrated terminal now shows both Client and Server sections.

Docker unavailable in Visual Studio Code?

  • Confirm Docker Desktop is still running on your Mac.
  • Close the current integrated terminal if it was open before installation.
  • Open a fresh integrated terminal in cloudeng-image-migration.
  • Help me connect Visual Studio Code to Docker.

Your Mac can now build and run local container images from the project folder. Next up, you will create the legacy shared base and its two working consumers.

Build the Legacy Base and Consumers

Your local Docker engine is ready. Your next goal is to create the shared image chain that the migration must protect.

A working legacy baseline gives you behavior to preserve. It also makes the effect of one shared base visible across two downstream consumers.

In this step, get ready to:
  • Build the legacy shared base from Alpine.
  • Build two consumers that inherit the shared base.
  • Run both consumers to confirm their observable behavior.
Create and build the legacy base

A build context supplies the files that Docker can use while building an image. This legacy context combines an Alpine base definition with a shared /reports directory.

  • Use the new folder control in the Visual Studio Code Explorer sidebar to create legacy-base inside cloudeng-image-migration.
  • Use the same control to create filesystem inside legacy-base.
  • Create reports inside legacy-base/filesystem.
  • Use the new file control to create README.txt inside legacy-base/filesystem/reports.
  • Enter Legacy writable reports directory. in README.txt.
  • Save README.txt.

The Explorer now shows legacy-base/filesystem/reports/README.txt. This file ensures the directory exists inside the finished image.

  • Use the new file control to create Dockerfile inside legacy-base.
  • Define the legacy image by copying this code into legacy-base/Dockerfile:
FROM alpine:3.23.5
LABEL legacy.image.role="serverless"
COPY filesystem/ /

What does this Dockerfile do?

  • The FROM instruction starts the image from alpine:3.23.5.
  • The LABEL instruction records the base image's project role.
  • The COPY instruction places the filesystem context at the image filesystem root.
  • Save legacy-base/Dockerfile.
  • Confirm the Explorer lists Dockerfile beside the filesystem folder.

Is the legacy context arranged differently?

  • Check that Dockerfile sits directly inside legacy-base.
  • Check that README.txt sits inside legacy-base/filesystem/reports.
  • Ask for help with the folder structure.
  • Build the legacy shared image by running this command in the integrated terminal:
docker build -t cloudeng-serverless:latest legacy-base

What does this build command do?

  • The -t option names the image cloudeng-serverless:latest.
  • The final legacy-base value selects the build context.
  • Docker copies the context's filesystem directory into the image.

You now have the shared legacy layer that both tools can inherit. The completed build output confirms the local image was tagged as cloudeng-serverless:latest.

Did the legacy build fail?

  • Confirm the integrated terminal is still inside cloudeng-image-migration.
  • Compare the spelling of legacy-base with the folder shown in the Explorer.
  • Get help with the failed legacy image build.
Create and build the consumers

Image inheritance lets each consumer start from the same shared base. Consumer A prints a status line when its container starts.

  • Use the new folder control in the Explorer to create consumer-a inside cloudeng-image-migration.
  • Use the new file control to create Dockerfile inside consumer-a.
  • Define consumer A's image by copying this code into consumer-a/Dockerfile:
ARG BASE_IMAGE=cloudeng-serverless:latest
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

How does consumer A inherit the base?

  • The global ARG sets cloudeng-serverless:latest as the default base image.
  • The FROM instruction creates the consumer from that base reference.
  • The ENTRYPOINT instruction runs the consumer script through /bin/sh when the container starts.
  • Save consumer-a/Dockerfile.
  • Confirm the Explorer lists Dockerfile inside consumer-a.

Does consumer A reference the wrong base?

  • Check that BASE_IMAGE defaults to cloudeng-serverless:latest.
  • Check that ARG appears before FROM.
  • Ask for help comparing the consumer Dockerfile.
  • Use the new file control to create tool.sh inside consumer-a.
  • Add consumer A's runtime behavior by copying this script into consumer-a/tool.sh:
#!/bin/sh
set -eu
printf '%s\n' 'consumer-a-ok'

What does consumer A do?

  • The first line selects /bin/sh as the script interpreter.
  • The set -eu line makes the script stop when a command fails or a required variable is missing.
  • The final line prints consumer-a-ok as the consumer's observable result.
  • Save consumer-a/tool.sh.
  • Confirm the Explorer shows both files inside consumer-a.

Does the consumer A script look incomplete?

  • Check that tool.sh contains three lines.
  • Check that the output text is exactly consumer-a-ok.
  • Ask for help checking consumer A's shell script.
  • Build consumer A from the shared base by running this command:
docker build -t cloudeng-tool-a:legacy consumer-a

What does this consumer build prove?

Docker resolves cloudeng-serverless:latest from the consumer's default build argument. The resulting cloudeng-tool-a:legacy image contains both the shared filesystem and consumer A's script.

Consumer A is now attached to the legacy dependency chain. Its completed build output confirms the local child image was created.

Did consumer A fail to build?

  • Confirm cloudeng-serverless:latest finished building before this consumer.
  • Confirm consumer-a contains both Dockerfile and tool.sh.
  • Get help with the consumer A build.
  • Run consumer A to check its first observable result:
docker run --rm cloudeng-tool-a:legacy

What should consumer A print?

The inherited entrypoint starts consumer-a/tool.sh through the Alpine shell. You should see consumer-a-ok in the terminal.

Consumer A now has a working legacy baseline. Its output becomes one compatibility target for the later migration.

Did consumer A produce no output?

  • Confirm the run command uses the image tag cloudeng-tool-a:legacy.
  • Compare the script path in ENTRYPOINT with the destination in COPY.
  • Ask for help tracing consumer A's entrypoint.

Consumer B has an extra filesystem behavior. It writes its result to /reports/result.txt before printing the same result to the terminal.

  • Use the new folder control in the Explorer to create consumer-b inside cloudeng-image-migration.
  • Use the new file control to create Dockerfile inside consumer-b.
  • Define consumer B's image by copying this code into consumer-b/Dockerfile:
ARG BASE_IMAGE=cloudeng-serverless:latest
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Why does consumer B use the same Dockerfile pattern?

Both consumers inherit the same shared base reference. Their separate scripts create different runtime behavior while the image relationship stays consistent.

  • Save consumer-b/Dockerfile.
  • Confirm the Explorer lists Dockerfile inside consumer-b.

Does consumer B use a different base reference?

  • Compare consumer-b/Dockerfile with consumer A's Dockerfile.
  • Confirm both files default to cloudeng-serverless:latest.
  • Ask for help checking the shared base reference.
  • Use the new file control to create tool.sh inside consumer-b.
  • Add consumer B's filesystem behavior by copying this script into consumer-b/tool.sh:
#!/bin/sh
set -eu
printf '%s\n' 'consumer-b-ok' > /reports/result.txt
printf '%s\n' 'consumer-b-ok'

What does consumer B depend on?

  • The first printf writes consumer-b-ok to /reports/result.txt.
  • The second printf sends the same value to the terminal.
  • The legacy base supplies the /reports directory through its build context.
  • Save consumer-b/tool.sh.
  • Confirm the Explorer shows both files inside consumer-b.

Is consumer B missing its file write?

  • Check that the first printf line ends with > /reports/result.txt.
  • Check that the second printf line has no file destination.
  • Ask for help comparing consumer B's script.
  • Build consumer B from the shared base by running this command:
docker build -t cloudeng-tool-b:legacy consumer-b

What joins consumer B to the chain?

The build resolves the same cloudeng-serverless:latest base used by consumer A. Docker then adds consumer B's script as the child image layer.

Consumer B is now part of the same legacy image family. Its completed build output confirms the second child image exists locally.

Did consumer B fail to build?

  • Confirm consumer-b contains both required files.
  • Confirm its Dockerfile uses cloudeng-serverless:latest as the default base.
  • Get help with the consumer B build.
  • Run consumer B to check its first observable result:
docker run --rm cloudeng-tool-b:legacy

What should consumer B print?

The script writes its result inside the container before printing it. You should see consumer-b-ok in the terminal.

Consumer B now works with the shared reports directory. You have captured a second compatibility target with extra filesystem behavior.

Did consumer B stop before printing?

  • Confirm legacy-base/filesystem/reports/README.txt exists in the Explorer.
  • Confirm legacy-base/Dockerfile copies filesystem/ to /.
  • Ask for help tracing the reports directory.
Verify the legacy dependency chain

The mutable tag cloudeng-serverless:latest names the shared base used during both consumer builds. Your final check confirms the behavior that future versions must preserve.

Before you run both images, do you expect each consumer to produce its own success line?

  • Run consumer A again to verify its observable contract:
docker run --rm cloudeng-tool-a:legacy

What does consumer A confirm?

You should see consumer-a-ok. This proves the first child image starts successfully from the legacy base.

  • Run consumer B again to verify its observable contract:
docker run --rm cloudeng-tool-b:legacy

What does consumer B confirm?

You should see consumer-b-ok. This proves the second child image can use the reports directory supplied by the legacy base.

Both consumers now pass from the same shared image. You have a working baseline with two distinct behaviors to protect.

  • Inspect the legacy base image configuration by running this command:
docker image inspect cloudeng-serverless:latest

What does the inspection connect?

  • The image configuration includes the legacy.image.role label with the value serverless.
  • The configuration contains no explicit non-root User value.
  • The Dockerfile's filesystem copy supplies the root-owned /reports directory used by consumer B.
  • Each consumer image preserves the base content that existed when that consumer was built.

Is the inspection output hard to follow?

  • Search the terminal output for legacy.image.role to locate the project label.
  • Search the configuration section for the default user field.
  • Ask for help reading the image inspection.

✔️ Awesome, I've got everything!

Your legacy base now supports two working consumer images. Double check that every source file is saved before moving on.

ⓧ I'd like to double check the full code

The final legacy-base/Dockerfile is:

FROM alpine:3.23.5
LABEL legacy.image.role="serverless"
COPY filesystem/ /

Legacy base definition

This file starts from Alpine. It labels the shared image and copies the filesystem contract into it.

The final legacy-base/filesystem/reports/README.txt is:

Legacy writable reports directory.

Legacy reports content

This file causes the build context to include the reports directory. The legacy runtime can write files there.

The final consumer-a/Dockerfile is:

ARG BASE_IMAGE=cloudeng-serverless:latest
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer A image definition

This file inherits the shared legacy image. It copies consumer A's script and runs it when the container starts.

The final consumer-a/tool.sh is:

#!/bin/sh
set -eu
printf '%s\n' 'consumer-a-ok'

Consumer A behavior

This script stops on shell failures. Its success output is consumer-a-ok.

The final consumer-b/Dockerfile is:

ARG BASE_IMAGE=cloudeng-serverless:latest
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer B image definition

This file uses the same inheritance pattern as consumer A. Its separate script supplies the different runtime behavior.

The final consumer-b/tool.sh is:

#!/bin/sh
set -eu
printf '%s\n' 'consumer-b-ok' > /reports/result.txt
printf '%s\n' 'consumer-b-ok'

Consumer B behavior

This script writes its result into the shared reports directory. It also prints consumer-b-ok for the compatibility check.

Your legacy ecosystem is ready. Next, you'll test whether a versioned non-root CI base preserves these consumer behaviors.

Expose the Hidden Runtime Contract

Your legacy base now supports two working consumer images. That baseline gives you behavior to protect during the migration.

The risk sits in assumptions inherited from the shared image. Each consumer must prove its own runtime requirements after the base changes.

In this step, get ready to:
  • Build a trial CI base with a non-root runtime identity.
  • Point each consumer at the trial base.
  • Compare both runtime results to uncover the shared filesystem contract.
Define the trial security boundary

A Dockerfile describes the base image plus its default runtime identity. This trial uses the numeric user and group 10001:10001 to model non-root execution.

  • In the Visual Studio Code Explorer sidebar, select the cloudeng-image-migration folder.
  • Click the new-folder icon in the Explorer sidebar.
  • Enter ci-base as the folder name.
  • Select the new ci-base folder.
  • Click the new-file icon in the Explorer sidebar.
  • Enter Dockerfile.trial as the filename.
  • Paste these contents into ci-base/Dockerfile.trial:
FROM alpine:3.24.1
ARG CI_IMAGE_VERSION=0.9.0
LABEL ci.image.version="${CI_IMAGE_VERSION}"
USER 10001:10001

What does this Dockerfile define?

  • The Alpine reference selects version 3.24.1 as the trial base.
  • The CI_IMAGE_VERSION argument supplies the trial version 0.9.0.
  • The ci.image.version label records that version in the image metadata.
  • The USER instruction sets the default runtime identity to 10001:10001.
  • Save ci-base/Dockerfile.trial.
  • Build the trial base image by running this command in the integrated terminal:
docker build -f ci-base/Dockerfile.trial -t cloudeng-ci:0.9.0 ci-base

What does this command do?

  • The -f option selects ci-base/Dockerfile.trial.
  • The -t option assigns the local tag cloudeng-ci:0.9.0.
  • The ci-base folder supplies the Docker build context.

The build should complete successfully with cloudeng-ci:0.9.0 available as the trial base image.

Did the trial base fail to build?

  • Confirm that the integrated terminal is still inside the cloudeng-image-migration folder.
  • Check that the filename is exactly ci-base/Dockerfile.trial.
  • Review the four Dockerfile lines for missing quotes or misspelled instructions.
  • Ask for help with the build output: Help me diagnose why my trial Docker image did not build.
Point both consumers at the trial base

A global ARG BASE_IMAGE lets each consumer choose its parent image before FROM ${BASE_IMAGE} starts the build stage. Changing that default moves both consumers onto the same migration candidate.

  • In the Explorer sidebar, select consumer-a/Dockerfile.
  • Find this current base-image default:
  • In the Explorer sidebar, select consumer-b/Dockerfile.
  • Confirm that the same current default appears there.
ARG BASE_IMAGE=cloudeng-serverless:latest

What does the current default mean?

Both consumers currently inherit from cloudeng-serverless:latest. That shared default produced the working legacy behavior you established earlier.

  • Replace the current line in consumer-a/Dockerfile with the trial default shown below.
  • Replace the matching line in consumer-b/Dockerfile with the same trial default.
ARG BASE_IMAGE=cloudeng-ci:0.9.0

What changes with this line?

Each consumer now builds from cloudeng-ci:0.9.0 by default. Their application files remain unchanged, which isolates the shared base as the migration variable.

  • Save consumer-a/Dockerfile.
  • Save consumer-b/Dockerfile.
  • Build both trial consumers by running these commands:
docker build -t cloudeng-tool-a:trial consumer-a
docker build -t cloudeng-tool-b:trial consumer-b

What do these builds produce?

The first build creates cloudeng-tool-a:trial from the updated consumer A context. The second build creates cloudeng-tool-b:trial from the updated consumer B context.

Both child images now inherit the default user 10001:10001 from the trial base. Their separate tags preserve the legacy images for comparison.

Both builds should complete successfully. You now have two trial images ready for runtime testing.

Did a consumer build fail?

  • Confirm that both consumer Dockerfiles use cloudeng-ci:0.9.0 as the default base.
  • Check that consumer-a/tool.sh still exists inside the consumer A context.
  • Check that consumer-b/tool.sh still exists inside the consumer B context.
  • Ask for help with the failing build: Help me debug my trial consumer build.
Run the compatibility test

A successful image build only proves that Docker assembled the layers. Runtime tests reveal whether each child image can still perform its observable job.

Before you run consumer A, do you expect its output to match the legacy version?

  • Run the trial consumer A image with automatic container cleanup by entering:
docker run --rm cloudeng-tool-a:trial

What does this command test?

The command starts a Container from cloudeng-tool-a:trial. The --rm option removes the stopped container after its entrypoint finishes.

Good start. Consumer A still prints consumer-a-ok under the non-root trial base.

Did consumer A fail?

  • Confirm that the image name is exactly cloudeng-tool-a:trial.
  • Check that consumer-a/tool.sh still prints consumer-a-ok.
  • Ask for help with the runtime output: Help me debug the consumer A trial failure.

The next run is the designed compatibility obstacle. Before you test consumer B, do you expect the same result as consumer A?

  • Run the trial consumer B image by entering:
docker run --rm cloudeng-tool-b:trial

What does this run exercise?

The container launches the unchanged consumer-b/tool.sh entrypoint under the inherited user 10001:10001. This is the first runtime test of consumer B against the trial base.

You will see the shell report that /reports/result.txt cannot be created. The container exits non-zero before it prints consumer-b-ok.

Which contract did the migration expose?

Consumer A only prints its output. Consumer B first writes to /reports/result.txt.

The legacy image supplied /reports while its process ran as root. The trial image runs as 10001:10001 without supplying that path.

The hidden runtime contract is a writable /reports directory. Consumer B proves that application output depends on more than the script alone.

The set -eu setting stops the script when the write fails. That non-zero exit turns an undocumented assumption into visible compatibility evidence.

Did consumer B pass unexpectedly?

  • Confirm that consumer-b/Dockerfile points to cloudeng-ci:0.9.0.
  • Confirm that consumer-b/tool.sh writes to /reports/result.txt before printing its output.
  • Rebuild the trial consumer B image using the earlier build command.
  • Ask for help comparing the unexpected result: Help me find why consumer B passed unexpectedly.

✔️ I have the trial state

Your trial base is built. Both consumer Dockerfiles point to cloudeng-ci:0.9.0.

Consumer A passes. Consumer B exposes the missing writable-directory contract.

  • Save all three changed Dockerfiles.

ⓧ I'd like to double check the full code

Compare each changed file with the complete trial state below.

FROM alpine:3.24.1
ARG CI_IMAGE_VERSION=0.9.0
LABEL ci.image.version="${CI_IMAGE_VERSION}"
USER 10001:10001

Trial base checkpoint

This file defines the 3.24.1 upstream image. It records candidate version 0.9.0 before setting the non-root runtime identity.

ARG BASE_IMAGE=cloudeng-ci:0.9.0
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer A checkpoint

Consumer A now inherits from cloudeng-ci:0.9.0. Its copied script plus entrypoint remain unchanged from the legacy version.

ARG BASE_IMAGE=cloudeng-ci:0.9.0
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer B checkpoint

Consumer B uses the same trial base reference. Its runtime failure comes from the unchanged script's filesystem dependency.

You have reproduced the migration failure without changing either tool script. Next, you will repair the shared base contract while keeping the non-root security boundary.

Repair the Base Contract

Your trial Docker migration exposed the hidden dependency behind consumer B. Its container needs a writable /reports directory while running as 10001:10001.

The shared CI base should define this runtime contract once for every consumer. In your Visual Studio Code workspace, you will add the directory with the correct ownership. You will keep the non-root security boundary intact.

In this step, get ready to:
  • Define the writable /reports contract in the shared CI base.
  • Rebuild both consumers from cloudeng-ci:1.0.0.
  • Create a compatibility gate that tests every consumer.
Define the writable base contract

The fixed base needs a real file under /reports in its build context. The COPY --chown=10001:10001 filesystem/ / instruction creates the path with ownership that matches the non-root user.

  • Use the file-creation control in the VS Code sidebar to create ci-base/filesystem/reports/README.txt.
  • Paste this contract description into README.txt:
Writable reports directory provided by the CI base contract.

Why include a file?

The file places the /reports directory inside the CI base's build context. Docker can now copy that directory into the image with the required ownership.

The text also records why the directory exists. Future maintainers can identify it as part of the consumer contract.

  • Save ci-base/filesystem/reports/README.txt.
  • Confirm the VS Code sidebar shows README.txt inside ci-base/filesystem/reports.

You now have a visible filesystem artifact for the writable directory contract.

  • Use the file-creation control in the VS Code sidebar to create ci-base/Dockerfile.
  • Paste the fixed CI base definition into ci-base/Dockerfile:
FROM alpine:3.24.1
ARG CI_IMAGE_VERSION=1.0.0
LABEL ci.image.version="${CI_IMAGE_VERSION}"
COPY --chown=10001:10001 filesystem/ /
USER 10001:10001

What does this base define?

  • The FROM instruction starts the fixed base from Alpine 3.24.1.
  • The CI_IMAGE_VERSION argument records release 1.0.0 in the image label.
  • The COPY --chown instruction gives UID and GID 10001:10001 ownership of the copied filesystem.
  • The final USER instruction preserves non-root execution for every inheriting consumer.
  • Save ci-base/Dockerfile.

Can't find the reports file?

Check that the path is exactly ci-base/filesystem/reports/README.txt. The filesystem directory must sit beside ci-base/Dockerfile.

Confirm that the COPY source is filesystem/. Docker resolves that source from the ci-base build context.

Help me check my CI base file paths.

Rebuild the shared image and consumers

Image inheritance copies the shared base configuration into each newly built consumer image. You need to rebuild the base first. Each consumer can then inherit the repaired directory contract.

  • Build the fixed CI base as cloudeng-ci:1.0.0 by running this command:
docker build --build-arg CI_IMAGE_VERSION=1.0.0 -t cloudeng-ci:1.0.0 ci-base

What does this build do?

The build passes 1.0.0 into CI_IMAGE_VERSION. It tags the resulting local image as cloudeng-ci:1.0.0.

The ci-base argument supplies the build context. This gives the Dockerfile access to the new filesystem/reports/README.txt file.

You should see the image build complete successfully. The local tag cloudeng-ci:1.0.0 now points to the repaired base.

Does the base build fail?

Confirm that your terminal is still using the cloudeng-image-migration folder. Docker needs to find the ci-base build context there.

Check that ci-base/Dockerfile is saved. Also confirm that ci-base/filesystem/reports/README.txt appears in the VS Code sidebar.

Help me diagnose the CI base build.

  • Locate ARG BASE_IMAGE=cloudeng-ci:0.9.0 at the top of consumer-a/Dockerfile.
  • Replace that line with the new default below:
ARG BASE_IMAGE=cloudeng-ci:1.0.0

What changes for consumer A?

The build argument now selects cloudeng-ci:1.0.0 as consumer A's default base. The remaining Dockerfile instructions stay unchanged.

  • Save consumer-a/Dockerfile.
  • Build consumer A from the fixed base by running this command:
docker build -t cloudeng-tool-a:ci-1.0.0 consumer-a

What does this build prove?

The command rebuilds consumer A with the updated base reference. The new cloudeng-tool-a:ci-1.0.0 tag remains separate from the earlier trial image.

You should see consumer A build successfully from cloudeng-ci:1.0.0.

  • Locate ARG BASE_IMAGE=cloudeng-ci:0.9.0 at the top of consumer-b/Dockerfile.
  • Replace that line with the new default below:
ARG BASE_IMAGE=cloudeng-ci:1.0.0

What changes for consumer B?

Consumer B now inherits the repaired /reports directory from cloudeng-ci:1.0.0. Its own tool.sh file still writes the same result to the same path.

  • Save consumer-b/Dockerfile.
  • Build consumer B from the fixed base by running this command:
docker build -t cloudeng-tool-b:ci-1.0.0 consumer-b

What does this build prove?

The command rebuilds consumer B without weakening its inherited user. The resulting cloudeng-tool-b:ci-1.0.0 image contains the repaired filesystem contract.

You should see consumer B build successfully. Both new consumer tags are now ready for the same compatibility test.

Does a consumer build fail?

Check that each consumer Dockerfile names cloudeng-ci:1.0.0 exactly. A different tag prevents the build from using the fixed local base.

Confirm that each consumer folder still contains its existing tool.sh file. The Dockerfiles copy that file into /opt/cloudeng/tool.sh.

Help me diagnose the failed consumer build.

Automate the compatibility gate

Testing one consumer by hand leaves the rest of the dependency graph unprotected. A compatibility gate runs every known consumer after a shared base change. It compares each tool's observable output with its expected contract.

  • Use the file-creation control in the VS Code sidebar to create verify-consumers.sh inside cloudeng-image-migration.
  • Paste this compatibility script into verify-consumers.sh:
#!/bin/sh
set -eu

consumer_a_image="${1:-cloudeng-tool-a:ci-1.0.0}"
consumer_b_image="${2:-cloudeng-tool-b:ci-1.0.0}"

check_output() {
  image="$1"
  expected="$2"
  actual="$(docker run --rm "$image")"
  printf '%s\n' "$image -> $actual"

  if [ "$actual" != "$expected" ]; then
    printf '%s\n' "Expected: $expected" >&2
    exit 1
  fi
}

check_output "$consumer_a_image" "consumer-a-ok"
check_output "$consumer_b_image" "consumer-b-ok"

printf '%s\n' "All consumer contract tests passed"

How does the gate work?

  • The consumer_a_image and consumer_b_image variables accept optional image names. Their defaults target the new 1.0.0 consumers.
  • The check_output function runs one image. It captures the container's output in actual.
  • The comparison exits with a failure when actual differs from expected.
  • The final message is reached only after both consumer contracts pass.
  • Save verify-consumers.sh.

Before you run the gate, do you expect both consumers to pass now that the base owns /reports?

  • Test both rebuilt consumers by running this command:
sh verify-consumers.sh cloudeng-tool-a:ci-1.0.0 cloudeng-tool-b:ci-1.0.0

What should I see?

You should see one successful output line for each consumer. The final line should read All consumer contract tests passed.

Consumer B's success proves that UID and GID 10001:10001 can write to the inherited /reports directory.

That's the contract repaired. Both consumers now preserve their observable behavior under the non-root CI base.

Does the compatibility gate stop early?

Read the last image name printed before the script stops. That identifies which consumer produced unexpected output.

Confirm that both 1.0.0 consumer images finished building. Also check that the script's expected values match consumer-a-ok and consumer-b-ok exactly.

Help me diagnose the failed compatibility gate.

Before you inspect consumer B, which inherited user do you expect its image configuration to contain?

  • Inspect consumer B's complete image configuration by running this command:
docker inspect --format='{{json .Config}}' cloudeng-tool-b:ci-1.0.0

What should I find?

You should find "User":"10001:10001" in the JSON configuration. This confirms that the repaired consumer still inherits the intended non-root user.

The passing runtime test proves that the writable contract works under that user. Together, these checks protect behavior and the security boundary.

✔️ Awesome, I've got everything!

  • Confirm your compatibility gate passes for both 1.0.0 consumers.
  • Confirm the inspected image configuration contains "User":"10001:10001".

ⓧ I'd like to double check the full code

  • Compare each file below with the matching file in your cloudeng-image-migration workspace.
FROM alpine:3.23.5
LABEL legacy.image.role="serverless"
COPY filesystem/ /

Legacy base reference

This file preserves the original shared base for comparison and rollback demonstrations.

Legacy writable reports directory.

Legacy filesystem reference

This file preserves the legacy /reports directory artifact.

FROM alpine:3.24.1
ARG CI_IMAGE_VERSION=0.9.0
LABEL ci.image.version="${CI_IMAGE_VERSION}"
USER 10001:10001

Trial base reference

This file preserves the failing trial base without the writable /reports contract.

FROM alpine:3.24.1
ARG CI_IMAGE_VERSION=1.0.0
LABEL ci.image.version="${CI_IMAGE_VERSION}"
COPY --chown=10001:10001 filesystem/ /
USER 10001:10001

Fixed base reference

This file adds the owned filesystem before setting the inherited runtime user.

Writable reports directory provided by the CI base contract.

Fixed filesystem reference

This file materializes the writable directory in the fixed base's build context.

ARG BASE_IMAGE=cloudeng-ci:1.0.0
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer A image reference

Consumer A inherits cloudeng-ci:1.0.0 while preserving its existing entrypoint.

#!/bin/sh
set -eu
printf '%s\n' 'consumer-a-ok'

Consumer A behavior reference

Consumer A preserves its observable consumer-a-ok output.

ARG BASE_IMAGE=cloudeng-ci:1.0.0
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer B image reference

Consumer B inherits the fixed base while preserving the same tool path and entrypoint.

#!/bin/sh
set -eu
printf '%s\n' 'consumer-b-ok' > /reports/result.txt
printf '%s\n' 'consumer-b-ok'

Consumer B behavior reference

Consumer B writes to /reports/result.txt before printing its observable success value.

#!/bin/sh
set -eu

consumer_a_image="${1:-cloudeng-tool-a:ci-1.0.0}"
consumer_b_image="${2:-cloudeng-tool-b:ci-1.0.0}"

check_output() {
  image="$1"
  expected="$2"
  actual="$(docker run --rm "$image")"
  printf '%s\n' "$image -> $actual"

  if [ "$actual" != "$expected" ]; then
    printf '%s\n' "Expected: $expected" >&2
    exit 1
  fi
}

check_output "$consumer_a_image" "consumer-a-ok"
check_output "$consumer_b_image" "consumer-b-ok"

printf '%s\n' "All consumer contract tests passed"

Compatibility gate reference

This script runs both default consumer images. It stops on a contract mismatch. It prints the passing message only when every check succeeds.

Your 1.0.0 images now have a tested filesystem contract and a preserved rollback trail. Next, you will rehearse a fresh patch release through the same consumer gate.

Release and Rehearse the Patch

Your fixed Docker base now protects the non-root boundary while preserving the writable /reports contract. Both consumers pass against the preserved 1.0.0 release.

Regular patching becomes safe when each fresh base build triggers consumer rebuilds. A compatibility gate provides evidence before promotion. Preserved image versions provide a rollback target.

In this step, get ready to:
  • Release a clean 1.0.1 build of the shared CI base.
  • Rebuild every consumer against the patched base.
  • Record the rollout evidence in a migration brief.
Release a fresh CI base

A patch release needs a distinct version so teams can promote it without replacing the known-good image. The clean build also checks the upstream Alpine reference again before Docker executes every build instruction.

  • In ci-base/Dockerfile, find ARG CI_IMAGE_VERSION=1.0.0.
  • Replace the existing build argument with this updated line:
ARG CI_IMAGE_VERSION=1.0.1

Why Bump the Build Argument?

The CI_IMAGE_VERSION argument supplies the value for the image label. The new label lets you inspect the built artifact and confirm that it represents release 1.0.1.

  • Save ci-base/Dockerfile.
  • Create the patched base image by running this command in the integrated terminal:
docker build --pull --no-cache --build-arg CI_IMAGE_VERSION=1.0.1 -t cloudeng-ci:1.0.1 ci-base

What Does This Build Do?

  • The --pull option checks for newer content behind the upstream base reference.
  • The --no-cache option re-executes every build step.
  • The --build-arg option records 1.0.1 in the project label.
  • The -t option preserves the result under the new cloudeng-ci:1.0.1 tag.

The build output should finish successfully with a tagged cloudeng-ci:1.0.1 image. Your cloudeng-ci:1.0.0 image remains available for rollback.

Before you inspect the image, which version do you expect its label to report?

  • Inspect the patched image label by running this command:
docker image inspect --format='{{json .Config.Labels}}' cloudeng-ci:1.0.1

What Does the Label Prove?

The inspection reads metadata from the built image. It proves that the release argument reached the final artifact.

You should find "ci.image.version":"1.0.1" in the output.

Did the Base Build Fail?

Confirm that Docker Desktop is still running. Check that the terminal remains inside the cloudeng-image-migration folder.

Check that ci-base/Dockerfile still copies the existing filesystem/ directory before setting the non-root user.

Use Help me debug the patched CI base build. You can also ask the NextWork community for environment-specific help.

Rebuild every consumer

An existing consumer image remains an immutable snapshot of its earlier base. Each consumer must reference the patched tag before a new build can carry the update forward.

  • In consumer-a/Dockerfile, find ARG BASE_IMAGE=cloudeng-ci:1.0.0.
  • Replace the existing base default with this updated line:
ARG BASE_IMAGE=cloudeng-ci:1.0.1

What Changes for Consumer A?

The global BASE_IMAGE argument now resolves the consumer's FROM instruction to cloudeng-ci:1.0.1.

  • In consumer-b/Dockerfile, find ARG BASE_IMAGE=cloudeng-ci:1.0.0.
  • Replace the existing base default with this updated line:
ARG BASE_IMAGE=cloudeng-ci:1.0.1

What Changes for Consumer B?

Consumer B now inherits the patched base. Its write to /reports/result.txt still depends on the shared writable-directory contract.

  • Save consumer-a/Dockerfile.
  • Save consumer-b/Dockerfile.
  • Build the two patched consumer images by running these commands:
docker build -t cloudeng-tool-a:ci-1.0.1 consumer-a
docker build -t cloudeng-tool-b:ci-1.0.1 consumer-b

Why Rebuild Both Consumers?

Each command creates a new consumer snapshot from cloudeng-ci:1.0.1. Testing every known consumer checks the shared runtime contract across the complete dependency graph.

You should see both builds finish successfully. The resulting tags are cloudeng-tool-a:ci-1.0.1 and cloudeng-tool-b:ci-1.0.1.

Did a Consumer Build Fail?

Check that both consumer folders still contain their original Dockerfile and tool.sh files. Confirm that the patched base build completed before rebuilding the consumers.

Use Help me debug a patched consumer image build. You can also share the build output with the NextWork community.

The compatibility script should use the current release when no image arguments are supplied. Updating its defaults turns the same test into the standard release gate.

  • In verify-consumers.sh, find the two default image declarations near the top.
  • Replace those declarations with these updated defaults:
consumer_a_image="${1:-cloudeng-tool-a:ci-1.0.1}"
consumer_b_image="${2:-cloudeng-tool-b:ci-1.0.1}"

How Do the Defaults Work?

Each shell expression uses a supplied argument when one exists. With no arguments, the script tests the two 1.0.1 images.

  • Save verify-consumers.sh.

Before you run the gate, do you expect both consumers to preserve their original output?

  • Test the patched consumer pair by running this command:
sh verify-consumers.sh

What Does the Gate Check?

The script runs each default image in a temporary container. It compares the observable output with the expected consumer contract.

You should see consumer-a-ok and consumer-b-ok. The final line should read All consumer contract tests passed.

Did the Compatibility Gate Stop Early?

Check that both patched image tags match the defaults in verify-consumers.sh. A tag mismatch prevents the script from starting the intended container.

If consumer B fails, confirm that ci-base/Dockerfile still contains the owned copy of filesystem/ before USER 10001:10001.

Use Help me diagnose a failing consumer compatibility gate. You can also ask the NextWork community to review the terminal output.

Record the rollout and rollback evidence

A successful local test proves the images work. A migration brief captures how the same evidence supports inventory, phased promotion, monitoring, automation, and rollback decisions.

  • In the Visual Studio Code Explorer sidebar, click the New File control.
  • Enter migration-notes.md as the file name.
  • Add the dependency evidence and contract evidence by pasting this first section:
# Shared Image Migration Evidence

## Dependency chain

Alpine upstream image -> CloudEng shared base -> consumer image -> deployment artifact

A base-image rebuild does not change an already-built consumer image. Each consumer must rebuild, pass its tests, and publish a new version before a deployment can use the updated base content.

## Failure found

The legacy base ran without an explicit non-root user and supplied a root-owned `/reports` directory. Consumer B depended on writing there. The trial CI base introduced `USER 10001:10001` but omitted the writable directory, so consumer B failed while consumer A passed.

## Contract fix

The CI base now creates `/reports` through its build context and assigns it to UID and GID `10001:10001`. Consumers inherit the non-root user and the writable directory contract.

What Does This Evidence Capture?

The dependency chain shows why rebuilding the shared base alone cannot update an existing consumer. The failure record turns the hidden writable-directory dependency into an explicit contract.

  • Save migration-notes.md.
  • Confirm that the editor shows the Dependency chain, Failure found, and Contract fix headings.

Is the Markdown Hard to Scan?

Check that each heading begins with the same number of # characters shown in the snippet. Keep one blank line between each heading and its body.

Use Help me check the structure of my migration notes. if the sections do not render clearly.

  • Add the compatibility gate and release process below the contract fix:
## Compatibility gate

The shared verification script runs every known consumer and compares its observable output with the expected contract. In a real organization, this gate belongs in CI and should run whenever the base image or a consumer reference changes.

## Rollout

1. Inventory all consumers and owners.
2. Build a candidate CI base with a new immutable release identifier.
3. Rebuild and test representative consumers first.
4. Migrate the remaining consumers in batches.
5. Monitor build failures, runtime errors, and deployment health.
6. Retire the legacy base only after the inventory shows no remaining consumers.

## Rollback

Preserve the previous consumer image versions. If the new release fails after deployment, redeploy the previous known-good consumer image while investigating. Do not repair a running container in place.

How Does the Release Process Reduce Risk?

The compatibility gate checks observable behavior before promotion. The phased rollout limits the affected consumer set while the previous image versions remain ready for rollback.

  • Save migration-notes.md.
  • Confirm that the rollout section contains six numbered stages.

Is a Rollout Stage Missing?

Compare the numbered list with the snippet above. The sequence starts with consumer inventory and ends with legacy-base retirement.

Use Help me review my phased image rollout checklist. if the progression is unclear.

  • Add the automation explanation and panel answer below the rollback section:
## How automation fits

GitHub Actions would build the shared base, rebuild consumers, execute compatibility checks, and publish approved images to a registry. Deployment systems would consume the newly published consumer image. Monitoring would decide whether rollout continues or rolls back. Scheduled dependency updates would propose new upstream tags or digests for review instead of silently changing production builds.

## Two-minute panel answer

I would start with an inventory of every consumer, owner, deployment target, and current base reference. I would define the behavior and filesystem contracts that must remain stable, then build a candidate CI image under a new version. I would rebuild representative consumers and run their normal tests plus image-level contract tests. I would migrate in batches, monitor failures and runtime health, and preserve the previous consumer images as rollback targets. Regular patching would create reviewed base-image updates, trigger the same consumer test matrix, and only promote the image if the evidence remained green.

How Would Automation Extend This Lab?

GitHub Actions would run the build and compatibility sequence whenever an approved update changes the base reference. An image registry would hold versioned release artifacts for deployment and rollback.

Scheduled update proposals keep upstream changes visible during review. The compatibility evidence decides whether each proposed release can move forward.

  • Save migration-notes.md.
  • Confirm that the final heading reads Two-minute panel answer.

Does the Panel Answer Feel Incomplete?

Check that it covers inventory, stable contracts, candidate builds, consumer testing, phased migration, monitoring, rollback, and regular updates. Each point comes directly from the evidence you produced.

Use Help me rehearse this shared-image migration answer. to practise responding to follow-up questions.

✔️ Awesome, I've got everything!

Your patched base, consumer defaults, verification defaults, and migration evidence are ready. Double check that every edited file is saved.

ⓧ I'd like to double check the full code

Compare the five edited files with these final versions.

FROM alpine:3.24.1
ARG CI_IMAGE_VERSION=1.0.1
LABEL ci.image.version="${CI_IMAGE_VERSION}"
COPY --chown=10001:10001 filesystem/ /
USER 10001:10001

CI Base Check

This complete file records release 1.0.1. It preserves the owned filesystem copy and non-root runtime user.

ARG BASE_IMAGE=cloudeng-ci:1.0.1
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer A Check

This complete file inherits cloudeng-ci:1.0.1 while preserving consumer A's original tool and entrypoint.

ARG BASE_IMAGE=cloudeng-ci:1.0.1
FROM ${BASE_IMAGE}
COPY tool.sh /opt/cloudeng/tool.sh
ENTRYPOINT ["/bin/sh", "/opt/cloudeng/tool.sh"]

Consumer B Check

This complete file inherits the same patched base. Consumer B keeps its existing runtime entrypoint.

#!/bin/sh
set -eu

consumer_a_image="${1:-cloudeng-tool-a:ci-1.0.1}"
consumer_b_image="${2:-cloudeng-tool-b:ci-1.0.1}"

check_output() {
  image="$1"
  expected="$2"
  actual="$(docker run --rm "$image")"
  printf '%s\n' "$image -> $actual"

  if [ "$actual" != "$expected" ]; then
    printf '%s\n' "Expected: $expected" >&2
    exit 1
  fi
}

check_output "$consumer_a_image" "consumer-a-ok"
check_output "$consumer_b_image" "consumer-b-ok"

printf '%s\n' "All consumer contract tests passed"

Compatibility Script Check

This complete script defaults to the two 1.0.1 consumers. It still accepts explicit image names for future releases or rollback checks.

# Shared Image Migration Evidence

## Dependency chain

Alpine upstream image -> CloudEng shared base -> consumer image -> deployment artifact

A base-image rebuild does not change an already-built consumer image. Each consumer must rebuild, pass its tests, and publish a new version before a deployment can use the updated base content.

## Failure found

The legacy base ran without an explicit non-root user and supplied a root-owned `/reports` directory. Consumer B depended on writing there. The trial CI base introduced `USER 10001:10001` but omitted the writable directory, so consumer B failed while consumer A passed.

## Contract fix

The CI base now creates `/reports` through its build context and assigns it to UID and GID `10001:10001`. Consumers inherit the non-root user and the writable directory contract.

## Compatibility gate

The shared verification script runs every known consumer and compares its observable output with the expected contract. In a real organization, this gate belongs in CI and should run whenever the base image or a consumer reference changes.

## Rollout

1. Inventory all consumers and owners.
2. Build a candidate CI base with a new immutable release identifier.
3. Rebuild and test representative consumers first.
4. Migrate the remaining consumers in batches.
5. Monitor build failures, runtime errors, and deployment health.
6. Retire the legacy base only after the inventory shows no remaining consumers.

## Rollback

Preserve the previous consumer image versions. If the new release fails after deployment, redeploy the previous known-good consumer image while investigating. Do not repair a running container in place.

## How automation fits

GitHub Actions would build the shared base, rebuild consumers, execute compatibility checks, and publish approved images to a registry. Deployment systems would consume the newly published consumer image. Monitoring would decide whether rollout continues or rolls back. Scheduled dependency updates would propose new upstream tags or digests for review instead of silently changing production builds.

## Two-minute panel answer

I would start with an inventory of every consumer, owner, deployment target, and current base reference. I would define the behavior and filesystem contracts that must remain stable, then build a candidate CI image under a new version. I would rebuild representative consumers and run their normal tests plus image-level contract tests. I would migrate in batches, monitor failures and runtime health, and preserve the previous consumer images as rollback targets. Regular patching would create reviewed base-image updates, trigger the same consumer test matrix, and only promote the image if the evidence remained green.

Migration Evidence Check

This complete brief connects the technical failure with compatibility testing, phased rollout, automation, and rollback. It also gives you a concise panel answer based on the work you completed.

Before you perform the final checks, do you expect the patched consumers and the preserved rollback images to exist side by side?

  • Run the final compatibility gate with the default patched image names:
sh verify-consumers.sh

What Should the Final Gate Confirm?

The script should print successful output for both patched consumers. Its final line should be All consumer contract tests passed.

Both consumer contract tests now pass on 1.0.1.

  • Inspect the final base-image label by running this command:
docker image inspect --format='{{json .Config.Labels}}' cloudeng-ci:1.0.1

What Should the Final Label Confirm?

The labels object should contain "ci.image.version":"1.0.1". This ties the local image tag to the release metadata inside the artifact.

The patched base identifies itself as release 1.0.1.

  • List the local image tags to confirm that the rollback versions remain available:
docker image ls

What Does the Image List Prove?

The local image list should include both consumer versions. Keeping 1.0.0 beside 1.0.1 gives you an immediate known-good rollback target.

You should find cloudeng-ci:1.0.0, cloudeng-tool-a:ci-1.0.0, and cloudeng-tool-b:ci-1.0.0 alongside their 1.0.1 replacements.

Is a Final Check Missing Evidence?

If the gate names 1.0.0, save the updated defaults in verify-consumers.sh before rerunning it. If the label still reports the older version, rebuild cloudeng-ci:1.0.1 from the saved Dockerfile.

Use Help me reconcile my patch release evidence. if the tag, label, and script defaults disagree.

That is the release rehearsal complete. Your patched consumers pass their shared compatibility gate while the previous known-good images remain ready for rollback.

Secret mission

Pin the CI Base by Digest

Tags can move after you test them. Pin the Alpine upstream image by digest, release the CI base as version 1.0.2, rebuild every consumer, and prove the complete dependency chain still works.

Clean Up Your Resources

Clean Up Your Resources

Your lab created project files and local Docker images, so leaving them on your Mac only uses disk space. Personal learning use costs $0.

Choose the cleanup option that matches what you want to do next.

Cost warning

Personal learning is free under Docker's terms. Professional use at a larger enterprise may require an employer-provided Docker Desktop subscription.

Resources you used:

  • Project files inside the cloudeng-image-migration folder.
  • Legacy images tagged cloudeng-serverless:latest, cloudeng-tool-a:legacy, and cloudeng-tool-b:legacy.
  • Trial images tagged cloudeng-ci:0.9.0, cloudeng-tool-a:trial, and cloudeng-tool-b:trial.
  • Rollback images tagged cloudeng-ci:1.0.0, cloudeng-tool-a:ci-1.0.0, and cloudeng-tool-b:ci-1.0.0.
  • Patched images tagged cloudeng-ci:1.0.1, cloudeng-tool-a:ci-1.0.1, and cloudeng-tool-b:ci-1.0.1.
  • Digest-pinned images tagged cloudeng-ci:1.0.2, cloudeng-tool-a:ci-1.0.2, and cloudeng-tool-b:ci-1.0.2.

Keep everything running

No action is needed. This option preserves the complete demonstration for another migration rehearsal.

  • Your legacy images remain available to demonstrate the original dependency chain.
  • Your 1.0.0 consumer images remain available as rollback targets.
  • Your digest-pinned 1.0.2 images remain ready for compatibility testing.
  • Your migration-notes.md file remains available for rehearsing the migration explanation.

Pause - I'll come back to this later

This option stops Docker Desktop to free memory while keeping every project file and local image.

  • Switch back to Docker Desktop.
  • Choose Quit Docker Desktop from the application menu.

Docker's local engine stops. Your project images remain available for the next time you start Docker Desktop.

Delete - I don't want to use this again

This option removes every project image and the complete cloudeng-image-migration folder.

  • Return to the Visual Studio Code integrated terminal from earlier.
  • Remove every project image by running:
docker image rm cloudeng-tool-a:legacy cloudeng-tool-b:legacy cloudeng-tool-a:trial cloudeng-tool-b:trial cloudeng-tool-a:ci-1.0.0 cloudeng-tool-b:ci-1.0.0 cloudeng-tool-a:ci-1.0.1 cloudeng-tool-b:ci-1.0.1 cloudeng-serverless:latest cloudeng-ci:0.9.0 cloudeng-ci:1.0.0 cloudeng-ci:1.0.1
docker image rm cloudeng-tool-a:ci-1.0.2 cloudeng-tool-b:ci-1.0.2 cloudeng-ci:1.0.2

What does this cleanup do?

These commands remove every image tag created during the project. Unrelated images on your Mac remain untouched.

Images still in use?

A container may still reference one of the listed images.

  • Remove the referencing container in Docker Desktop.

Ask for help identifying the container that blocks image cleanup.

  • Confirm that the project image tags are gone by running:
docker image ls

What should I see?

You should no longer see tags beginning with cloudeng- in the local image list. This confirms that the project images are removed.

Deleting the project folder is permanent. The next command targets only the cloudeng-image-migration folder beside your terminal's current location.

  • Press Cmd+Space to open macOS search.
  • Type Finder and press Enter to open it.
  • Open the folder that contains cloudeng-image-migration.
  • Switch back to the Visual Studio Code integrated terminal.
  • Delete the project folder by running:
rm -rf ../cloudeng-image-migration

What does this cleanup do?

The command permanently removes the cloudeng-image-migration folder from its parent directory. This includes every Dockerfile, script, report, and migration note from the lab.

Folder still present?

The command expects the integrated terminal to be inside cloudeng-image-migration when it runs.

Ask for help checking the terminal location before retrying the folder cleanup.

  • Switch back to Finder.

You should no longer see the cloudeng-image-migration folder in its former parent folder.

Nice Work!

Nice Work!

You completed a safe shared-image migration across a local Docker ecosystem. Both consumer tools now preserve their behavior while inheriting a patched non-root CI base.

You've learned how to:

  • Build a legacy shared image with two dependent consumers. Reproduce how a base-image migration can break a downstream tool through a hidden writable-directory contract.
  • Repair the non-root runtime contract in the shared CI base. Prove every known consumer still behaves correctly with one repeatable compatibility gate.
  • Rehearse a patch release with a phased rollout. Preserve known-good consumer images as rollback targets.
  • Secret Mission: Pin the upstream Alpine image to an immutable digest. Rebuild both consumers to revalidate the complete dependency chain.

Ready to quiz yourself?