Containerize a Python Web App with Docker
Package a Python web app into a portable Docker container image.
Introduction
30 Second Summary
Every developer has heard "it works on my machine." The fix is packaging your app with everything it needs into a single portable unit that runs identically everywhere.
In this project, you will containerize a Python web application using Docker. You'll write the app, create a Dockerfile, build an image, and run it as a container you can visit in your browser.
What You'll Build
A containerized Python web application that displays system information to demonstrate how containers create isolated environments.
By the end of this project, you'll have:
- A running Docker container serving a Python web app accessible at http://localhost:8000 in your browser.
- A custom Docker image built from a Dockerfile you wrote from scratch.
- Hands-on experience with the core Docker commands used daily by developers and DevOps engineers.
- Secret Mission: Add a second service with Docker Compose to run your app alongside a Redis container and display a visitor counter.
Are there any prerequisites?
You should be comfortable navigating a terminal (running commands, changing directories). A basic understanding of what a web server does is helpful but not required.
Install Docker Engine
Before you can containerize anything, you need the engine that makes containers possible.
Docker Engine is the core runtime that builds and runs containers on Linux. In this step, you'll install it directly from Docker's official repository so you always get the latest, supported version.
In this step, get ready to:
- Set up Docker's official apt repository on your system.
- Install Docker Engine and its companion packages.
- Verify the installation and configure non-root access.
Set up Docker's apt repository
Linux package managers need to know where to find Docker's packages. You'll add Docker's official GPG key (to verify downloads are authentic) and register their repository as a package source.
- Open your terminal.
- Add Docker's official GPG key and repository to your system by running these commands:
# Add Docker's official GPG key:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
What does this code do?
The first section downloads Docker's GPG key to /etc/apt/keyrings/docker.asc. This key lets your system verify that any packages downloaded from Docker's servers are authentic and untampered.
The second section creates a new file at /etc/apt/sources.list.d/docker.sources that tells apt where to find Docker's package repository. The final sudo apt update refreshes the package list so your system knows about Docker's packages.
- When prompted for your password, enter it. You won't see characters as you type.
- Wait for all commands to finish. You should see the final apt update output listing Docker's repository as a source.
Seeing a permission denied error?
Make sure each command that starts with sudo includes that prefix. Without sudo, the commands don't have the permissions needed to write to system directories.
Help me fix a permission denied error when setting up Docker's apt repository.
Install Docker Engine packages
Now that your system knows where to find Docker, you can install the engine and its companion tools in a single command.
- Install Docker Engine and all required packages by running:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
What are these packages?
- docker-ce is the Docker Engine itself (the daemon that runs containers).
- docker-ce-cli provides the docker command you'll use throughout this project.
- containerd.io is the low-level container runtime that Docker delegates to.
- docker-buildx-plugin adds enhanced build capabilities.
- docker-compose-plugin enables multi-container orchestration (you'll use this in the Secret Mission).
- When asked Do you want to continue? [Y/n], type Y and press **Enter**.
- Wait for the installation to complete. You'll see a stream of download and install progress messages.
Package not found?
If apt says it can't find docker-ce, the repository setup from the previous substep didn't complete successfully. Run sudo apt update again and check for errors referencing Docker's repository URL.
Help me fix apt not finding docker-ce packages.
Verify the installation and configure non-root access
Docker is installed, but you need to confirm it works. You'll also set up your user account so you don't need to type sudo before every Docker command.
- Verify Docker is working by running the official test image:
sudo docker run hello-world
- You should see a message that includes Hello from Docker! followed by an explanation of what just happened. This confirms Docker can pull images and run containers.
What just happened?
Docker downloaded a tiny test image from Docker Hub, created a container from it, ran the container (which printed the greeting), and then the container exited. This single command exercises the entire Docker pipeline: image pull, container creation, and execution.
- Now configure your user to run Docker without sudo by adding yourself to the docker group. Run these commands:
sudo usermod -aG docker $USER
newgrp docker
What does this do?
sudo usermod -aG docker $USER adds your current user to the docker group. Members of this group can communicate with the Docker daemon without root privileges.
newgrp docker refreshes your current terminal session so the group membership takes effect immediately (instead of requiring a full logout and login).
- Confirm non-root access works by running the hello-world container without sudo:
docker run hello-world
- You should see the same Hello from Docker! message as before. This time it ran without sudo, proving your group membership is active.
Still getting permission denied without sudo?
If newgrp docker didn't work, try logging out of your system completely and logging back in. Some environments require a full session restart for group changes to take effect.
Help me fix Docker permission denied errors after adding my user to the docker group.
Docker Engine is installed and ready to go. Next up, you'll run your first real container and write the Python app you'll containerize.
Run Your First Container and Create the App
Docker is installed and ready to use. Before you build your own container image, it helps to see one in action.
In this step, you'll run a pre-built container from Docker Hub, then write a Python web application that you'll containerize in the next step.
In this step, get ready to:
- Run a pre-built container to see Docker in action.
- Write a Python web application that displays system information.
- Test the app locally before containerizing it.
Run a pre-built container
Docker Hub hosts thousands of ready-made container images. You'll pull and run one now to see the full Docker workflow firsthand.
- Run the docker/welcome-to-docker container by running this command:
docker run -d -p 8080:80 docker/welcome-to-docker
What does this command do?
- docker run creates and starts a new container from an image.
- -d runs the container in the background (detached mode) so your terminal stays free.
- -p 8080:80 maps port 8080 on your machine to port 80 inside the container. This is called port mapping.
- docker/welcome-to-docker is the image name hosted on Docker Hub.
- Open your browser and visit http://localhost:8080.
- You should see a welcome page served from inside the container.
Now explore what Docker created behind the scenes.
- List all running containers by running this command:
docker ps
You should see a table with one row showing the docker/welcome-to-docker image, a status of "Up", and port 8080->80/tcp in the PORTS column.
- List all images Docker has downloaded by running this command:
docker images
You should see docker/welcome-to-docker listed with its tag, image ID, and size.
Images vs containers
An image is a read-only template containing everything needed to run an application. A container is a running instance of that image.
Think of an image like a recipe and a container like the dish you cook from it. You can cook multiple dishes from the same recipe.
Don't see the welcome page?
- Make sure you typed port 8080 in the browser URL, not 80.
- Run docker ps to confirm the container is listed as "Up". If it exited, re-run the docker run command.
Help me troubleshoot my docker/welcome-to-docker container not loading in the browser.
- Stop the welcome container by running this command:
docker stop $(docker ps -q --filter ancestor=docker/welcome-to-docker)
This frees up the terminal and stops the background process so it doesn't interfere later.
Create a project directory and write the web app
You need a simple application to containerize. You'll write a Python web server that displays system information about its host environment.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir docker-demo
cd docker-demo
- Confirm the folder was created by running:
ls ~/Desktop
You should see docker-demo listed in the output.
- Create and open app.py in the nano text editor by running:
nano app.py
- Paste the following code into nano. This is the imports and request handler for your web server:
import socket
import platform
from http.server import HTTPServer, BaseHTTPRequestHandler
# Handle incoming web requests
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Gather system info to display
host = socket.gethostname()
py_version = platform.python_version()
# Build an HTML page with system details
body = f"""<!DOCTYPE html>
<html>
<head><title>Docker Demo</title></head>
<body>
<h1>Hello from Docker!</h1>
<p><strong>Hostname:</strong> {host}</p>
<p><strong>Python Version:</strong> {py_version}</p>
<p><strong>Platform:</strong> {platform.platform()}</p>
<p>This page is served from inside a container.</p>
</body>
</html>"""
# Send the response back to the browser
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(body.encode())
What does this code do?
- MyHandler is a class that responds to HTTP GET requests by building an HTML page.
- socket.gethostname() fetches the machine's hostname. When running inside a container, this shows the container's unique ID.
- platform.python_version() and platform.platform() reveal what Python version and OS the code is running on.
- Below the class you just pasted, add this code to start the server:
# Start the web server on port 8000
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8000), MyHandler)
print("Server running on port 8000...")
server.serve_forever()
What does the main block do?
- HTTPServer(("0.0.0.0", 8000), MyHandler) creates a server that listens on all network interfaces at port 8000.
- "0.0.0.0" means the server accepts connections from any IP address. This is important for containers, which need to be reachable from outside their isolated network.
- serve_forever() keeps the server running until you manually stop it.
- Save the file by pressing Ctrl+O, then Enter to confirm.
- Exit nano by pressing Ctrl+X.
Seeing a syntax error when saving?
- Make sure the triple-quoted f-string uses three double quotes f""" at the start and """ at the end.
- Check that the indentation inside the class uses consistent spaces (4 spaces per level).
Help me fix a syntax error in my app.py file.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and exited nano before continuing.
ⓧ I'd like to double check the full code
Here is the complete app.py file. Compare it against what you have:
import socket
import platform
from http.server import HTTPServer, BaseHTTPRequestHandler
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
host = socket.gethostname()
py_version = platform.python_version()
body = f"""<!DOCTYPE html>
<html>
<head><title>Docker Demo</title></head>
<body>
<h1>Hello from Docker!</h1>
<p><strong>Hostname:</strong> {host}</p>
<p><strong>Python Version:</strong> {py_version}</p>
<p><strong>Platform:</strong> {platform.platform()}</p>
<p>This page is served from inside a container.</p>
</body>
</html>"""
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(body.encode())
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 8000), MyHandler)
print("Server running on port 8000...")
server.serve_forever()
Test the app locally
Before containerizing the app, confirm it works on your machine. This way you know any issues later are Docker-related, not code-related.
- Start the Python server by running this command:
python3 app.py
You should see Server running on port 8000... printed in your terminal.
- Open a browser and visit http://localhost:8000.
You should see an HTML page displaying your machine's hostname, Python version, and platform information.
Why test locally first?
When you eventually run this inside a container, the hostname will change to the container's ID and the platform will show the container's OS. Testing locally gives you a baseline to compare against.
- Stop the server by pressing Ctrl+C in the terminal.
Page not loading at localhost:8000?
- Make sure you stopped the welcome-to-docker container from earlier. It uses a different port, but confirm nothing else is using port 8000.
- Check your terminal for error messages. If you see Address already in use, another process is occupying port 8000.
- Verify the server printed Server running on port 8000... without any traceback errors above it.
Help me troubleshoot why my Python server isn't loading in the browser.
Your app runs locally. Next up, you'll write a Dockerfile to package it into a portable image that runs identically on any machine.
Write Your Dockerfile and Build the Image
Your Python app works on your machine. The goal now is to make it work the same way on any machine. That is exactly what a Docker image solves.
A Dockerfile is a recipe that tells Docker how to assemble an image. Each instruction in the file creates a layer, and Docker stacks those layers to produce a portable image you can run anywhere.
In this step, get ready to:
- Create a Dockerfile that packages your Python app.
- Build a custom Docker image tagged my-web-app:1.0.
- Inspect the image to see its layers and metadata.
Create a Dockerfile for your app
A Dockerfile is a plain text file with instructions that tell Docker how to assemble your image. Each instruction adds a new layer on top of the previous one.
- Create a new file called Dockerfile in your project directory by running this command:
nano Dockerfile
- Paste the following content into the file:
# Use the official Python slim image as the base
FROM python:3.12-slim
# Set the working directory inside the container
WORKDIR /app
# Copy the application file into the container
COPY app.py .
# Document the port the app listens on
EXPOSE 8000
# Set the default command to run the app
CMD ["python", "app.py"]
What does each instruction do?
- FROM python:3.12-slim specifies the base image your build extends. This is the official Python image, slim variant, without extra tooling.
- WORKDIR /app sets the working directory for all subsequent instructions. Any files you copy land here.
- COPY app.py . copies your application file from the host into the container image.
- EXPOSE 8000 documents that the container listens on port 8000. This is metadata only and does not publish the port.
- CMD ["python", "app.py"] sets the default command a container runs when started from this image.
- Save the file by pressing Ctrl+O, then Enter, then exit nano by pressing Ctrl+X.
- Confirm the file exists by running:
ls
You should see both app.py and Dockerfile listed in the output.
Don't see Dockerfile in the output?
- Make sure the filename is exactly Dockerfile with a capital D and no file extension. Docker looks for this exact name by default.
- Confirm you are in your project directory (the same folder where app.py lives).
Why isn't Docker finding my Dockerfile?
✔️ Awesome, I've got everything!
Great. Make sure you have saved Dockerfile before moving on.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile for comparison:
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
Build your Docker image
With the Dockerfile written, you can now build an image from it. The build command reads each instruction and executes it as a separate layer.
- Build your image and tag it my-web-app:1.0 by running this command:
docker build -t my-web-app:1.0 .
What does this command do?
- docker build tells Docker to build an image from a Dockerfile in the specified directory.
- -t my-web-app:1.0 tags the image with a name (my-web-app) and a version (1.0).
- . is the build context. It tells Docker to look for the Dockerfile and files in the current directory.
You should see Docker processing each layer. The first build takes longer because Docker needs to download the python:3.12-slim base image from Docker Hub.
Seeing a permission or file error?
- Make sure you are running the command from the same directory where your Dockerfile lives.
- Check that app.py is in the same directory. The COPY app.py . instruction expects to find it in the build context.
- If you see a network error while pulling the base image, check your internet connection and try again.
Help me fix my Docker build error.
Inspect the image layers and metadata
Now that your image is built, you can explore it to understand how Docker structured the layers and stored the configuration.
- List all images on your system by running:
docker images
You should see my-web-app listed with the tag 1.0, along with its size and creation time.
- Get detailed metadata about your image by running:
docker image inspect my-web-app:1.0
What does this output show?
The docker image inspect command displays detailed JSON information about the image. Look for these key sections:
- RootFS.Layers shows all the layers stacked to form your image. Each layer corresponds to an instruction in your Dockerfile.
- Config.Cmd shows the default command (python app.py) that runs when a container starts from this image.
- Config.ExposedPorts confirms port 8000 is documented in the image metadata.
Getting a 'No such image' error?
- Double check the image name and tag match exactly: my-web-app:1.0.
- Run docker images again to confirm the image is listed. If it is missing, re-run the build command from the previous substep.
Help me find my Docker image.
Your custom image is built and ready to go. Next up, you will run it as a container and see your app live in the browser.
Run and Manage Your Container
Your image is built and ready to go. The whole point of packaging an app into a Docker image is to run it as a container. A container is a live, running instance of that image.
In this step, you will start your container, explore what's happening inside it, and then see how Docker's layer caching makes rebuilds fast.
In this step, get ready to:
- Start a container from your image and view your web app in the browser.
- Explore the running container with logs, a shell session, and resource stats.
- Stop the container and rebuild with a code change to observe layer caching.
Start your container and view the app
The docker run command creates a live container from your image. You will map port 8000 inside the container to port 8000 on your host machine so your browser can reach the app.
- Start your container by running this command:
docker run --rm -d -p 8000:8000 --name my-app my-web-app:1.0
What do these flags mean?
- --rm automatically removes the container when it stops. No leftover containers to clean up.
- -d runs the container in detached mode (in the background), so you get your terminal back.
- -p 8000:8000 maps port 8000 on your host to port 8000 inside the container.
- --name my-app gives the container a friendly name instead of a random one.
- my-web-app:1.0 is the image you built in the previous step.
- Open your browser and visit http://localhost:8000.
You should see a page titled "Hello from Docker!" displaying a hostname (a short container ID), your Python version, and platform info.
Why is the hostname a random string?
The hostname displayed is the container's unique ID. Each container gets its own isolated network identity, separate from your host machine. This proves your app is running inside the container, not directly on your system.
Page not loading in the browser?
- Make sure the container is actually running. Run docker ps and check that my-app appears in the list.
- Check that nothing else is using port 8000. If another process has it, stop that process or change the host port (e.g., -p 9000:8000 then visit http://localhost:9000).
- Confirm you typed the URL with http:// and not https://.
Help me debug why my Docker container isn't reachable at localhost:8000.
Explore the running container
A running container is not a black box. Docker gives you several commands to peek inside and understand what's happening.
- View your container's log output by running this command:
docker logs my-app
You should see the "Server running on port 8000..." message, followed by HTTP request logs from your browser visit.
What does docker logs show?
The docker logs command retrieves everything your app printed to standard output (stdout) and standard error (stderr). It is the primary way to debug container issues in real-world deployments.
- Open an interactive shell inside the running container by running this command:
docker exec -it my-app sh
Your terminal prompt changes to something like # indicating you are now inside the container's filesystem.
What does docker exec do?
The docker exec command runs a new command inside an already-running container. The -it flags give you an interactive terminal. sh is the shell available in the slim Python image.
This is useful for debugging. You can inspect files, check environment variables, or test network connectivity from inside the container.
- While inside the container, list the files in the working directory:
ls
You should see app.py listed. This confirms the COPY instruction in your Dockerfile placed the file into /app inside the container.
- Exit the container shell by typing:
exit
You are back in your host machine's terminal now.
- View live resource usage for your container by running this command:
docker stats
You should see a live-updating table showing CPU usage, memory, and network I/O for all running containers. Press Ctrl+C to stop the live view and return to your terminal.
Why use docker stats?
In production, docker stats helps you spot containers that are consuming too much memory or CPU. It is a quick health check without needing external monitoring tools.
Stop the container and rebuild with a code change
Docker caches each layer of your image during a build. When you change only your application code, Docker reuses cached layers (like the base image download) and only rebuilds what changed. This makes subsequent builds much faster.
- Stop your running container by running this command:
docker stop my-app
Because you started the container with the --rm flag, Docker automatically removes it after stopping. You can confirm by running docker ps -a and seeing that my-app no longer appears.
- In app.py, find this line:
<h1>Hello from Docker!</h1>
- Change the heading text to something different, like:
<h1>Hello from my Docker container!</h1>
- Save app.py.
- Rebuild the image by running this command:
docker build -t my-web-app:1.0 .
Watch the build output closely. You should see Docker print "CACHED" for the early layers (like FROM and WORKDIR) and only re-run the COPY step. The build finishes in a fraction of a second compared to the first time.
What is layer caching?
Each instruction in a Dockerfile creates a layer. Docker stores these layers and reuses them on the next build if nothing has changed. Since your base image and WORKDIR did not change, only the COPY layer gets rebuilt.
This is why Dockerfiles are ordered with the least-changing instructions at the top (base image, dependencies) and the most-changing instructions at the bottom (application code). You will use this pattern in the Secret Mission when adding a requirements.txt layer.
Build not using cache?
- Make sure you are running the build command from the same directory as your Dockerfile. The . at the end tells Docker where to find it.
- If you accidentally changed the Dockerfile itself, Docker invalidates all layers from that point down. Only app.py should change for this test.
Why isn't Docker caching my layers during the rebuild?
Secret mission
Add Docker Compose with a Visitor Counter
Your app runs in a single container. Real-world applications often need multiple services working together. In this secret mission, you'll add a Redis database alongside your web app using Docker Compose, and display a live visitor counter on your page.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine. There are no cloud resources or ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Project directory on your Desktop (containing app.py, Dockerfile, compose.yaml, requirements.txt).
- Docker images stored locally (my-web-app:1.0, python:3.12-slim, redis:7-alpine, docker/welcome-to-docker).
- Docker Engine and its companion packages installed via apt.
Keep everything running
No action needed. Choose this if you plan to keep experimenting with Docker or building on this project.
- Docker Engine stays installed and ready for your next project.
- Your project files remain in the project directory on your Desktop.
- Downloaded images stay cached locally, which speeds up future builds.
Pause - I'll come back to this later
Free up disk space from unused images while keeping your project files intact.
- Remove unused Docker images by running:
docker image prune -a
- This removes all images not currently used by a running container. Your project files stay untouched, so you can rebuild the image later with docker build.
Delete - I don't want to use this again
Remove all project resources and Docker itself. Follow each step to fully clean up.
- Remove your project directory by running:
cd ~/Desktop
rm -rf docker-demo
- Remove all Docker images, containers, and volumes by running:
docker system prune -a --volumes
- Uninstall Docker Engine and its packages by running:
sudo apt purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
- Remove Docker's data directories by running:
sudo rm -rf /var/lib/docker
sudo rm -rf /var/lib/containerd
- Remove Docker's apt repository and GPG key by running:
sudo rm /etc/apt/sources.list.d/docker.sources
sudo rm /etc/apt/keyrings/docker.asc
Nice Work!
Nice Work!
You just containerized your first application with Docker. From installing the engine to running a multi-container stack, you now have the foundational skill behind modern deployment and cloud infrastructure.
You've learned how to:
- Wrote a Dockerfile from scratch and built a custom Docker image using python:3.12-slim as the base.
- Ran, inspected, and managed containers with core Docker CLI commands including docker logs, docker exec, and docker stats.
- Understood layer caching and how Docker optimizes rebuilds by reusing unchanged layers.
- Added Docker Compose to orchestrate a multi-container application with Redis, tracking page visitors across requests.
Ready to quiz yourself?