Build and Debug an Apache2 Container
Build a custom Apache httpd container serving an SRE Status Dashboard.
Introduction
30 Second Summary
When a website goes down at 3 AM, someone has to find the problem and fix it before users notice. That skill set starts with mastering the environment your applications run inside.
In this project, you will build a custom Docker container serving a static SRE Status Dashboard using Apache httpd. You'll write a Dockerfile with a production-grade health check, then debug and manage the container using real SRE tools and workflows.
What You'll Build
A custom Docker container running Apache httpd, serving a static status dashboard page built from a Dockerfile with health checks, metadata labels, and proper layer ordering.
By the end of this project, you'll have:
- A custom Dockerfile based on httpd:2.4.66 with a production-grade HEALTHCHECK instruction.
- Hands-on debugging experience using docker exec, docker logs, docker inspect, and docker history.
- A working mental model of image layers, build cache, and PID 1 signal handling for graceful container shutdown.
- A secret mission to enable Apache mod_status for real-time container observability.
Are there any prerequisites?
You should be comfortable navigating a Linux terminal and running basic commands. A basic understanding of what a web server does is helpful but not required.
Before We Start
Before We Start...
Verify Your Docker Environment
Before building your custom Apache container, you need to confirm that Docker is installed and running correctly on your machine.
In this step, you'll verify your Docker environment, pull the base Apache httpd image for your project, and create the directory structure where all your files will live.
In this step, get ready to:
- Confirm Docker Engine is running and check version details.
- Pull and examine the official Apache httpd image.
- Create the project directory structure.
Confirm Docker Engine is running and check versions
Docker Engine is the background service that builds and runs your containers. Every command in this project depends on it being active.
- Confirm your Docker installation by running this command:
docker version
You should see two sections in the output: Client and Server. Both appearing confirms that the Docker CLI can communicate with the Docker daemon.
I see Client and Server info
You're all set. The Docker daemon is running and ready for commands.
Command not found
If you see command not found, Docker is not installed or not in your PATH. Follow the official Docker Engine install guide for your Linux distribution.
If you see Cannot connect to the Docker daemon, Docker is installed but the daemon is not running. Start it with this command:
sudo systemctl start docker
If you're using Docker Desktop, open Docker Desktop from your applications and wait for it to finish starting up. Then run docker version again.
- Check your Docker system configuration by running:
docker info
What should I look for in docker info?
The output contains a lot of detail. Three fields are especially useful for production debugging:
- Server Version confirms the exact Docker Engine version running on the host.
- Storage Driver shows how Docker stores image layers on disk (commonly overlay2).
- Cgroup Driver controls how Linux resource limits (CPU, memory) are applied to containers.
Pull the official httpd image and examine it
Your SRE Status Dashboard will be built on top of the official Apache httpd image. Pulling a specific version tag locks your project to a known-good release instead of relying on latest, which can change unexpectedly.
- Pull the specific Apache httpd image version by running:
docker pull httpd:2.4.66
- Confirm the image was downloaded by running:
docker images httpd
You should see httpd listed with the tag 2.4.66 along with the image ID and total size on disk.
Pull failed or timing out?
- Check your internet connection. Docker needs to reach Docker Hub to download the image.
- If you're behind a corporate proxy, configure Docker's proxy settings in the Docker daemon config file.
- help me debug why docker pull is failing
- Examine how the image is configured to run by running:
docker inspect httpd:2.4.66
This outputs a large JSON blob with every detail about the image. Scroll through it and look for the fields highlighted below.
What do these fields tell you?
- Cmd is the default command the container runs. You'll see httpd-foreground, which starts Apache in the foreground so Docker can manage the process.
- ExposedPorts shows port 80/tcp, which is the port Apache listens on inside the container.
- WorkingDir points to /usr/local/apache2, the root of the Apache installation inside the image.
These fields tell you exactly how the image was designed to run. You'll use this knowledge when writing your own Dockerfile in a later step.
Pro tip: filter inspect output
You can extract a single field using the --format flag. For example:
docker inspect httpd:2.4.66 --format='{{json .Config.Cmd}}'
Create the project directory structure
All your project files will live in one directory. This includes the Dockerfile, your HTML content, and later the custom Apache configuration.
- Create your project directory and its subdirectory by running:
mkdir -p ~/docker-fundamentals/public-html
- Move into the project directory by running:
cd ~/docker-fundamentals
- Confirm the directory structure by running:
ls
You should see public-html listed in the output. This is the folder where your SRE Status Dashboard HTML file will go.
Don't see public-html?
- Make sure you included the -p flag in the mkdir command. The -p flag creates the parent directory and subdirectory in one step.
- Run pwd to confirm you are inside ~/docker-fundamentals. If not, run cd ~/docker-fundamentals and try ls again.
- help me create the docker-fundamentals project directory
Your environment is verified and your project directory is ready. Next up, you'll run a stock Apache container and explore what's happening inside it.
Run a Stock Apache Container and Explore It
Your Docker environment is ready and the httpd:2.4.66 image is sitting on your machine. Now it's time to actually run a container from that image.
Running a container is just one command. Understanding what's happening inside it is the real SRE skill you'll practice here.
In this step, get ready to:
- Run a stock Apache httpd container and verify it serves traffic.
- Explore the container's filesystem, processes, and network settings from the inside.
- Examine container logs and understand how PID 1 signal handling affects shutdown.
Run the default httpd container
- Start a detached Apache container with port mapping by running this command:
docker run -dit --name stock-apache -p 8080:80 httpd:2.4.66
What do these flags mean?
- -d runs the container in the background (detached mode).
- -i and -t together allocate an interactive terminal, useful if you need to attach to the container later.
- --name stock-apache assigns a readable name instead of Docker's random default.
- -p 8080:80 maps Port 8080 on your host machine to port 80 inside the container.
- Verify the container is serving traffic by running:
curl -I http://localhost:8080
You should see an HTTP/1.1 200 OK response at the top of the output.
- Check the running container's details by running:
docker ps
Notice the COMMAND column shows httpd-foreground. This is the entrypoint script that runs Apache in the foreground as the container's main process.
Not seeing a 200 response?
- If you see Connection refused, run docker ps to check that the container is actually running. If it's not listed, run docker ps -a to see if it exited immediately.
- If port 8080 is already in use by another application, you'll see a port is already allocated error. Stop the other application or use a different host port like -p 8081:80.
- If the container name stock-apache is already taken from a previous attempt, remove it first with docker rm stock-apache and try again.
Help me debug why my Docker container isn't responding on port 8080.
Explore the container internals with exec and inspect
A running container looks like a black box from the outside. The docker exec command lets you open a shell inside it and poke around, just like SSH-ing into a server.
- Open a shell inside the running container by running:
docker exec -it stock-apache bash
Your prompt changes to something like root@<container-id>:/usr/local/apache2#. You're now inside the container's filesystem.
- View the default Apache welcome page by running:
cat /usr/local/apache2/htdocs/index.html
You should see the simple It works! HTML page. This is what you saw when you ran curl earlier.
- List the Apache configuration directory by running:
ls /usr/local/apache2/conf/
You should see httpd.conf along with other config files. This is the main Apache configuration file you'll customize later.
- View the running processes inside the container by running:
ps aux
What is PID 1 inside a container?
Look at the process list. The parent httpd process runs as PID 1. Several worker processes are spawned beneath it.
PID 1 is special in a container. It's the process that receives signals from Docker (like stop and kill commands). If PID 1 doesn't handle these signals properly, the container can't shut down gracefully. You'll see this in action at the end of this step.
- Type exit and press Enter to leave the container shell.
Seeing 'ps: command not found'?
Some minimal Docker images don't include ps. If this happens, you can install it inside the container by running apt-get update && apt-get install -y procps and then retry ps aux.
Help me run ps inside my httpd Docker container.
Now that you've explored from the inside, let's query the container's metadata from the host using docker inspect.
- Get the container's main process ID on the host by running:
docker inspect stock-apache --format='{{.State.Pid}}'
This returns the host-side PID of the container's PID 1 process. Every container process maps to a real process on the host.
- View the port mapping details by running:
docker inspect stock-apache --format='{{json .NetworkSettings.Ports}}'
What does docker inspect tell you?
The docker inspect command returns the full configuration and state of a container as JSON. The --format flag lets you pull out specific fields using Go template syntax instead of scrolling through hundreds of lines.
The port mapping output confirms that host port 8080 forwards to container port 80. This is one of the first things to check when debugging connectivity issues.
Examine logs and understand PID 1 signal handling
Container logs are the first thing an SRE checks when something goes wrong. Docker captures everything your application writes to stdout and stderr.
- View the container's logs by running:
docker logs stock-apache
You should see Apache's access and error log entries. The curl request you made earlier appears as an access log line.
- Follow the logs in real time by running:
docker logs -f stock-apache
- Open a second terminal window.
- From the second terminal, make another request by running:
curl -I http://localhost:8080
Switch back to the first terminal. You should see a new log entry appear in real time. Press Ctrl+C to stop following the logs.
Pro tip: log following in production
The -f flag (follow) works exactly like tail -f on a log file. In production debugging, you'll often combine it with --since and --until to narrow down a specific time window.
Now let's stop the container and observe how Docker's shutdown process works with PID 1.
- Stop the container by running:
docker stop stock-apache
Notice how fast the stop completes. It should take about one second or less.
Why did the container stop so quickly?
When you run docker stop, Docker sends a SIGTERM signal to PID 1 inside the container. If the process handles SIGTERM properly, it shuts down gracefully and Docker moves on.
If the process ignores SIGTERM, Docker waits 10 seconds and then sends SIGKILL to force-kill it. Apache httpd handles SIGTERM gracefully by default, so the stop is fast. A slow stop (10+ seconds) is a red flag that your application isn't handling signals correctly.
- Remove the stopped container by running:
docker rm stock-apache
- Confirm the container is gone by running:
docker ps -a
You should no longer see stock-apache in the list. The container and its writable layer are gone. The httpd:2.4.66 image is still on your machine, ready to create new containers from.
Container won't stop or remove?
- If docker rm says the container is still running, you need to stop it first with docker stop stock-apache before removing it.
- If the stop seems stuck (takes more than 10 seconds), you can force-remove a container with docker rm -f stock-apache. This sends SIGKILL immediately.
Help me debug why my Docker container won't stop or remove.
You've now run, explored, and torn down a stock Apache container. Next up, you'll build a custom Docker image with your own HTML page and a production-grade health check.
Build a Custom Image with a Dockerfile
In the last step, you poked around a stock Apache container and saw how it works from the inside. Now it's time to build your own custom image that serves a real page instead of the default "It works!" placeholder.
You'll write a Dockerfile that layers your content and configuration on top of the base httpd:2.4.66 image. The Dockerfile also includes a production-grade HEALTHCHECK so Docker can automatically verify your container is actually serving traffic.
In this step, get ready to:
- Create a static SRE Status Dashboard HTML page.
- Write a Dockerfile with metadata labels, a curl install, and a HEALTHCHECK.
- Build and run your custom container, then verify it reports as healthy.
Create the SRE Status Dashboard HTML page
Every SRE team has an internal status page that shows whether key services are up or down. You'll build a simplified version of one as the page your container serves.
- Open public-html/index.html inside your docker-fundamentals folder using your text editor.
- Copy the following HTML into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SRE Status Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
background: #0d1117;
color: #c9d1d9;
padding: 2rem;
}
h1 {
color: #58a6ff;
margin-bottom: 0.5rem;
font-size: 1.5rem;
}
.subtitle {
color: #8b949e;
margin-bottom: 2rem;
font-size: 0.9rem;
}
.services {
max-width: 600px;
}
.service {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #21262d;
}
.service-name {
font-weight: 600;
}
.status {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.operational {
background: #0d419d;
color: #58a6ff;
}
.degraded {
background: #4d2d00;
color: #d29922;
}
.footer {
margin-top: 2rem;
color: #8b949e;
font-size: 0.8rem;
}
</style>
</head>
<body>
<!-- Dashboard header and service list -->
<h1>SRE Status Dashboard</h1>
<p class="subtitle">Internal Service Health Overview</p>
<div class="services">
<div class="service">
<span class="service-name">API Gateway</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Auth Service</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Database Primary</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Cache Layer</span>
<span class="status degraded">Degraded</span>
</div>
<div class="service">
<span class="service-name">Message Queue</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">CDN Edge</span>
<span class="status operational">Operational</span>
</div>
</div>
<p class="footer">
Container ID: check via <code>docker inspect</code> |
Built with httpd:2.4.66 |
Served by Apache/2.4
</p>
</body>
</html>
What does this page do?
- The page lists six services (API Gateway, Auth Service, Database Primary, etc.) with color-coded status badges.
- The .operational class renders a blue badge. The .degraded class renders a yellow badge. Cache Layer is intentionally marked as degraded so the dashboard looks realistic.
- The dark background and monospace font mimic real internal dashboards you'd see in SRE teams.
- Save public-html/index.html.
- Confirm the file exists by running:
ls public-html/
- You should see index.html in the output.
Don't see index.html?
- Make sure you're in the ~/docker-fundamentals directory. Run pwd to check.
- Confirm the public-html folder exists. If it doesn't, create it with mkdir public-html and save your file inside it.
- Help me debug my project directory structure.
Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SRE Status Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
background: #0d1117;
color: #c9d1d9;
padding: 2rem;
}
h1 {
color: #58a6ff;
margin-bottom: 0.5rem;
font-size: 1.5rem;
}
.subtitle {
color: #8b949e;
margin-bottom: 2rem;
font-size: 0.9rem;
}
.services {
max-width: 600px;
}
.service {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #21262d;
}
.service-name {
font-weight: 600;
}
.status {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.operational {
background: #0d419d;
color: #58a6ff;
}
.degraded {
background: #4d2d00;
color: #d29922;
}
.footer {
margin-top: 2rem;
color: #8b949e;
font-size: 0.8rem;
}
</style>
</head>
<body>
<h1>SRE Status Dashboard</h1>
<p class="subtitle">Internal Service Health Overview</p>
<div class="services">
<div class="service">
<span class="service-name">API Gateway</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Auth Service</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Database Primary</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Cache Layer</span>
<span class="status degraded">Degraded</span>
</div>
<div class="service">
<span class="service-name">Message Queue</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">CDN Edge</span>
<span class="status operational">Operational</span>
</div>
</div>
<p class="footer">
Container ID: check via <code>docker inspect</code> |
Built with httpd:2.4.66 |
Served by Apache/2.4
</p>
</body>
</html>
Write the Dockerfile
A Dockerfile is a recipe that tells Docker how to build an image, layer by layer. Each instruction in the file (FROM, RUN, COPY) creates a new layer on top of the previous one.
- Create a file called Dockerfile (no file extension) in your docker-fundamentals folder.
- Copy the following into the file:
# syntax=docker/dockerfile:1
# Start from the official Apache image (Debian-based for easier debugging)
FROM httpd:2.4.66
# Add metadata so docker inspect shows who owns this image
LABEL maintainer="sre-team"
LABEL version="1.0"
LABEL description="SRE Status Dashboard on Apache httpd"
# Install curl for the HEALTHCHECK, then clean up to keep the image small
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
# Replace the default "It works!" page with our dashboard
COPY public-html/ /usr/local/apache2/htdocs/
# Document the port Apache listens on
EXPOSE 80
# Automatically verify the server is responding every 30 seconds
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["curl", "-f", "http://localhost:80/", "-o", "/dev/null", "-s", "-w", "%{http_code}"]
What does each instruction do?
- FROM httpd:2.4.66 sets the base image. Everything you add builds on top of this official Apache image, which is based on Debian.
- LABEL adds metadata (maintainer, version, description) that shows up when you run docker inspect on the image.
- RUN apt-get ... installs curl inside the image because the base httpd:2.4.66 image does not include it. The HEALTHCHECK needs curl to test the server. The rm -rf /var/lib/apt/lists/* at the end cleans up cached package lists so they don't bloat the image.
- COPY public-html/ /usr/local/apache2/htdocs/ replaces the default Apache page with your dashboard.
- HEALTHCHECK tells Docker to run a curl command every 30 seconds to confirm the server is responding. If the check fails 3 times in a row, Docker marks the container as unhealthy.
Why install curl just for a health check?
The HEALTHCHECK instruction runs inside the container, so it can only use tools that are installed in the image. The base Apache image ships lean and does not include curl. Without it, the health check would have no way to verify the server is responding to HTTP requests.
- Save the Dockerfile.
- Confirm both project files are in place by running:
ls
- You should see Dockerfile and public-html in the output.
Missing the Dockerfile or public-html folder?
- Make sure the Dockerfile has no file extension. It should be Dockerfile, not Dockerfile.txt.
- Confirm you're in the ~/docker-fundamentals directory, not inside public-html.
- Help me figure out why my Dockerfile or public-html folder is missing.
Awesome, I've got everything!
Great. Double check you've saved the Dockerfile before building.
I'd like to double check the full code
# syntax=docker/dockerfile:1
FROM httpd:2.4.66
LABEL maintainer="sre-team"
LABEL version="1.0"
LABEL description="SRE Status Dashboard on Apache httpd"
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY public-html/ /usr/local/apache2/htdocs/
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["curl", "-f", "http://localhost:80/", "-o", "/dev/null", "-s", "-w", "%{http_code}"]
Build the image and run the custom container
Your Dockerfile and HTML are ready. Time to turn them into a runnable image and launch it.
- Build your custom image by running this command from inside the docker-fundamentals folder:
docker build -t sre-dashboard:v1 .
What is happening during the build?
Watch the terminal output as it runs. Each step in your Dockerfile creates a new layer in the image.
- -t sre-dashboard:v1 tags the image with the name sre-dashboard and version v1.
- The . at the end tells Docker to use the current directory as the build context. Docker sends all files in this directory to the build engine.
- The RUN apt-get step takes the longest because it downloads and installs curl. You'll see this layer get cached on future builds.
- Verify the image was created by running:
docker images sre-dashboard
You should see a row with sre-dashboard as the repository and v1 as the tag.
Build failed with an error?
- If you see failed to read dockerfile, confirm you're running the command from the ~/docker-fundamentals directory where the Dockerfile lives.
- If you see COPY failed, make sure the public-html folder with index.html is in the same directory as the Dockerfile.
- If apt-get fails with a network error, check your internet connection and try the build again.
- Help me debug my Docker build failure.
- Start a container from your new image by running:
docker run -dit --name sre-dashboard -p 8080:80 sre-dashboard:v1
What do these flags do?
- -dit runs the container detached (-d), interactive (-i), and with a terminal (-t).
- -p 8080:80 maps port 8080 on your machine to port 80 inside the container.
- --name sre-dashboard gives the container a memorable name instead of a random one.
- Verify the dashboard is being served by running:
curl http://localhost:8080
You should see the full HTML of your SRE Status Dashboard printed in the terminal.
- Check the container's health status by running:
docker ps
The STATUS column will initially say Up ... (health: starting). Wait about 30-40 seconds and run docker ps again. It should now show (healthy) next to the uptime.
- Inspect the full health check history by running:
docker inspect sre-dashboard --format='{{json .State.Health}}'
What does the health output show?
This outputs a JSON object with the container's health status, the number of consecutive failures, and a log of recent health check results. Each log entry shows the exit code, start time, and output of the curl command. An exit code of 0 means the check passed.
Container not showing as healthy?
- If docker ps still shows (health: starting), wait another 30 seconds. The HEALTHCHECK has a 10-second start period plus a 30-second interval before the first check runs.
- If the status says (unhealthy), run docker inspect sre-dashboard --format='{{json .State.Health}}' and check the Output field for error details. This usually means curl was not installed correctly during the build.
- If curl http://localhost:8080 returns Connection refused, the container may not be running. Run docker ps -a to check if it exited.
- Help me debug my container's health check.
Your custom image is built and your container is running with a working health check. Next up, you'll dig into how Docker layers and build cache work under the hood to make rebuilds faster.
Understand Image Layers and Build Cache
Your custom image is running with a passing health check. Now it's time to look under the hood at how Docker actually built that image.
Docker images are stacks of read-only layers, one per Dockerfile instruction. Understanding how these layers work and how Docker caches them is the key to writing efficient Dockerfiles.
In this step, get ready to:
- Inspect image layers using docker history.
- Trigger a cache miss by editing a file and rebuilding the image.
- Compare image sizes and layer counts between versions.
Inspect image layers with docker history
Every instruction in your Dockerfile creates a separate layer in the final image. You can see these layers and their sizes using the docker history command.
- View the layers in your custom image by running this command:
docker history sre-dashboard:v1
What does this output show?
- Each row represents one layer created by a Dockerfile instruction.
- The SIZE column shows how much data each instruction added to the image.
- The most recent layer is at the top, and the base image layers are at the bottom.
The default output truncates the commands that created each layer. You can view the full, untruncated output to trace each layer back to your Dockerfile.
- View the full commands for each layer by running:
docker history --no-trunc sre-dashboard:v1
What should I see?
Every FROM, RUN, COPY, and HEALTHCHECK instruction from your Dockerfile appears as a separate layer.
Compare this output to your Dockerfile. You can trace each layer back to the exact instruction that created it.
Trigger a cache miss and observe rebuild behavior
Docker caches each layer after the first build. If an instruction and its inputs have not changed, Docker reuses the cached layer instead of rebuilding it.
To see this in action, you will make a small change to your HTML file and rebuild the image.
- Open public-html/index.html in your text editor.
- Find the Cache Layer service entry. It currently looks like this:
<div class="service">
<span class="service-name">Cache Layer</span>
<span class="status operational">Operational</span>
</div>
- Change operational to degraded in the class name.
- Change Operational to Degraded in the text content.
- The updated entry should look like this:
<div class="service">
<span class="service-name">Cache Layer</span>
<span class="status degraded">Degraded</span>
</div>
- Save index.html.
Awesome, I've got everything!
Double check you have saved your file before moving on.
I'd like to double check the full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SRE Status Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
background: #0d1117;
color: #c9d1d9;
padding: 2rem;
}
h1 {
color: #58a6ff;
margin-bottom: 0.5rem;
font-size: 1.5rem;
}
.subtitle {
color: #8b949e;
margin-bottom: 2rem;
font-size: 0.9rem;
}
.services {
max-width: 600px;
}
.service {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid #21262d;
}
.service-name {
font-weight: 600;
}
.status {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.operational {
background: #0d419d;
color: #58a6ff;
}
.degraded {
background: #4d2d00;
color: #d29922;
}
.footer {
margin-top: 2rem;
color: #8b949e;
font-size: 0.8rem;
}
</style>
</head>
<body>
<h1>SRE Status Dashboard</h1>
<p class="subtitle">Internal Service Health Overview</p>
<div class="services">
<div class="service">
<span class="service-name">API Gateway</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Auth Service</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Database Primary</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">Cache Layer</span>
<span class="status degraded">Degraded</span>
</div>
<div class="service">
<span class="service-name">Message Queue</span>
<span class="status operational">Operational</span>
</div>
<div class="service">
<span class="service-name">CDN Edge</span>
<span class="status operational">Operational</span>
</div>
</div>
<p class="footer">
Container ID: check via <code>docker inspect</code> |
Built with httpd:2.4.66 |
Served by Apache/2.4
</p>
</body>
</html>
Now rebuild the image with the changed file to observe how Docker's build cache behaves.
- Rebuild the image with a new tag by running this command:
docker build -t sre-dashboard:v2 .
What should I see in the build output?
- The layers before the COPY instruction show CACHED next to them. Docker reused these because nothing changed.
- The COPY layer and everything after it rebuilt because the file contents changed. This is called cache invalidation.
- A change in any layer forces all subsequent layers to rebuild too.
Why does instruction ordering matter?
Expensive operations like apt-get install should come before frequently-changing operations like COPY of source code.
Your Dockerfile already follows this pattern. The RUN apt-get layer stays cached even when you change the HTML files. If you moved COPY before RUN apt-get install, every HTML change would also force a reinstall of curl.
- Confirm the build completed successfully. You should see each build step finish, with CACHED appearing next to the reused layers.
Build not using cache?
- Make sure you are running docker build from the ~/docker-fundamentals directory where your Dockerfile is located.
- Check that you did not accidentally modify the Dockerfile itself. Only the HTML file should have changed.
- If all layers are rebuilding, your build cache may have been cleared. Run docker builder prune to see if there is cache present.
Why is my Docker build not caching layers?
Compare image sizes and layers
Both v1 and v2 of your image exist on disk now. Docker shares identical layers between images, so storing two versions costs less disk space than you might expect.
- List your sre-dashboard images by running this command:
docker images sre-dashboard
Both images should be nearly the same size. They share every layer except the COPY and HEALTHCHECK layers that rebuilt after your HTML change.
- Check your total Docker disk usage by running:
docker system df
What does docker system df show?
- The Images row shows total image storage, including shared layers.
- The Containers row shows writable container layers for running and stopped containers.
- The Build Cache row shows cached intermediate layers from your builds.
Now compare the actual layer counts between your custom image and the base image it was built from.
- Count the layers in your custom image by running:
docker image inspect sre-dashboard:v1 --format='{{len .RootFS.Layers}}'
- Compare that to the base image layer count by running:
docker image inspect httpd:2.4.66 --format='{{len .RootFS.Layers}}'
Your custom image should have a few more layers than the base. Each RUN, COPY, and HEALTHCHECK instruction in your Dockerfile added one layer on top of the base.
Why do shared layers matter?
When multiple images share the same base layers, Docker stores those layers only once on disk. This is why pulling a second image with the same base is much faster than pulling the first one.
In production, teams standardize on a common base image so that deployments only transfer the few layers that changed. The bigger the shared foundation, the faster your builds and deploys.
You now understand how Docker builds images layer by layer and how the cache speeds up rebuilds. Next up, you will test restart policies, simulate failures, and debug containers like an SRE.
Container Lifecycle, Restart Policies, and Debugging
In the last step, you explored how Docker builds images layer by layer and how build cache speeds up rebuilds. Now it's time to face a production reality: containers crash.
In this step, you'll configure restart policies so Docker recovers from failures automatically. You'll also practice the exact debugging workflows SREs use to diagnose container problems in production.
In this step, get ready to:
- Test container restart policies to handle crashes automatically.
- Simulate and debug common container failures.
- Inspect container resource usage and clean up your environment.
Test restart policies
Docker restart policies tell the daemon what to do when a container's main process exits. Without a restart policy, a crashed container stays dead until someone manually restarts it.
- Stop and remove the current sre-dashboard container by running this command:
docker stop sre-dashboard && docker rm sre-dashboard
- Start a new container with the --restart=unless-stopped policy by running this command:
docker run -dit --name sre-dashboard --restart=unless-stopped -p 8080:80 sre-dashboard:v2
What does --restart=unless-stopped do?
This policy tells Docker to restart the container automatically whenever the main process exits. The one exception is when you explicitly stop the container with docker stop.
The always policy would also restart after an explicit stop when the Docker daemon restarts. That behavior can be surprising, which is why unless-stopped is the more common production choice.
- Simulate a crash by killing PID 1 inside the container:
docker exec sre-dashboard kill 1
- Check the container status immediately by running:
docker ps
The container should already be running again. Docker detected that PID 1 exited and restarted the container automatically.
- Confirm the restart count incremented by running:
docker inspect sre-dashboard --format='{{.RestartCount}}'
You should see a restart count of 1 or higher.
Container didn't restart?
- Make sure you included --restart=unless-stopped when you ran the container. Without this flag, Docker uses the default no restart policy and won't restart anything.
- If docker ps shows the container as exited, run docker logs sre-dashboard to check for errors in the container output.
- Why isn't my container restarting after I kill PID 1?
Simulate and debug common failures
Not every container failure is a crash. Sometimes containers refuse to start because of resource conflicts or bad configurations. Knowing how to diagnose these quickly is a core SRE skill.
The first failure you'll simulate is a port conflict, which is one of the most common deployment issues.
- Try running a second container on the same port by running this command:
docker run -dit --name conflict-test -p 8080:80 httpd:2.4.66
Docker refuses with a "port is already allocated" error. Port 8080 is already bound to your sre-dashboard container, so no other container can claim it.
Why do port conflicts happen?
Each host port can only be bound to one container at a time. If you need to run two Apache containers simultaneously, you'd map them to different host ports (e.g. -p 8081:80 for the second container).
- Clean up the failed container by running:
docker rm conflict-test
The second failure you'll simulate is a container that exits immediately because its command doesn't exist.
- Run a container with a nonexistent command:
docker run --name bad-cmd httpd:2.4.66 /nonexistent
The container exits immediately. Now use three debugging commands to figure out what went wrong.
- Check all containers, including exited ones, by running:
docker ps -a
The bad-cmd container shows a status of Exited because the command /nonexistent doesn't exist inside the image.
- Check the error logs by running:
docker logs bad-cmd
- Get the exit code to confirm the failure reason:
docker inspect bad-cmd --format='{{.State.ExitCode}}'
What do exit codes tell you?
A non-zero exit code means the process failed. Exit code 127 specifically means "command not found." This is the same convention used by Linux shells.
The debugging pattern you just used works for any container that won't start: check docker ps -a for the status, docker logs for error output, and docker inspect for the exit code.
- Clean up the failed container by running:
docker rm bad-cmd
Clean up and inspect resource usage
Before wrapping up, take a look at how your running container uses system resources. These commands are your first observability tools for containers.
- View real-time CPU, memory, network, and disk I/O for your container by running:
docker stats sre-dashboard --no-stream
What does docker stats show?
The docker stats command shows a live snapshot of container resource consumption. The --no-stream flag gives you a single snapshot instead of a continuously updating display.
In production, you'd feed these metrics into monitoring tools like Prometheus or Datadog for alerting and dashboards.
- View the processes running inside the container from the host's perspective by running:
docker top sre-dashboard
Compare this output with what you saw earlier when running ps aux from inside the container using docker exec. The process list is the same, but the PIDs differ because the host and container have separate PID namespaces.
- Stop and remove the sre-dashboard container by running:
docker stop sre-dashboard && docker rm sre-dashboard
- Check total Docker disk usage by running:
docker system df
This shows how much space images, containers, and build cache are consuming on your system.
- Remove dangling images from intermediate builds by running:
docker image prune -f
What are dangling images?
Dangling images are layers that no longer belong to any tagged image. They accumulate as you rebuild images with the same tag. The -f flag skips the confirmation prompt.
- Run docker system df again to confirm the reclaimed space.
docker system df
You should see the reclaimable space for images decrease after the prune.
Secret mission
Enable Apache mod_status for Real-Time Observability
Your dashboard is up and your health check is green. But what is Apache actually doing under the hood? Configure Apache's built-in mod_status module to expose real-time metrics on worker threads, request rates, and connection state.
Before you go
Clean Up Your Resources
This project runs entirely on your local machine using Docker, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker images: httpd:2.4.66, sre-dashboard:v1, sre-dashboard:v2.
- Docker containers: sre-dashboard (if still running).
- Project directory: ~/docker-fundamentals/ containing your Dockerfile, HTML files, and any config files.
- Build cache from intermediate image layers.
Keep everything running
No action needed. Choose this if you're still actively building or want to continue experimenting with your containers.
- Your Docker images and containers will remain available on your local machine.
- Any running containers will continue to use a small amount of CPU and memory.
- You can check what's running at any time with docker ps.
Pause - I'll come back to this later
Stop any running containers to free up memory. Your images and project files will remain on disk so you can pick up where you left off.
- Stop any running containers:
docker stop sre-dashboard
- Your images (sre-dashboard:v1, sre-dashboard:v2) and project files will stay on your machine.
- To restart later, run docker start sre-dashboard or rebuild from your Dockerfile.
Delete - I don't want to use this again
Remove all project containers, images, and files to start completely fresh.
- Stop and remove all project containers:
docker stop sre-dashboard 2>/dev/null
docker rm sre-dashboard stock-apache conflict-test bad-cmd 2>/dev/null
- Remove the custom images:
docker rmi sre-dashboard:v1 sre-dashboard:v2 httpd:2.4.66
- Clean up any dangling images and build cache:
docker image prune -f
docker builder prune -f
- Delete the project directory:
rm -rf ~/docker-fundamentals
- Verify everything is cleaned up by running docker system df to confirm space has been reclaimed.
Mission accomplished
That's a Wrap!
Nice work! You just built, ran, debugged, and managed a custom Apache2 container from scratch. That is the exact workflow SREs use every day to ship and troubleshoot containerized services in production.
You've learned how to:
- Build a custom Docker image with a Dockerfile, including a production-grade HEALTHCHECK and proper metadata labels.
- Debug and inspect running containers using docker exec, docker logs, docker inspect, and docker history.
- Manage the full container lifecycle including restart policies, PID 1 signal handling, build cache invalidation, and image layer optimization.
- Enable Apache mod_status for real-time observability into your running container via a /server-status endpoint.
Ready to quiz yourself?