Containerize a Python Web App
Package a Flask app into a Docker container and run it in your browser.
Introduction
30 Second Summary
Have you ever had software work perfectly on your computer but break on someone else's? That's one of the most common problems in software development.
In this project, you will package a Python web application into a Docker container. You'll write the app from scratch, create a Dockerfile to package it, and run it as a portable container accessible from your browser.
What You'll Build
A containerized Python web app using Flask that serves a personal greeting page, built and run entirely with Docker on your local machine.
By the end of this project, you'll have:
- A working Dockerfile that packages a Python web application into a container image.
- A running container serving your app on localhost, accessible from your browser.
- A .dockerignore file and an optimized Dockerfile that leverages build cache for fast rebuilds.
- Secret Mission: Add a multi-stage build that produces a minimal production image.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need, including Docker Desktop. All you need is a Windows computer and a willingness to try something new.
Install Docker Desktop
In the real world, software gets shipped inside containers. A container packages your code, its dependencies, and its runtime environment into a single portable unit that runs identically on any machine.
To build and run containers on your machine, you need Docker Desktop. In this step, you will install Docker Desktop, verify it works, and create a project folder for the Flask app you will containerize.
In this step, get ready to:
- Install Docker Desktop on your machine.
- Verify Docker is running with a test command.
- Create a project folder for your Flask app.
Download and install Docker Desktop
Docker Desktop is the application that lets you build, run, and manage containers on Windows. It bundles the Docker engine, CLI tools, and a graphical dashboard into one installer.
- Open your browser and go to the Docker Desktop for Windows install page:
- https://docs.docker.com/desktop/setup/install/windows-install/
- Click the download button at the top of the page to download Docker Desktop Installer.exe.
- Double-click Docker Desktop Installer.exe to run the installer.
- The installer asks which installation mode you prefer. Choose per-user (no administrator privileges required).
- On the Configuration page, ensure Use WSL 2 instead of Hyper-V is selected.
- Click through to finish the installation.
What is WSL 2?
WSL 2 (Windows Subsystem for Linux 2) is a lightweight Linux kernel built into Windows. Docker Desktop uses it to run Linux containers efficiently without a full virtual machine.
- When installation completes, click Close.
- Press the Windows key to open your search bar.
- Type Docker Desktop and press Enter to launch it.
- Accept the Docker Subscription Service Agreement when prompted.
Is Docker Desktop free?
Docker Desktop is free for personal use, education, and small businesses (fewer than 250 employees and less than $10M annual revenue). You do not need to pay anything for this project.
Wait for Docker Desktop to finish starting up. You will see a green status indicator in the bottom-left corner of the Docker Desktop window when it is ready.
Verify Docker is running
With Docker Desktop running, you can use the docker command-line tool in any terminal. Let's confirm everything is working.
- Press the Windows key to open your search bar.
- Type Windows Terminal and press Enter to open it.
- Check that Docker is installed by running this command:
docker --version
✔️ I see a version number
You should see something like Docker version 29.5.3 (your exact number may differ). Docker is installed and ready.
ⓧ Command not found or an error
If the terminal does not recognize the command, try these fixes:
- Make sure Docker Desktop is running. Check your system tray for the Docker whale icon.
- Close your terminal and reopen it. Docker adds itself to your system PATH during installation, but an already-open terminal may not pick it up.
- If you still see an error, restart your computer and try again. Enabling WSL 2 for the first time may require a reboot.
- Test that Docker can pull and run images by running this command:
docker run hello-world
Docker downloads a small test image and runs it. You should see a message that starts with Hello from Docker! confirming your installation is working correctly.
Not seeing Hello from Docker?
- Make sure Docker Desktop has finished starting. Look for the green status indicator in the Docker Desktop window.
- Check your internet connection. The docker run hello-world command needs to download the image from Docker Hub.
- If you see a permissions error, make sure you chose the per-user installation mode. Restart Docker Desktop and try again.
Help me troubleshoot my Docker installation
Create a project folder
You need a dedicated folder to hold your Flask app and Dockerfile. Let's create one on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder by running this command:
mkdir docker-flask-app
- Confirm the folder was created by running:
ls
You should see docker-flask-app in the output.
- Move into the folder by running this command:
cd docker-flask-app
Don't see the folder?
- Make sure you ran cd ~/Desktop first. Without it, the folder may have been created in a different location.
- If ls shows a long list, scroll up to look for docker-flask-app in the output.
Help me find or create my project folder
Docker is installed, verified, and your project folder is ready. Next up, you will write a simple Flask web application to containerize.
Write the Flask Application
Docker is installed and ready to go. Now you need something to put inside a container.
In this step, you will write a small web application using Flask, a lightweight Python web framework. You will also create the supporting files that Docker needs to build a clean image.
In this step, get ready to:
- Create a Flask web application that serves a greeting page.
- Create a requirements file listing the project's dependency.
- Create a .dockerignore file to keep the image clean.
Create the application file
Flask lets you define web pages as Python functions. You will create a single-route app that returns an HTML greeting page when someone visits it in a browser.
- Press the Windows key to open your search bar.
- Type the name of your text editor (VS Code or Notepad++) and press Enter to open it.
- Open the docker-flask-app folder in your editor using File then Open Folder.
- Create a new file called app.py in the docker-flask-app folder and paste the following code:
# Import Flask to create our web application
from flask import Flask
# Create the Flask application instance
app = Flask(__name__)
# Define the home page route
@app.route("/")
def hello():
# Return an HTML page with a greeting message
return """
<html>
<head><title>Docker Greeter</title></head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1>Hello from Docker!</h1>
<p>This app is running inside a container.</p>
</body>
</html>
"""
# Run the app on all network interfaces, port 5000
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
This is a complete web application in just a few lines. Here is what each section does:
- app = Flask(__name__) creates a new Flask application. Flask uses this to know where to find your project files.
- @app.route("/") tells Flask to run the hello() function when someone visits the home page.
- app.run(host="0.0.0.0", port=5000) starts the web server. The 0.0.0.0 part means the app listens on all network interfaces, which is required for Docker containers to accept outside connections.
- Save app.py.
File not saving to the right place?
Make sure the file is saved directly inside the docker-flask-app folder, not in a subfolder. Check the file path shown in your editor's title bar or tab.
help me make sure my app.py is in the right folder
Next, you need a file that tells Docker which Python packages to install inside the container.
- Create a new file called requirements.txt in the same docker-flask-app folder and paste this single line:
flask==3.1.1
Why pin the version?
Writing flask==3.1.1 locks the exact version. This guarantees your container installs the same Flask version every time, no matter when you rebuild. Without pinning, a future rebuild could pull a newer version that breaks your app.
- Save requirements.txt.
- Switch back to your terminal and confirm both files exist by running:
ls
You should see app.py and requirements.txt listed in the output.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files, then continue below.
ⓧ I'd like to double check the full code
Here is the complete app.py for comparison:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return """
<html>
<head><title>Docker Greeter</title></head>
<body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
<h1>Hello from Docker!</h1>
<p>This app is running inside a container.</p>
</body>
</html>
"""
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
And here is requirements.txt:
flask==3.1.1
Create a .dockerignore file
When Docker builds an image, it sends every file in the project folder to the build engine. A .dockerignore file tells Docker which files to skip. This keeps your image small and avoids leaking sensitive files like .env.
- Create a new file called .dockerignore in the docker-flask-app folder and paste the following:
# Python bytecode cache
__pycache__
*.pyc
# Version control
.git
# Environment files with secrets
.env
# Virtual environment
.venv
What does each entry exclude?
- __pycache__ and *.pyc are compiled Python bytecode files. They are regenerated automatically and waste space in the image.
- .git is the version control history folder. It can be large and has no purpose inside a running container.
- .env often contains secrets like API keys. Excluding it prevents those secrets from being baked into the image.
- .venv is a local Python virtual environment. The container builds its own environment, so including yours would just add bloat.
- Save .dockerignore.
- Switch back to your terminal and confirm all three files exist by running:
ls
You should see app.py, requirements.txt, and .dockerignore in the output.
Don't see .dockerignore in the file list?
Files starting with a dot are sometimes hidden by default. In PowerShell, run ls -Force to show hidden files. If .dockerignore still does not appear, check that your editor did not add a .txt extension (the file must have no extension).
help me find my .dockerignore file
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved, then continue below.
ⓧ I'd like to double check the full code
Here is the complete .dockerignore for comparison:
__pycache__
*.pyc
.git
.env
.venv
Your application code is ready. Next up, you will write the Dockerfile that tells Docker how to package these files into a container image.
Write the Dockerfile
You have a Flask app and a dependency list ready to go. Now you need to give Docker a set of instructions for packaging everything into a container image.
A Dockerfile is a plain text file that Docker reads line by line to build an image. Each instruction creates a new layer, and the order of those layers directly affects how fast your builds run.
In this step, get ready to:
- Create a Dockerfile with build instructions for your Flask app.
- Understand how layer ordering speeds up rebuilds.
Create the Dockerfile
A Dockerfile tells Docker which base image to start from, what files to copy in, which commands to run, and how to launch your app. You will write one that installs your Flask dependency and runs app.py.
- In your text editor, create a new file called Dockerfile (no file extension) inside your docker-flask-app folder.
- Paste the following content into your Dockerfile:
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
What does each instruction do?
- FROM python:3.13-slim starts from a lightweight official Python image. The slim tag keeps the image small by excluding build tools you do not need.
- WORKDIR /app sets the directory inside the container where all subsequent commands run.
- COPY requirements.txt . copies only the dependency list into the container first. This is intentional for caching (explained below).
- RUN pip install --no-cache-dir -r requirements.txt installs Flask. The --no-cache-dir flag tells pip not to store downloaded packages, keeping the image smaller.
- COPY . . copies the rest of your project files (like app.py) into the container.
- EXPOSE 5000 documents that the container listens on port 5000. It does not publish the port by itself.
- CMD ["python", "app.py"] tells Docker what command to run when the container starts.
- Save Dockerfile in your text editor.
- Confirm the file exists by running this in your terminal:
ls
You should see Dockerfile listed alongside app.py, requirements.txt, and .dockerignore.
Don't see the Dockerfile?
- Make sure the file is named exactly Dockerfile with a capital D and no extension. Some editors add .txt automatically. If you see Dockerfile.txt, rename it to remove the extension.
- Check that you saved the file inside docker-flask-app, not in a parent or sibling folder.
- help me figure out why my Dockerfile isn't showing up
✔️ Awesome, I've got everything!
Great. Make sure you have saved your Dockerfile before moving on.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile for comparison:
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Understand the layer caching strategy
You might have noticed that requirements.txt is copied separately before the rest of the code. This split is deliberate and exists for performance.
Why copy requirements.txt first?
Docker caches each layer. If a layer's inputs have not changed, Docker reuses the cached version instead of re-running the instruction.
By copying requirements.txt and running pip install before copying app.py, Docker caches the install layer. When you later edit only your application code, Docker skips the entire install step on rebuild because requirements.txt did not change.
This makes rebuilds significantly faster. You will see this in action in the next step.
Your Dockerfile is ready. Next up, you will build an image from it and run your first container.
Build and Run the Container
Your Dockerfile is written and ready. Now it's time to turn those instructions into a real, running container you can open in your browser.
Docker's build command reads your Dockerfile line by line, executing each instruction and stacking layers into an image. Once the image exists, you can launch a container from it in seconds.
In this step, get ready to:
- Build a Docker image from your Dockerfile.
- Run a container and view your app in the browser.
- Practice container lifecycle commands (ps, logs, stop, rm).
Build the Docker image
The docker build command reads your Dockerfile and creates an image. The -t flag lets you give that image a human-readable name.
- Build your image by running this command:
docker build -t flask-greeter .
What does this command do?
docker build reads the Dockerfile in the current directory (the . at the end) and executes each instruction.
-t flask-greeter tags (names) the resulting image so you can reference it later without using a long ID hash.
Watch your terminal as Docker processes each step. You should see output for each layer being created, ending with a line that says the image was successfully tagged as flask-greeter.
Seeing an error about the Dockerfile not found?
- Make sure you are inside the docker-flask-app folder. Run cd docker-flask-app if needed.
- Check that your file is named exactly Dockerfile with a capital D and no file extension.
- Confirm Docker Desktop is running. Look for the whale icon in your system tray.
Help me debug my Docker build error.
Run the container
Your image is ready. Now you can spin up a container from it and access your Flask app through your browser using port mapping.
- Start a container from your image by running this command:
docker run -d -p 5000:5000 --name my-greeter flask-greeter
What do these flags mean?
- -d runs the container in detached mode (in the background), so your terminal stays free.
- -p 5000:5000 maps port 5000 on your machine to port 5000 inside the container. Without this, the app is unreachable from your browser.
- --name my-greeter gives the container a friendly name so you can reference it by name instead of a random ID.
- Open your browser and go to http://localhost:5000.
You should see a page with the heading "Hello from Docker!" and a message confirming the app is running inside a container.
Not seeing the page in your browser?
- Make sure you typed http://localhost:5000 (not https).
- Check that the container is running. Run docker ps and look for my-greeter in the list.
- If port 5000 is already in use by another program, stop that program first or choose a different host port (e.g. -p 5001:5000).
Help me figure out why my container isn't serving the page.
Manage the container
Now that your app is running, you need to know how to inspect, stop, and remove containers. These are the core lifecycle commands you will use constantly.
- View all running containers by running this command:
docker ps
You should see a table with your my-greeter container listed, showing its ID, image name, port mapping, and status.
- View the container's log output by running:
docker logs my-greeter
What are container logs?
Logs capture everything your application prints to the console. For Flask, this includes each HTTP request your browser made. This is how you debug issues without stepping inside the container.
- Stop the running container by running:
docker stop my-greeter
This gracefully shuts down the container. The container still exists on disk but is no longer running.
- Remove the stopped container by running:
docker rm my-greeter
Why stop then remove?
Docker separates stopping (halting the process) from removing (deleting the container record). A stopped container can be restarted later. Removing it frees the name and disk space permanently.
- Confirm the container is gone by running:
docker ps -a
The my-greeter container should no longer appear in the list.
Getting an error that the container name is already in use?
- This means a container with the name my-greeter already exists. Run docker rm my-greeter to remove it, then try again.
- If the container is still running, stop it first with docker stop my-greeter before removing.
Help me fix a container name conflict.
You have built an image, run a container, served your app to the browser, and cleaned up after yourself. Next up, you will make a code change and see how Docker's build cache makes rebuilds lightning fast.
Rebuild and Iterate
You have a working container image and you know how to run it. Now it's time to experience the real development workflow: editing code, rebuilding, and redeploying.
This is where Docker's build cache becomes your best friend. Because you ordered your Dockerfile layers strategically (dependencies first, code second), Docker skips the slow pip install step when only your application code changes.
In this step, get ready to:
- Edit your Flask app and rebuild the image to see caching in action.
- Run the updated container and verify the change in your browser.
- Clean up containers and images using Docker lifecycle commands.
Make a code change
To see caching in action, you need to change something in your application code without touching requirements.txt. This proves that Docker skips the dependency installation layer on rebuild.
- Open app.py in your text editor.
- Find the line that says:
<h1>Hello from Docker!</h1>
- Change the text inside the <h1> tag to anything you like. For example, change it to Hello from my container! or your own custom greeting.
- Save app.py.
Now rebuild the image to pick up your code change.
- Rebuild your Docker image by running this command:
docker build -t flask-greeter .
What should I see in the output?
Watch the build output carefully. You should see CACHED next to the steps that copy requirements.txt and run pip install.
Docker recognizes that requirements.txt has not changed, so it reuses the cached layer from your first build. Only the COPY . . step re-executes because your application code is different.
The rebuild should complete noticeably faster than your original build.
Not seeing CACHED in the output?
Make sure you did not edit requirements.txt. Any change to that file invalidates the cache for the pip install layer and all layers after it.
Also confirm you are running the build command from inside the docker-flask-app folder where your Dockerfile lives.
help me understand Docker build cache
Run the updated container
- Start a new container from your updated image by running this command:
docker run -d -p 5000:5000 --name my-greeter flask-greeter
What do these flags do?
- -d runs the container in detached mode (in the background).
- -p 5000:5000 maps port 5000 on your machine to port 5000 inside the container.
- --name my-greeter gives the container a friendly name so you can reference it easily.
- Open your browser and go to http://localhost:5000.
You should see your updated greeting message displayed on the page.
Still seeing the old message?
Try a hard refresh in your browser (Ctrl+Shift+R on Windows) to clear any cached page.
If you see a connection error, confirm the container is running by running docker ps in your terminal. If the container is not listed, it may have exited due to an error in your code change. Run docker logs my-greeter to check for Python errors.
help me fix my Docker container
Clean up images and containers
Docker containers and images stay on your machine until you explicitly remove them. Cleaning up after yourself keeps your system tidy and frees disk space.
- Stop the running container by running this command:
docker stop my-greeter
- Remove the stopped container by running:
docker rm my-greeter
- List all Docker images on your machine by running:
docker images
You should see flask-greeter in the list along with its size and creation timestamp.
- Remove the image by running:
docker rmi flask-greeter
You should see output confirming the image layers were deleted.
Why remove images?
Docker images can take up hundreds of megabytes each. Over time, old images accumulate and consume significant disk space. Running docker images regularly helps you spot images you no longer need.
Seeing a conflict error when removing the image?
If you get an error about the image being used by a container, make sure you stopped and removed the container first. Run docker ps -a to see all containers (including stopped ones) and remove any that reference flask-greeter.
help me remove a Docker image
Secret mission
Add a Multi-Stage Build for a Smaller Image
Your current Dockerfile works, but production images need to be lean and secure. In this secret mission, you will create a multi-stage Dockerfile that isolates the build step from the runtime step. The result is a cleaner image that runs as a non-root user.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers (my-greeter, flask-greeter-prod container).
- Docker images (flask-greeter, flask-greeter-prod, python:3.13-slim, hello-world).
- Project folder (docker-flask-app) with app.py, requirements.txt, .dockerignore, Dockerfile, and Dockerfile.prod.
- Docker Desktop (installed application).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue experimenting with Docker containers.
- Your containers, images, and project files remain on your machine.
- Docker Desktop stays installed and available from your Start menu.
- You can restart stopped containers any time with docker start my-greeter.
Pause - I'll come back to this later
Stop running containers to free up memory, but keep your files and images so you can pick up where you left off.
- Stop all running containers by running:
docker stop $(docker ps -q)
- Quit Docker Desktop by right-clicking the Docker icon in the system tray and selecting Quit Docker Desktop.
- When you are ready to come back, launch Docker Desktop again and your images and containers will still be there.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Stop and remove all project containers:
docker stop my-greeter 2>nul
docker rm my-greeter 2>nul
docker stop $(docker ps -q --filter ancestor=flask-greeter-prod) 2>nul
docker rm $(docker ps -aq --filter ancestor=flask-greeter-prod) 2>nul
- Remove all project images:
docker rmi flask-greeter flask-greeter-prod
docker rmi python:3.13-slim hello-world
- Navigate to the parent folder of your project and delete the project folder.
- On Windows (PowerShell):
cd ..
Remove-Item -Recurse -Force docker-flask-app
- On macOS or Linux:
cd ..
rm -rf docker-flask-app
- Uninstall Docker Desktop. Open Windows Settings, select Apps, find Docker Desktop in the list, and select Uninstall.
Nice Work!
Nice Work!
You just containerized a Python web application from scratch using Docker. Your app is packaged as a portable image that can run identically on any machine with Docker installed.
You've learned how to:
- Write a Dockerfile with proper layer ordering that leverages build cache for fast rebuilds.
- Build a Docker image and run it as a container accessible in your browser via port mapping.
- Manage the full container lifecycle with commands like docker run, docker stop, docker rm, and docker logs.
- Secret Mission: Created a multi-stage build with a non-root user for a leaner, more secure production image.
Ready to quiz yourself?