Docker Images: Pull, Inspect, Build
Pull, inspect, and build Docker images, then run versioned NGINX dashboards.
Introduction
30 Second Summary
You will turn a packaged website into a page that runs in your browser. Along the way, you will uncover what happens between downloading it and seeing it work.
In this project, you will trace a Docker image from an online registry to a running container. You will build a versioned NGINX dashboard that opens in your browser.
What You'll Build
You will open two independently running dashboard versions side by side to show how a downloaded image becomes a versioned website.
By the end of this project, you'll have:
- Local image evidence that reveals tags, digests, metadata, and layer history.
- A browser-visible dashboard served from your own versioned image.
- An immutability demo showing why source edits require a fresh image build.
- Secret Mission: an optional challenge to push your skills further.
Are there any prerequisites?
You need a supported 64-bit computer with hardware virtualization enabled and internet access. Docker Desktop is free for personal use, education, non-commercial open source, and qualifying small businesses.
Before We Start
Before you begin, commit to building a Docker Image Expedition dashboard. Clarifying why you want to trace an artifact from a registry image through local layers to a running container gives every later command a purpose.
Set Up Your Docker Workstation
Your Docker Image Expedition needs a working connection between the Docker client on your computer and the engine that manages images and containers.
In this step, you will install the correct runtime for your operating system. You will prove the complete client-server path before creating an empty workspace for the expedition.
In this step, get ready to:
- Install and start the Docker runtime for your operating system.
- Verify that the Docker CLI can reach the engine.
- Create the empty docker-image-expedition workspace on your Desktop.
Install Docker for your operating system
Docker Desktop bundles the client and engine for Windows or macOS. Docker Engine provides the runtime directly on supported Linux distributions.
Choose the path that matches your current installation state. The project uses Docker Desktop 4.87.0 or Docker Engine 29.7.2.
✔️ I have Docker Desktop 4.87.0 or Docker Engine 29.7.2
Your Docker runtime is ready for the connection check. Keep Docker Desktop running on Windows or macOS.
On Linux, keep the Docker service running. The next substep confirms that the client can reach it.
ⓧ I have an older version
Update your existing installation before continuing. This keeps the project aligned with Docker CLI and Docker Buildx behavior used throughout the expedition.
macOS
- Download the current Docker.dmg from the Docker macOS installation page.
- Move Docker to Applications when the installer opens.
- Select Use recommended settings (Requires password) during setup.
- Select Finish after the update completes.
Windows
- Download Docker Desktop Installer.exe from the Docker Windows installation page.
- Run the installer using the recommended per-user installation.
- Select Use WSL 2 instead of Hyper-V if the installer displays that option.
- Start Docker Desktop after the update completes.
Linux
- Use Docker's distribution installation guides to update the repository configuration for your Linux distribution.
- Install the current packages listed by the guide for your distribution.
- Restart the Docker service after the package update.
ⓧ Docker is not installed
Your operating system determines which runtime package you need. Follow one complete platform path below.
macOS
Docker Desktop requires a supported 64-bit Mac with at least 4 GB of RAM. Download the package that matches Apple silicon or Intel hardware.
- Download Docker.dmg from the Docker macOS installation page.
- Open the downloaded package.
- Drag Docker into Applications.
- Press Cmd+Space to open macOS search.
- Type Docker in the search bar.
- Press Enter to start Docker.
- Accept the subscription agreement.
- Select Use recommended settings (Requires password).
- Select Finish.
Docker does not start on macOS?
Confirm that you downloaded the package for your Mac processor. Help me diagnose why Docker Desktop will not start on my Mac.
Windows
The documented Windows path uses WSL 2.1.5 or later. It also requires 8 GB of system RAM.
- Press the Windows key to open search.
- Type PowerShell in the search bar.
- Start PowerShell with administrator access.
- Check the installed WSL version by running this command:
wsl --version
What does this command do?
This displays the installed WSL components and their versions. Version details confirm that WSL is already present.
- Install WSL by running this command if version details did not appear:
wsl --install
What does this command do?
This installs the Windows features needed for the WSL path. Windows may request a restart before setup is complete.
- Update WSL by running this command if version details appeared:
wsl --update
What does this command do?
This updates the existing WSL installation. The updated backend can then support Docker Desktop's Linux containers.
- Restart Windows if requested.
- Download Docker Desktop Installer.exe from the Docker Windows installation page.
- Run the recommended per-user installation.
- Select Use WSL 2 instead of Hyper-V if prompted.
- Start Docker Desktop through Windows search.
- Accept the subscription agreement.
- Keep Docker in Linux containers mode.
Docker is waiting for its engine?
Allow Docker Desktop to finish starting before testing the connection. Help me diagnose why the Docker Desktop engine is unavailable on Windows.
Linux
Docker Engine installs from Docker's repository for your exact distribution. Repository bootstrap commands vary between distributions, so use the live guide before running the final package command.
- Complete the repository setup from Docker's Linux installation guide for your distribution.
- Install Docker on Ubuntu or Debian by running:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
What does this command install?
This installs Docker Engine plus the Docker CLI and Buildx plugin from the configured repository.
Package installation failed?
Confirm that Docker's stable repository is configured for your exact distribution. Help me fix my Docker package installation on Ubuntu or Debian.
- Install Docker on Fedora or CentOS by running:
sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
What does this command install?
This installs Docker Engine plus the Docker CLI and Buildx plugin from the configured RPM repository.
Packages are unavailable?
Confirm that Docker's documented repository was added before running the package command. Help me fix my Docker package installation on Fedora or CentOS.
- Start Docker at boot on Fedora or CentOS by running:
sudo systemctl enable --now docker
What does this command do?
This starts Docker immediately. It also enables the service for future system starts.
Docker service did not start?
Check that package installation completed before starting the service. Help me diagnose why the Docker service will not start on Linux.
Why Docker Desktop on Windows and macOS?
Docker Desktop coordinates the client and engine for these operating systems. This gives the project one consistent Docker CLI workflow across platforms.
Linux uses Docker Engine directly from Docker's repository. This avoids an additional desktop virtual machine.
Verify the client-server connection
The Docker CLI can be installed while the engine remains unavailable. A complete setup must display information from both sides of the connection.
- Press Cmd+Space on macOS or the Windows key on Windows to open search.
- Type Visual Studio Code in the search bar.
- Press Enter to start Visual Studio Code.
- Open a fresh Visual Studio Code terminal.
Before you run the check, which two sections should appear if the client can reach the engine?
- Check the complete Docker connection by running:
docker version
What does this command prove?
- The Client section confirms that the Docker CLI is available.
- The Server section confirms that the CLI reached the Docker Engine.
You should see both Client and Server sections. Do not continue with only the client section.
Missing the Server section?
On Windows or macOS, confirm that Docker Desktop has finished starting. On Linux, start the service before retrying.
Help me restore the connection between my Docker CLI and Docker Engine.
- Start Docker manually on Linux by running this command if the service is stopped:
sudo systemctl start docker
What does this command do?
This starts the Docker service for the current Linux session. The CLI can connect after the engine becomes ready.
Before the final engine check, what system-wide details do you expect Docker to report?
- Display system-wide engine information by running:
docker info
What does this command prove?
This reports system-wide information from the running Docker installation. Successful output proves that the engine can answer detailed client requests.
You should see engine details covering containers, images, storage, and the active runtime. Your Docker workstation now has a working client-server path.
Cannot display engine information?
Wait for Docker Desktop to finish starting or confirm that the Linux service is active. Help me troubleshoot why docker info cannot reach the engine.
Create the expedition workspace
The project begins with an empty directory so the first artifact comes from Docker. Keeping it on your Desktop makes the workspace easy to find.
- Move the Visual Studio Code terminal to your Desktop by running this command:
cd ~/Desktop
What does this command do?
This changes the terminal location to the Desktop folder in your home directory. The new workspace is created there.
Before you run the next commands, what folder do you expect Visual Studio Code to display?
- Create the empty workspace and open it in Visual Studio Code by running these commands:
mkdir docker-image-expedition
cd docker-image-expedition
code .
What do these commands do?
- The first command creates the docker-image-expedition directory on your Desktop.
- The second command moves the terminal into that directory.
- The third command opens the directory as the active Visual Studio Code workspace.
Visual Studio Code should display docker-image-expedition as an empty workspace. No project files should appear yet.
Workspace did not open?
Confirm that the terminal entered docker-image-expedition before the final command. Help me open my Docker expedition workspace in Visual Studio Code.
✔️ Awesome, I've got everything!
Your empty docker-image-expedition workspace is open. Docker is running with a reachable client and server.
ⓧ I'd like to double check the full code
There is no code to compare in this setup step. The docker-image-expedition workspace must be empty and open in Visual Studio Code.
Your Docker workstation is ready. Next, you will pull an exact NGINX image and investigate the layers stored on your machine.
Pull and Inspect an NGINX Image
Your Docker workstation can reach the engine. Your empty workspace is ready for its first visible artifact.
Before an image can create a container, its registry reference must resolve into local layers. You will pull a pinned NGINX image from Docker Hub.
In this step, get ready to:
- Pull the pinned NGINX image into your local image store.
- Compare its repository, tag, image ID, and digest.
- Inspect its metadata and layer history.
Pull the pinned image
A registry stores images that Docker can download. A pinned tag gives this expedition a consistent starting point.
- Pull nginx:1.31.4-alpine into the local image store by running this command:
docker image pull nginx:1.31.4-alpine
What Does This Command Do?
- The image reference selects the nginx repository.
- The tag selects the 1.31.4-alpine version.
- Docker downloads the image layers into its local image store.
The terminal shows each image layer as Docker downloads or reuses it. The final lines confirm the image reference and registry digest.
Did the Pull Fail?
Confirm that Docker Desktop or Docker Engine is still running. Check your internet connection if the registry cannot be reached.
Ask for help with the pull output.
What do you expect Docker to do when every required layer is already local?
- Repeat the pull by running the same command:
docker image pull nginx:1.31.4-alpine
What Changed on the Second Pull?
Docker checks the registry reference against the local image. It reports that the local image is current instead of downloading every layer again.
Compare the image inventory
Docker exposes several identities for one local image. The repository and tag form a readable reference. The image ID identifies local content. The digest identifies registry content.
- List every local image by running this command:
docker image ls
What Does This Inventory Show?
The table lists locally available images. Find the row whose repository is nginx.
That row includes the tag, local image ID, creation time, and size.
- Display the registry digest for the pinned image by running this command:
docker image ls --digests nginx:1.31.4-alpine
Why Display the Digest?
A tag is a readable image reference. A digest is an immutable content reference supplied by the registry.
- Filter the inventory to the pinned NGINX reference by running this command:
docker image ls --filter reference=nginx:1.31.4-alpine
What Does the Filter Prove?
The filter limits the table to the matching image reference. This makes a crowded local inventory easier to query.
Is the NGINX Row Missing?
Check that the repository is nginx. Confirm that its tag is 1.31.4-alpine.
Ask for help comparing the pull result with the image inventory.
Inspect metadata and layers
The inventory provides a summary. Inspection reveals the image configuration and filesystem layer references.
- Display the image metadata by running this command:
docker image inspect nginx:1.31.4-alpine
What Does Inspection Reveal?
The structured output describes the local image. It includes identity, configuration, architecture, and filesystem layer details.
- Trace the image layer history by running this command:
docker image history nginx:1.31.4-alpine
How Does History Help?
Each history row represents work that contributed to the image. Reusable layers make image transfer and storage more efficient.
- Display the complete layer-creation commands by running this optional command:
docker image history --no-trunc nginx:1.31.4-alpine
Why Use the Full History?
The option prevents Docker from shortening the history output. The wider result exposes the full layer-creation commands.
Before the final check, which columns do you expect to identify the readable reference and immutable registry content?
- Verify the pinned image inventory by running this command:
docker image ls --digests nginx:1.31.4-alpine
What Should You See?
The result shows the nginx repository and the 1.31.4-alpine tag. It also shows a registry digest, local image ID, and image size.
Is the Digest Missing?
Confirm that you included the digest display option in the final command. Repeat the pull if the exact image reference is absent.
Ask for help interpreting the image inventory.
Your pinned NGINX image is now stored locally with its identities and layers exposed. Next, you will turn this inert image into a running browser-visible container.
Run an NGINX Container
You have traced the local NGINX Docker image back to its registry reference. Now you can use that inert package to create a running process.
A container applies runtime settings to an image. This step shows how those settings control browser access.
In this step, get ready to:
- Create a named container without published ports.
- Replace it with a detached container that publishes port 8080.
- View the NGINX page and inspect its request logs.
Run the image without publishing a port
A container can run even when your browser has no route to its web server. You will test that gap before adding runtime configuration.
Predict whether the NGINX page will be available from your browser after this first run.
- Create the initial container from nginx:1.31.4-alpine by running this command:
docker run --name expedition-base -d nginx:1.31.4-alpine
What does this command do?
- The image reference selects the local NGINX image as the container template.
- The --name flag assigns the stable name expedition-base.
- The -d flag leaves the container running in the background.
- Open http://localhost:8080 in your browser.
The page is unreachable. This is the intended shortfall.
Predict whether Docker still lists expedition-base as running.
- Check the container state by running this command:
docker ps
What does this command show?
The command lists running containers. Its output proves that browser access and container execution are separate states.
The output includes expedition-base. The container runs successfully without a published host port.
Container missing from the list?
Confirm that Docker Desktop or Docker Engine is still running. Check that the image reference is spelled exactly as nginx:1.31.4-alpine.
Ask for help with the container state: Why is my NGINX container missing from Docker's running container list?
Replace the container with a published port
Port publishing creates a route from a host address to a container port. Binding to 127.0.0.1 keeps that route on your computer.
- Replace the initial container with the published version by running these commands:
docker stop expedition-base
docker rm expedition-base
docker run --name expedition-base -d -p 127.0.0.1:8080:80 nginx:1.31.4-alpine
How does the replacement work?
- The first command stops the running container process.
- The second command removes the stopped container.
- The final command creates a replacement in detached mode.
- The -p flag maps host loopback port 8080 to container port 80.
Docker prints a new container ID. That ID confirms the replacement was created.
Replacement command failed?
A name conflict means the earlier container was not removed. A port conflict means another process already uses port 8080.
Ask for help with the replacement: Why did Docker fail to create my published expedition-base container?
Verify browser access and inspect logs
The published route now connects your browser to NGINX. Each browser request also creates a record in the container logs.
- Open http://localhost:8080 in your browser.
- Refresh the NGINX welcome page twice.
Predict which port mapping Docker will show for expedition-base.
- Verify the running container by running this command:
docker ps --filter name=expedition-base
What does the filter prove?
The name filter narrows the running-container inventory to this experiment. The port column exposes the active host-to-container mapping.
The row shows expedition-base with 127.0.0.1:8080->80/tcp. The browser displays the NGINX welcome page.
- View the five most recent log lines by running this command:
docker logs --tail 5 expedition-base
What do the logs reveal?
NGINX records recent HTTP requests from your browser. Refreshing the page gives you fresh request activity to inspect.
The recent lines include requests generated by opening or refreshing the welcome page.
- Compare running containers with all containers by running these commands:
docker ps
docker ps -a
How do these inventories differ?
The first command lists running containers. The second includes stopped containers as well.
- Stop and remove the experiment by running these commands:
docker stop expedition-base
docker rm expedition-base
Why clean up in this order?
Stopping ends the container process cleanly. Removing then deletes the container record while keeping the local image.
Predict whether the all-container inventory will still include expedition-base.
- Confirm the container is gone by running this command:
docker ps -a
What confirms cleanup?
The all-container inventory contains no expedition-base row. The local nginx:1.31.4-alpine image remains available for reuse.
Container still listed?
A remaining row means the stop or removal command did not complete. Check the container name before repeating the cleanup.
Ask for help with cleanup: Why does expedition-base still appear after I stopped and removed it?
You have turned an inert image into a browser-visible service while controlling its lifecycle. Next, you will build your own versioned image from project files.
Build an Immutable Dashboard Image
Your local NGINX image can serve a website. Now you will package your own dashboard into a custom Docker image.
A Dockerfile snapshots the dashboard files into image layers. Editing the original files later gives you direct evidence that the built image is immutable.
In this step, get ready to:
- Create the dashboard source files and image instructions.
- Build and run the version 1.0 dashboard image.
- Edit the source and prove that the existing image stays unchanged.
Create the dashboard files
The build context contains every file Docker can access during the build. Your .dockerignore file keeps unrelated files out of that context.
- In the Visual Studio Code Explorer sidebar for docker-image-expedition, click the New File button.
- Enter Dockerfile as the file name.
- Add the image instructions by copying this code into Dockerfile:
FROM nginx:1.31.4-alpine
COPY index.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/style.css
What does this file do?
- The base image supplies the NGINX web server.
- The two copy instructions snapshot your HTML and CSS inside NGINX's static content directory.
- Save Dockerfile.
- Confirm that Dockerfile appears in the Visual Studio Code Explorer sidebar.
- Click the New File button in the Explorer sidebar.
- Enter .dockerignore as the file name.
- Define the excluded files by copying this code into .dockerignore:
.git
.gitignore
Dockerfile*
README.md
What does this file do?
These patterns exclude Git metadata, documentation, and Dockerfile variants from the build context. A smaller context keeps the build focused on the files the image needs.
- Save .dockerignore.
- Confirm that .dockerignore appears beside Dockerfile in the Explorer sidebar.
Missing one of the files?
Check that both files sit directly inside docker-image-expedition. Make sure .dockerignore begins with a full stop.
Help me check the Docker build files in my workspace.
The dashboard HTML describes the artifact journey from registry to browser. Its first version identifies the image as docker-lab:1.0.
- Click the New File button in the Explorer sidebar.
- Enter index.html as the file name.
- Create the document header and dashboard introduction by copying this first section into index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Docker Image Expedition</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="dashboard">
<p class="eyebrow">Docker Image Expedition</p>
<h1>Image version 1.0 is running</h1>
<p class="summary">
This page was copied into an immutable image layer and is being served by an NGINX container.
</p>
What does this section do?
- The document header connects style.css to the page.
- The dashboard heading makes the running image version visible in the browser.
- Add the artifact route and concept cards below the final </p> line by copying this section:
<section class="route" aria-label="Artifact journey">
<span>Registry</span>
<span aria-hidden="true">→</span>
<span>Image</span>
<span aria-hidden="true">→</span>
<span>Container</span>
<span aria-hidden="true">→</span>
<span>Browser</span>
</section>
<section class="cards" aria-label="Docker concepts">
<article>
<h2>Reference</h2>
<p><code>docker-lab:1.0</code></p>
</article>
<article>
<h2>Container port</h2>
<p><code>80/tcp</code></p>
</article>
<article>
<h2>Key lesson</h2>
<p>Change source, then rebuild a new image.</p>
</article>
</section>
</main>
</body>
</html>
What does this section do?
The route shows each stage of the artifact journey. The cards expose the image reference, container port, and rebuild lesson you will test.
- Save index.html.
- Confirm that the file contains a version 1.0 heading and a docker-lab:1.0 reference.
Does the HTML look incomplete?
Make sure the second section sits directly below the first section. The final lines must close the section, main element, body, and HTML document.
Help me find missing HTML in my dashboard.
The stylesheet turns the artifact journey into a browser-ready dashboard. Each group controls one part of the layout.
- Click the New File button in the Explorer sidebar.
- Enter style.css as the file name.
- Set the page palette and base layout by copying this code into style.css:
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: #08111f;
color: #e6edf7;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background:
radial-gradient(circle at top left, #123a61 0, transparent 34rem),
#08111f;
}
What does this styling do?
The root rules establish the dark palette and font stack. The body grid centers the dashboard over a blue radial background.
- Add the dashboard panel and eyebrow styling below the body rules by copying this code:
.dashboard {
width: min(960px, calc(100% - 2rem));
padding: clamp(2rem, 6vw, 4rem);
border: 1px solid #29435f;
border-radius: 24px;
background: rgb(10 24 42 / 92%);
box-shadow: 0 24px 80px rgb(0 0 0 / 35%);
}
.eyebrow {
margin: 0 0 0.75rem;
color: #55c2ff;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
What does this styling do?
The dashboard becomes a bordered panel with responsive spacing. The eyebrow creates a compact label above the main heading.
- Add the heading and summary styling below the eyebrow rules by copying this code:
h1 {
max-width: 14ch;
margin: 0;
font-size: clamp(2.5rem, 8vw, 5.5rem);
line-height: 0.98;
}
.summary {
max-width: 62ch;
margin: 1.5rem 0 2rem;
color: #b9c8d8;
font-size: 1.1rem;
line-height: 1.7;
}
What does this styling do?
The heading scales with the browser width. The summary remains readable by limiting its width and increasing its line spacing.
- Add the artifact route and card grid styling below the summary rules by copying this code:
.route,
.cards {
display: grid;
gap: 1rem;
}
.route {
grid-template-columns: repeat(7, auto);
align-items: center;
justify-content: start;
margin-bottom: 2rem;
color: #8de4c1;
font-weight: 700;
}
.cards {
grid-template-columns: repeat(3, 1fr);
}
What does this styling do?
The route arranges seven journey labels in a row. The cards use three equal columns for the image details.
- Add the card surfaces and code colors below the grid rules by copying this code:
article {
padding: 1.25rem;
border: 1px solid #29435f;
border-radius: 16px;
background: #0d2036;
}
h2 {
margin-top: 0;
color: #8de4c1;
font-size: 1rem;
}
code {
color: #ffd479;
}
What does this styling do?
Each article becomes a separate card. The heading and code colors distinguish labels from technical values.
- Finish the stylesheet with the responsive layout by copying this code below the code color rules:
@media (max-width: 720px) {
.route {
grid-template-columns: 1fr;
}
.route span[aria-hidden="true"] {
transform: rotate(90deg);
justify-self: start;
}
.cards {
grid-template-columns: 1fr;
}
}
What does this styling do?
The media query stacks the route and cards on narrow screens. Rotated route markers preserve the journey direction.
- Save style.css.
- Confirm that Dockerfile, .dockerignore, index.html, and style.css appear in the Explorer sidebar.
Is a style section missing?
Check that every selector appears once in style.css. Confirm that each opening brace has a matching closing brace.
Help me compare my dashboard stylesheet with the provided sections.
Build and run version 1.0
A build converts the files in your workspace into immutable image layers. The tag docker-lab:1.0 gives that snapshot a versioned local reference.
- Build the version 1.0 image from the current workspace by running:
docker build --pull -t docker-lab:1.0 .
What does this command do?
Docker uses the current folder as the build context. The pull flag checks the referenced base image before creating the tagged custom image.
- Confirm that the custom image exists by running:
docker image ls docker-lab:1.0
What should you see?
The image inventory shows the docker-lab repository with the 1.0 tag. It also shows an image ID and size.
- Inspect the layers created for the custom image by running:
docker image history docker-lab:1.0
What does the history prove?
The history connects your custom copy instructions to layers above the NGINX base. Those layers preserve the files as they existed during the build.
Did the build fail?
Confirm that all four files sit directly inside docker-image-expedition. Check the copy paths and file names for exact capitalization.
Help me diagnose my Docker image build.
The image now contains the version 1.0 dashboard. A container turns that snapshot into a running web server.
- Predict what version the browser will display when this new container starts.
- Start the dashboard container on localhost port 8080 by running:
docker run --name dashboard-v1 -d -p 127.0.0.1:8080:80 docker-lab:1.0
What does this command do?
The command creates a container named dashboard-v1 from the versioned image. Detached mode keeps it running while localhost port 8080 forwards requests to the container's web server.
- Open http://localhost:8080 in your browser.
The dashboard displays Image version 1.0 is running. Its reference card shows docker-lab:1.0.
Can't load the dashboard?
Confirm that another process is not using localhost port 8080. Check that the container name and image tag match the command exactly.
Help me diagnose why my dashboard container is not reachable.
Edit the source and test immutability
The running container reads files from the image snapshot created during the build. Changing the host copy of index.html sets up the immutability test.
- Return to index.html in the Visual Studio Code editor.
- Find the dashboard content that currently contains both version 1.0 values:
<h1>Image version 1.0 is running</h1>
<p class="summary">
This page was copied into an immutable image layer and is being served by an NGINX container.
</p>
<section class="route" aria-label="Artifact journey">
<span>Registry</span>
<span aria-hidden="true">→</span>
<span>Image</span>
<span aria-hidden="true">→</span>
<span>Container</span>
<span aria-hidden="true">→</span>
<span>Browser</span>
</section>
<section class="cards" aria-label="Docker concepts">
<article>
<h2>Reference</h2>
<p><code>docker-lab:1.0</code></p>
</article>
<article>
<h2>Container port</h2>
<p><code>80/tcp</code></p>
</article>
<article>
<h2>Key lesson</h2>
<p>Change source, then rebuild a new image.</p>
</article>
</section>
What are you changing?
The heading and reference card are the two visible version markers. They currently describe the snapshot inside docker-lab:1.0.
- Replace both visible 1.0 values with 2.0 so the same section looks like this:
<h1>Image version 2.0 is running</h1>
<p class="summary">
This page was copied into an immutable image layer and is being served by an NGINX container.
</p>
<section class="route" aria-label="Artifact journey">
<span>Registry</span>
<span aria-hidden="true">→</span>
<span>Image</span>
<span aria-hidden="true">→</span>
<span>Container</span>
<span aria-hidden="true">→</span>
<span>Browser</span>
</section>
<section class="cards" aria-label="Docker concepts">
<article>
<h2>Reference</h2>
<p><code>docker-lab:2.0</code></p>
</article>
<article>
<h2>Container port</h2>
<p><code>80/tcp</code></p>
</article>
<article>
<h2>Key lesson</h2>
<p>Change source, then rebuild a new image.</p>
</article>
</section>
What does this edit change?
The host source now describes version 2.0. The existing image and container still hold the earlier version 1.0 snapshot.
- Save index.html.
- Predict whether refreshing the browser will show version 1.0 or version 2.0.
- Refresh http://localhost:8080 in your browser.
The browser still displays Image version 1.0 is running. This unchanged result is the intended proof that the running container uses the immutable image snapshot.
Why does the old version remain?
The build copied the original HTML into docker-lab:1.0. The container has no live connection to the edited file in your workspace.
A source change becomes part of a container only after you build a new image and create a container from that new snapshot.
✔️ Awesome, I've got everything!
Your workspace contains all four source files. The host HTML says version 2.0, while the running dashboard-v1 container still serves version 1.0.
ⓧ I'd like to double check the full code
Confirm that your files contain the chunks from this step in the same order.
- Check that Dockerfile starts from nginx:1.31.4-alpine and copies index.html plus style.css into the NGINX static content directory.
- Check that .dockerignore contains the four exclusion patterns shown above.
- Check that index.html contains the document header, dashboard introduction, artifact route, concept cards, and both version 2.0 values.
- Check that style.css contains the palette, page layout, dashboard panel, typography, route, cards, and responsive media query chunks shown above.
- Check that dashboard-v1 remains running from docker-lab:1.0 on localhost port 8080.
You have captured the stale version on purpose. Next, you will build a new tagged image and compare both snapshots side by side.
Rebuild, Compare, and Clean Up
Your version 1.0 dashboard proved that a running Docker container keeps the content stored in its image. The edited source now needs a new image before the browser can show version 2.0.
You will build the new version with BuildKit. You will compare both containers before removing the temporary resources in dependency order.
In this step, get ready to:
- Build the version 2.0 image and observe build cache reuse.
- Run versions 1.0 and 2.0 as independent containers.
- Remove both containers and custom image tags safely.
Build and inspect version 2.0
A new image captures the edited source as a fresh immutable snapshot. Its new tag preserves version 1.0 while giving version 2.0 its own reference.
- Build the edited dashboard as docker-lab:2.0 by running this command:
docker build -t docker-lab:2.0 .
What does this command do?
Docker uses the current docker-image-expedition folder as the build context. The tag records this snapshot as docker-lab:2.0.
BuildKit can reuse unchanged work from the earlier build. The updated index.html produces a new source-copy layer.
The completed build leaves both custom image tags available locally.
Build not completing?
Confirm the terminal is still inside the docker-image-expedition folder. Check that Docker Desktop or Docker Engine is still running.
Ask for help with the build output: Why is my docker-lab:2.0 image build failing?
Before you repeat the build, predict which steps BuildKit can reuse.
- Repeat the version 2.0 build by running the same command:
docker build -t docker-lab:2.0 .
What should you notice?
The source files match the previous build. BuildKit reuses cached work instead of recreating every layer.
- Compare the custom image inventory and version 2.0 layer history by running these commands:
docker image ls --filter reference=docker-lab:*
docker image history docker-lab:2.0
What does this evidence show?
The inventory contains separate docker-lab:1.0 and docker-lab:2.0 references. The history shows the layers that make up version 2.0.
The shared base work can be reused. The changed page remains part of the newer image snapshot.
Run both dashboard versions
An image tag selects the snapshot used when Docker creates a container. Each resulting container runs independently with its own name and published host port.
- Start the version 2.0 dashboard on host port 8081 by running this command:
docker run --name dashboard-v2 -d -p 127.0.0.1:8081:80 docker-lab:2.0
What does this command do?
Docker creates dashboard-v2 from the version 2.0 image. Detached mode keeps it running in the background.
The port mapping sends browser requests from host port 8081 to the container's web server on port 80.
Version 2.0 not starting?
Confirm that docker-lab:2.0 appears in your local image inventory. Another process may already be using host port 8081.
Ask for help with the container output: Why is dashboard-v2 failing to start?
Before you check the browsers, predict which version each address serves.
- Open http://localhost:8080 in your browser.
- Open http://localhost:8081 in a second browser tab.
Port 8080 shows version 1.0. Port 8081 shows version 2.0.
What does the comparison prove?
The two containers serve different image snapshots at the same time. Editing the source did not mutate version 1.0.
Building version 2.0 made the change explicit. Separate tags keep both results available until you choose to remove them.
Remove the dashboard resources
Containers depend on images, so cleanup starts with the running containers. Removing the custom images afterwards leaves the reusable NGINX base image available.
- Stop both dashboard containers by running this command:
docker stop dashboard-v1 dashboard-v2
What does this command do?
Docker stops both running web server processes. Their container records still exist until you remove them.
- Remove both stopped containers by running this command:
docker rm dashboard-v1 dashboard-v2
What does this command do?
Docker removes the container records for both dashboard versions. Their image tags remain available for the next cleanup action.
- Remove both custom image tags by running this command:
docker image rm docker-lab:1.0 docker-lab:2.0
What does this command do?
Docker removes both local custom image tags. Shared base layers remain available through nginx:1.31.4-alpine.
Image still in use?
A remaining container can keep an image in use. Confirm that both dashboard containers were stopped before the removal commands.
Ask for help with the cleanup output: Why can’t Docker remove my dashboard images?
Before you verify cleanup, predict which filtered commands return no rows and which reusable image remains.
- Verify the final container and image state by running these commands:
docker ps -a --filter name=dashboard
docker image ls --filter reference=docker-lab:*
docker image ls
What should you see?
The dashboard container filter returns no matching rows. The custom image filter also returns no matching rows.
The full image inventory still includes nginx:1.31.4-alpine. Your source files remain in docker-image-expedition.
Secret mission
Put Pull Policies and Automatic Cleanup to the Test
Test how Docker behaves when registry pulls are blocked. Prove that an exited container can remove itself automatically.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally, so the remaining resources have no ongoing usage charges.
Resources you used:
- The source files in the docker-image-expedition folder.
- The local NGINX image tagged nginx:1.31.4-alpine.
Keep everything running
No action needed. Choose this if you are still actively building with Docker.
- Keep the docker-image-expedition folder so you can rebuild the dashboard.
- Keep nginx:1.31.4-alpine locally so future builds can reuse the base image.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files and local images remain available for later.
- Quit Docker Desktop if you are using Windows or macOS.
- Stop Docker Engine on Linux by running this command:
sudo systemctl stop docker
What does this command do?
This stops the Docker service on Linux. Your local images and project files remain stored on disk.
Delete - I don't want to use this again
Remove all project resources to start fresh. Confirm the project containers are absent before deleting the remaining image and files.
- Confirm that no dashboard or policy test containers remain by running these commands:
docker ps -a --filter name=dashboard
docker ps -a --filter name=policy-test
What should you see?
Both commands should return no matching container rows. This confirms the project containers are already gone.
- Remove the reusable base image if no other container or image needs it by running this command:
docker image rm nginx:1.31.4-alpine
What does this command do?
This removes the local NGINX image from Docker's image store. It frees the disk space used by that image.
Image still in use?
A remaining container or dependent image can prevent removal. Use the earlier container checks to find project containers before trying again.
Ask for help identifying what still depends on the image.
- Delete the docker-image-expedition folder using your operating system's file manager.
- Confirm that docker-image-expedition no longer appears in its previous location.
Nice Work!
Nice Work!
Nice work! You traced a Docker image from its registry reference to browser-visible containers. You also built versioned dashboard images from your own source files.
You've learned how to:
- Pull and investigate an exact NGINX image using its tag, digest, image ID, metadata, and layer history.
- Turn an inert image into a named container that serves a website through a published localhost port.
- Build versioned dashboard images from a Dockerfile. You proved image immutability by rebuilding the changed source as version 2.0.
- Complete the Secret Mission by proving local-only pull behavior with --pull=never. You also proved automatic container removal with --rm.
Ready to quiz yourself?