Containerize a Python Web App with Docker

Build and containerize a Python Flask app using Docker from scratch.

Introduction

30 Second Summary

Every developer eventually runs into this problem: your app works perfectly on your machine but breaks when someone else tries to run it. Docker solves this by packaging your app with everything it needs so it runs the same way everywhere.

In this project, you will containerize a small Python Flask web app using Docker. You will write every line of the Dockerfile yourself and explore how Docker packages and runs your code.

What You'll Build

A containerized Python Flask web app with a homepage and a health-check endpoint, built from a Dockerfile you write and understand line by line.

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

  • A working Docker image built from a Dockerfile you wrote yourself.
  • A running container serving a Python Flask web app you can access in your browser.
  • A clear mental model of how images, containers, layers, and port mapping work under the hood.
  • Secret Mission: Use Docker Compose to run your app alongside a Redis container with a compose.yaml file.

Are there any prerequisites?

This project assumes you have Docker Desktop installed and running on your machine. You should also have basic familiarity with Python and the command line.

Verify Docker Desktop and Create the Project Folder

Before writing any code, you need to confirm that Docker is installed and running on your machine. Docker will handle packaging and running your Flask app inside a container later in this project.

In this step, you'll verify your Docker installation, create a project folder, and run your first container to see the entire Docker workflow in action.

In this step, get ready to:
  • Confirm Docker Desktop is installed and running.
  • Create a project folder called docker-flask-app.
  • Run your first Docker container with hello-world.
Open a terminal and confirm Docker is running

Docker runs as a background service on your machine. You need to verify it's active before you can build or run anything.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check your Docker version by running this command:
docker --version

✔️ I see a version number

You're all set. The output shows your Docker version (for example, Docker version 29.4.1). This confirms Docker is installed on your machine.

ⓧ Command not found

Docker isn't installed yet. Download and install Docker Desktop from the official site.

  • Visit the Docker Desktop download page and download Docker Desktop for your operating system.
  • Run the installer and follow the prompts to complete the installation.
  • Restart your terminal after installation completes.
  • Run docker --version again to confirm it works.
  • Get more details about your Docker installation by running this command:
docker info

What does docker info tell you?

This command shows details about your Docker Engine, including how many containers and images exist on your machine. If you see a "Server" section with details like "Operating System" and "Total Memory," Docker Desktop is running correctly.

Seeing a 'Cannot connect to the Docker daemon' error?

This means Docker Desktop isn't running yet. Open Docker Desktop from your applications and wait for the whale icon to appear in your system tray (Windows) or menu bar (macOS). Then run docker info again.

Help me fix a 'Cannot connect to the Docker daemon' error.

Create the project folder

Every Docker project starts with a folder that holds your application code, dependencies, and Dockerfile. Docker uses this folder as the "build context" when creating an image.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir docker-flask-app
cd docker-flask-app
  • Confirm you're inside the new folder by running:
pwd

You should see a path ending in docker-flask-app, like /Users/yourname/Desktop/docker-flask-app or C:\Users\yourname\Desktop\docker-flask-app.

Don't see the folder?

Make sure you ran cd ~/Desktop before mkdir. If you see a "file already exists" error, the folder was already created from a previous attempt.

Delete it with rm -rf docker-flask-app (macOS/Linux) or Remove-Item -Recurse docker-flask-app (Windows), then run the mkdir and cd commands again.

I can't create the docker-flask-app folder on my Desktop.

Run the hello-world container

Docker has an official test image called hello-world that exists purely to verify your setup works end to end. Running it triggers the complete Docker workflow in miniature.

  • Run your first container by running this command:
docker run hello-world

You should see a message that starts with "Hello from Docker!" followed by an explanation of what just happened.

What just happened?

That single command triggered five steps. Docker checked your machine for the hello-world image and didn't find it locally. It then pulled the image from Docker Hub, which is Docker's public registry of pre-built images.

After downloading the image, Docker created a new container from it. It ran the container's default command (which prints the greeting), and then the container exited. This pull-create-run-exit cycle is the core Docker workflow you'll repeat throughout this project.

Seeing an error instead of the greeting?

If you see "Unable to find image" followed by a network error, check your internet connection. Docker needs to download the hello-world image from Docker Hub the first time you run it.

If you see "permission denied" or "Cannot connect to the Docker daemon," make sure Docker Desktop is running. Look for the Docker whale icon in your system tray (Windows) or menu bar (macOS).

I'm getting an error when running docker run hello-world.

Your Docker installation is verified and your project folder is ready. Next up, you'll build the Flask app that Docker will containerize.

Build the Flask App

Docker is running and your docker-flask-app folder is ready. Now you need an application to put inside a container.

In this step, you'll create a small Python web app using Flask. The app is intentionally simple because the real star of this project is Docker, not the web framework.

In this step, get ready to:
  • Create a Flask web app with a homepage and a health check endpoint.
  • Pin the Flask dependency in a requirements.txt file.
Create the Flask application

Flask is a lightweight Python library for building web apps. You'll create a small app with two routes that Docker will package into a portable image later.

  • Create a new file called app.py in your docker-flask-app folder using your text editor.
  • Add the Flask imports, app instance, and homepage route by pasting this code:
# Import Flask and its JSON helper
from flask import Flask, jsonify

# Create the Flask app instance
app = Flask(__name__)

# Homepage route - returns an HTML welcome message
@app.route("/")
def home():
    return "<h1>Hello from Docker!</h1><p>This app is running inside a container.</p>"

What does this code do?

  • Flask(__name__) creates a new Flask application. The __name__ variable tells Flask where to find your app's files.
  • @app.route("/") maps the root URL to the home() function. When someone visits your app in a browser, this is the page they see.
  • The function returns raw HTML that the browser renders as a heading and paragraph.
  • Save app.py.
  • Confirm the file was created by running:
ls

You should see app.py in the output.

Don't see app.py?

  • Make sure you created the file inside the docker-flask-app folder, not somewhere else on your system.
  • Check that the file is named exactly app.py with no extra extension like .txt.
  • Make sure your terminal is still in the docker-flask-app directory. Run pwd (macOS/Linux) or cd (Windows) to check.

Why can't I see app.py in my directory listing?

  • Back in app.py, add the health check endpoint and server startup code below your home() function by pasting this:
# Health check route - returns JSON so services can verify this app is running
@app.route("/health")
def health():
    return jsonify(status="healthy")

# Start the Flask server when this file is run directly
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

What does this code do?

  • @app.route("/health") creates a second endpoint at /health. It returns a JSON response using jsonify(). Monitoring tools and orchestrators use health endpoints like this to check whether an app is alive.
  • if __name__ == "__main__": makes sure the server only starts when you run app.py directly, not when it is imported as a module.
  • app.run(host="0.0.0.0", port=5000) starts the Flask development server on port 5000.

Why 0.0.0.0 instead of 127.0.0.1?

By default, Flask binds to 127.0.0.1. That means it only accepts connections from inside the same machine.

Inside a container, 127.0.0.1 means "inside the container only." Your browser lives on the host machine, outside the container. Using 0.0.0.0 tells Flask to listen on all network interfaces, which allows traffic from your host machine to reach the app through Docker's port mapping.

  • Save app.py.

You won't be able to run the app yet because Flask isn't installed locally. The Dockerfile you write in the next step handles installation inside the container.

✔️ Awesome, I've got everything!

Double check you've saved app.py before moving on.

ⓧ I'd like to double check the full code

Here is the complete app.py file. Make sure yours matches this exactly.

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/")
def home():
    return "<h1>Hello from Docker!</h1><p>This app is running inside a container.</p>"

@app.route("/health")
def health():
    return jsonify(status="healthy")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Pin your Flask dependency

Every Python project lists its dependencies in a requirements.txt file. Docker uses this file to install exactly the right packages inside the container.

  • Create a new file called requirements.txt in your docker-flask-app folder.
  • Pin the Flask dependency by pasting this:
flask==3.1.3

What is version pinning?

The == syntax tells pip to install exactly Flask version 3.1.3. Without pinning, pip grabs the latest version available.

If a new Flask release introduces a breaking change, your app could stop working. Pinned versions guarantee your container builds produce the same result every time, no matter when or where you build them.

  • Save requirements.txt.
  • Confirm both project files exist by running:
ls

You should see app.py and requirements.txt in the output.

Your Flask app is written and your dependency is pinned. Next, you'll write the Dockerfile that packages everything into a portable image.

Write the Dockerfile Line by Line

Your Flask app and dependency file are ready. Now it's time to write the instructions that package everything into a portable Docker image.

A Dockerfile is a text file that tells Docker how to build your image, one instruction at a time. Each instruction creates a layer, and the order of those layers matters for build speed.

In this step, get ready to:
  • Choose a base image that includes Python and Linux.
  • Install your app's dependencies in a cache-friendly order.
  • Configure the container's startup command.
Choose your base image

The first line of every Dockerfile is FROM. This instruction picks the starting image your container is built on top of.

  • Create a new file called Dockerfile inside your docker-flask-app folder. The file has no extension. It is literally just Dockerfile.
  • Add this as the first line:
FROM python:3.12-slim

What is a base image?

A base image is a pre-built environment that your image starts from. python:3.12-slim is an official Docker image with Python 3.12 pre-installed on Debian Linux.

The slim variant strips out extras like compilers and documentation, keeping the image around 150MB instead of 1GB. This single line gives you a complete Linux environment with Python ready to go.

  • Save Dockerfile.
  • Confirm the file was created by running:
ls

You should see Dockerfile listed alongside app.py and requirements.txt.

Don't see Dockerfile in the output?

  • On Windows, some text editors silently add a .txt extension. If you see Dockerfile.txt, rename it to remove the extension.
  • Make sure you saved the file inside the docker-flask-app folder, not in a parent or sibling directory.
  • My Dockerfile isn't showing up in the ls output. I'm on Windows using PowerShell.
Install dependencies before copying your code

The next block of instructions sets up a working directory and installs your Python packages. The order of these lines is the most important optimization in any Dockerfile.

  • In Dockerfile, add these lines below FROM python:3.12-slim:
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

Why install dependencies before copying the app code?

Docker caches each layer. If your app code changes but requirements.txt stays the same, Docker reuses the cached dependency layer and skips reinstalling packages entirely.

If you copied everything at once with a single COPY, changing even one line of app.py would force Docker to reinstall all your packages from scratch. This is called layer caching, and it is the single biggest Dockerfile optimization you can make.

Here is what each line does:

  • WORKDIR /app sets the working directory inside the container. Every instruction after this runs from /app.
  • COPY requirements.txt . copies only the dependency file into the image first.
  • RUN pip install --no-cache-dir -r requirements.txt installs your packages. The --no-cache-dir flag tells pip not to store downloaded files, keeping the image smaller.
  • COPY . . copies the rest of your project code into the image.
  • Save Dockerfile.

You won't see a visible change yet. These instructions take effect when you build the image in the next step.

Not sure the order is right?

  • The two COPY instructions must be separate. COPY requirements.txt . comes first, then RUN pip install, then COPY . ..
  • Make sure WORKDIR /app appears before any COPY or RUN instructions. It sets the directory everything else runs in.
  • Help me verify my Dockerfile instruction order is correct.
Set the startup command

The last two instructions tell Docker which port your app uses and what command to run when the container starts.

  • In Dockerfile, add these lines at the bottom, below COPY . .:
EXPOSE 5000

CMD ["python", "app.py"]

What do EXPOSE and CMD do?

  • EXPOSE 5000 is documentation only. It tells anyone reading the Dockerfile that the app listens on port 5000, but it does NOT actually open the port. You will do that with the -p flag when you run the container.
  • CMD ["python", "app.py"] sets the default command that runs when a container starts from this image. The square bracket format is called the "exec form" and is the recommended way to write it.
  • Save Dockerfile.

Seeing a syntax highlighting error?

  • Check the CMD line carefully. Each element must be a separate string in double quotes inside square brackets: ["python", "app.py"].
  • Make sure your file is still named Dockerfile with a capital D and no file extension.
  • My Dockerfile has an error and I'm not sure what's wrong.

✔️ Awesome, I've got everything!

Double check you've saved your Dockerfile and you're ready to build.

ⓧ I'd like to double check the full code

Here is the complete Dockerfile. Make sure yours matches exactly.

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Your Dockerfile is complete. Next up, you'll build it into a Docker image and watch each layer come together.

Build Your Docker Image and Explore Layers

Your Dockerfile is complete. It describes every layer of the environment your Flask app needs to run.

Now it's time to turn that Dockerfile into an actual Docker image you can run. Along the way, you'll see how Docker builds images in layers and caches them for faster rebuilds.

In this step, get ready to:
  • Build a Docker image from your Dockerfile.
  • Inspect the image's layers with docker history.
  • Observe layer caching by rebuilding after a code change.
Build your first Docker image
  • Build your Docker image by running this command:
docker build -t flask-app .

What does this command do?

  • docker build tells Docker to read your Dockerfile and create an image from it.
  • -t flask-app tags (names) your image flask-app so you can refer to it later.
  • . tells Docker to use the current directory as the build context. Docker sends every file in this directory to the Docker Engine so it can copy what the Dockerfile needs.
  • Docker executes each instruction in your Dockerfile in order. Each instruction creates a new layer in the image.
  • Watch the build output in your terminal. You should see Docker process each step of your Dockerfile, from pulling the base image to copying your app code.
  • The build is complete when you see your image name flask-app at the end of the output with no errors.

Build failed?

  • If you see Cannot connect to the Docker daemon, make sure Docker Desktop is running. Look for the Docker whale icon in your system tray.
  • If you see unable to prepare context or no such file, confirm you are inside the docker-flask-app folder and that your file is named exactly Dockerfile with no extension.
  • If the base image pull is slow or times out, check your internet connection. Docker needs to download python:3.12-slim from Docker Hub the first time you build.

Still stuck? Ask for help.

Inspect your image and its layers

Every instruction in your Dockerfile created a separate layer inside the image. Think of a Docker image as a stack of read-only layers, each one building on top of the previous one.

  • Confirm your image was created by running:
docker images

You should see flask-app in the list with the tag latest and its total image size.

  • Inspect the individual layers that make up your image by running:
docker history flask-app

You should see a table listing each layer. Each row corresponds to one instruction from your Dockerfile (plus layers from the base image).

What are image layers?

Each layer is a snapshot of the filesystem changes made by one instruction. FROM creates the bottom layer with the base operating system and Python. RUN pip install creates a layer with your installed packages.

Layers are read-only and stacked on top of each other. When you run a container from this image, Docker adds one thin writable layer on top for any changes the running app makes. The image layers underneath never change.

See layer caching in action

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 from scratch.

Let's test this. You'll make a small change to your app code and rebuild. Because requirements.txt did not change, Docker should skip reinstalling your dependencies entirely.

  • Open app.py in your text editor.
  • Find this line inside the home() function:
return "<h1>Hello from Docker!</h1><p>This app is running inside a container.</p>"
  • Change the text to anything you like. For example, replace it with:
return "<h1>Hello from Docker v2!</h1><p>This app has been updated.</p>"
  • Save app.py.
  • Rebuild your image by running the same build command:
docker build -t flask-app .

Watch the output carefully this time. You should see the word CACHED next to the early layers. The COPY . . layer and everything after it will rebuild.

Why were some layers cached?

Your requirements.txt did not change, so Docker skipped the COPY requirements.txt . and RUN pip install layers entirely. Those cached layers are reused as-is.

However, app.py changed. The COPY . . instruction detected new file contents, so Docker rebuilt that layer. Every layer after a changed layer must also rebuild.

This is exactly why the Dockerfile installs dependencies BEFORE copying the app code. Dependencies change rarely. App code changes often. Putting the rarely-changing instruction first means Docker can cache it and skip the slow pip install step on most rebuilds.

All layers rebuilt instead of showing CACHED?

  • Make sure you did not edit requirements.txt or the Dockerfile. Changing either of those files invalidates the dependency layers.
  • If Docker Desktop was restarted between builds, the build cache may have been cleared. Run the build twice in a row to see caching on the second run.

Need help understanding the build output?

Your image is built and you understand how Docker stacks and caches layers. Next up, you'll run this image as a live container and see your app in the browser.

Run Your Container and Understand Port Mapping

Your Docker image is built and you have seen exactly how its layers stack together. Now it is time to bring that image to life.

An image is a read-only blueprint. To actually run your app, you need to create a container from it. But how does traffic from your browser reach an app that is isolated inside a container? That is where port mapping comes in.

In this step, get ready to:
  • Run your Flask app inside a Docker container.
  • Inspect a running container using Docker commands.
  • Stop, remove, and re-run your container to learn the full container lifecycle.
Run your container with port mapping

Your Flask app listens on port 5000 inside the container. Your browser lives outside the container. Port mapping creates a tunnel between the two so your browser can reach the app.

  • Start your container by running this command:
docker run -d -p 5000:5000 --name my-flask-app flask-app

What do these flags mean?

  • -d runs the container in detached mode. It starts in the background so your terminal stays free.
  • -p 5000:5000 maps port 5000 on your machine (left side) to port 5000 inside the container (right side). This is the tunnel that lets your browser talk to the app.
  • --name my-flask-app gives the container a human-readable name instead of a random string like quirky_pasteur.
  • flask-app at the end is the name of the image you built in the previous step.

The -p flag is what actually publishes the port. The EXPOSE 5000 line in your Dockerfile is just documentation for humans. Without -p, your browser cannot reach the app no matter what EXPOSE says.

  • Open your browser and navigate to http://localhost:5000.
  • You should see a heading that says Hello from Docker! and a paragraph underneath that says This app is running inside a container..
  • Try the health endpoint too. Navigate to http://localhost:5000/health and you should see {"status":"healthy"}.

Don't see your app in the browser?

  • Make sure Docker Desktop is still running. The whale icon should be visible in your system tray.
  • If you see an error about port 5000 already being in use, another application is using that port. Close the conflicting app or choose a different host port (e.g. -p 8080:5000, then visit http://localhost:8080 instead).
  • If you see docker: Error response from daemon: Conflict. The container name "/my-flask-app" is already in use, a container with that name already exists. Remove it first with docker rm -f my-flask-app and try again.

Still stuck? Ask for help.

Inspect your running container

Your container is running in the background. Docker gives you several commands to peek inside and see what is happening without stopping anything.

  • List all running containers by running this command:
docker ps

You should see a table with one row for my-flask-app. The table shows the container ID, the image it was created from, how long it has been running, and the port mapping.

  • View the Flask server logs by running:
docker logs my-flask-app

You should see Flask's startup output, including the line Running on http://0.0.0.0:5000. If you visited the app in your browser, you will also see the HTTP request logs here.

Why 0.0.0.0 instead of 127.0.0.1?

In your app.py, you set host="0.0.0.0". This tells Flask to listen on all network interfaces inside the container.

If you used the default 127.0.0.1, Flask would only accept connections from inside the container itself. Your browser lives outside the container, so the -p port mapping would have nothing to forward to.

Now let's go inside the running container. The next command opens an interactive Linux shell inside the container. Your terminal prompt will change.

  • Open a shell inside your running container by running this command:
docker exec -it my-flask-app sh

What do -it and sh mean?

  • -i keeps the input stream open so you can type commands.
  • -t allocates a terminal so the output is formatted properly.
  • sh is the shell program to run. The slim base image includes sh but not the heavier bash.

Your prompt should now look something like / # or #. You are now inside the container's Linux environment.

  • Explore the container's filesystem by running these commands one at a time:
ls /app
python --version
cat /etc/os-release

What should I see?

  • ls /app shows your project files (app.py, requirements.txt, Dockerfile) copied into the container's /app directory.
  • python --version shows Python 3.12.x, matching the base image you chose in your Dockerfile.
  • cat /etc/os-release reveals the container is running Debian Linux. This is the operating system baked into the python:3.12-slim base image.
  • Leave the container shell by typing:
exit

Your terminal prompt returns to normal. The container keeps running in the background.

What is container isolation?

Each container has its own isolated filesystem, process space, and network. The files you saw inside the container are separate copies, not the originals on your machine.

If you deleted app.py inside the container, the original on your machine would be untouched. This isolation is what makes containers portable and predictable.

Can't get into the container?

  • If you see Error: No such container, your container may have stopped. Run docker ps to check. If it is not listed, re-run the docker run command from earlier in this substep.
  • Make sure you type the container name exactly as my-flask-app. Names are case-sensitive.

Need more help?

Stop, remove, and re-run your container

Containers are designed to be disposable. You can stop, remove, and recreate them at any time from the same image. Understanding this lifecycle is essential for working with Docker day to day.

  • Stop your running container by running this command:
docker stop my-flask-app
  • Confirm it stopped by running:
docker ps

Your container no longer appears in the list. It is not running.

  • Check if the stopped container still exists by running:
docker ps -a

Your container shows up here with a status of Exited. Stopped containers still exist on disk until you explicitly remove them.

What is the container lifecycle?

A Docker container goes through four states: created, running, stopped, and removed.

When you run docker run, Docker creates the container and immediately starts it. docker stop halts the process but keeps the container on disk. docker rm deletes it entirely.

  • Remove the stopped container by running:
docker rm my-flask-app
  • Confirm it is gone by running:
docker ps -a

The my-flask-app row is gone. The container has been fully removed. Your image still exists, so you can create a fresh container at any time.

  • Re-run the container from the same image by running:
docker run -d -p 5000:5000 --name my-flask-app flask-app
  • Open http://localhost:5000 in your browser again.
  • You should see the same Hello from Docker! page. A brand new container, created in seconds from the same image.

Container name already in use?

  • If you see an error about the name my-flask-app being in use, the previous container was not fully removed. Run docker rm -f my-flask-app to force-remove it, then try the docker run command again.
  • If port 5000 is in use, make sure you stopped the old container before starting a new one.

Still running into issues?

Your container is running and you have a handle on the full container lifecycle. Next up, you will add a .dockerignore file to keep your images clean and secure.

Add a .dockerignore and Rebuild

In the last step, you ran a container, explored its filesystem, and managed the full container lifecycle. Your app is live and serving traffic at http://localhost:5000.

Right now, your Dockerfile uses COPY . . to copy everything in your project folder into the image. That includes files Docker does not need, like Python bytecode, Git history, and secret environment files.

In this step, get ready to:
  • Create a .dockerignore file to exclude unnecessary files from your image.
  • Rebuild the image and check its size.
Create a .dockerignore file

A .dockerignore file tells Docker which files and folders to skip when building the image. It works just like a .gitignore file.

  • Create a new file called .dockerignore in your docker-flask-app folder and add the following content:
__pycache__
*.pyc
.git
.env
venv/

What does each line do?

  • __pycache__ and *.pyc are Python bytecode files. Python regenerates them automatically, so they are dead weight inside an image.
  • .git is your version control history. It has no use inside a container and can be surprisingly large.
  • .env often stores secrets like API keys. Excluding it prevents accidentally baking credentials into your image.
  • venv/ is a local Python virtual environment. The container already has its own Python installation, so a host virtual environment is unnecessary.
  • Save .dockerignore.
  • Confirm the file exists by running:
ls

You should see .dockerignore listed alongside app.py, requirements.txt, and Dockerfile.

Don't see .dockerignore in the list?

  • Check that the file name starts with a dot: .dockerignore, not dockerignore.
  • Make sure you created the file inside the docker-flask-app folder, not a parent or sibling directory.
  • If you are using WSL or a Linux-based terminal, run ls -a instead. Files starting with a dot are hidden by default on Linux.

Why isn't my .dockerignore file showing up?

✔️ Awesome, I've got everything!

Great. Double check you have saved your .dockerignore file before moving on.

ⓧ I'd like to double check the full code

Here is the complete .dockerignore file:

__pycache__
*.pyc
.git
.env
venv/
Rebuild the image and check its size

Now that your .dockerignore is in place, Docker will skip the excluded files the next time you build. Rebuild the image to put it into effect.

  • Rebuild your image by running:
docker build -t flask-app .

What should I see in the build output?

The build output looks similar to before. The key difference is invisible: Docker is no longer copying the excluded files into the build context.

You may notice the dependency layer says CACHED since requirements.txt has not changed. This is layer caching at work again.

  • Check your image size by running:
docker images

You should see flask-app listed with its size in the SIZE column. Take note of this number.

Why does image size matter?

Smaller images pull faster from registries like Docker Hub. They also deploy faster to servers.

Fewer files in the image also means a reduced attack surface. There are fewer things that could contain vulnerabilities.

Using .dockerignore is one of the easiest ways to trim image size. Choosing a slim base image like python:3.12-slim is the other.

Build failed?

  • Make sure you are running the command from inside the docker-flask-app folder. The . at the end of the command refers to the current directory as the build context.
  • Check that Docker Desktop is still running. If it was closed, reopen it and wait for the engine to start before trying again.
  • Make sure your Dockerfile has not been accidentally modified. It should still start with FROM python:3.12-slim.

Why is my Docker build failing after adding .dockerignore?

Secret mission

Add Docker Compose with Redis

Your Flask app runs in a container, but real-world apps rarely run alone. They need databases, caches, and other services running alongside them. In this Secret Mission, you will use Docker Compose to run your Flask app alongside a Redis container and connect them over a shared network.

Before you go

Clean Up Your Resources

This project runs entirely on your local machine using Docker Desktop, 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:

  • The docker-flask-app project folder containing app.py, requirements.txt, Dockerfile, and .dockerignore.
  • The flask-app Docker image.
  • The my-flask-app Docker container.
  • Docker Desktop installed on your machine.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project with the Secret Mission.

  • Your Docker image and container stay on your machine, ready to use.
  • Docker Desktop continues running in the background.
  • Your project folder and all files remain on your Desktop.

Pause - I'll come back to this later

Shut down running containers and quit Docker Desktop to free up memory. Your files and images stay on your machine for when you come back.

  • Stop any running containers by opening your terminal and running:
docker stop my-flask-app
  • Quit Docker Desktop by right-clicking the Docker icon in your system tray and selecting Quit Docker Desktop.
  • Your project folder, Docker image, and stopped container all remain on your machine.
  • When you are ready to resume, open Docker Desktop and run docker start my-flask-app to pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your container, image, and project files.

  • Stop and remove the container by running:
docker stop my-flask-app
docker rm my-flask-app
  • Remove the Docker image by running:
docker rmi flask-app
  • Delete the project folder by running:
cd ~/Desktop
Remove-Item -Recurse -Force docker-flask-app
  • If you also want to remove Docker Desktop, open Settings, select Apps, then Installed apps, and uninstall Docker Desktop.

Mission accomplished

That's a Wrap!

Nice work! You just containerized a Python web app from scratch and saw exactly how Docker works under the hood.

You've learned how to:

  • Write a Dockerfile from scratch and understand what every instruction does, from FROM to CMD.
  • Build Docker images, understand them as stacks of read-only layers, and use layer caching to speed up rebuilds.
  • Run containers with port mapping, inspect logs, shell into a running container, and manage the full container lifecycle.
  • Use Docker Compose to run a multi-container app with Flask and Redis.

Ready to quiz yourself?