Containerize a Python Flask App

Package a Flask web app into a Docker container and run it locally.

Introduction

30 Second Summary

Have you ever installed an app that worked perfectly on one computer but broke on another? That's the exact problem Docker solves for software teams everywhere.

In this project, you will build a Python web application from scratch and package it inside a Docker container. By the end, you'll have a running app that works the same way on any machine.

What You'll Build

A Python Flask web application packaged into a Docker container, accessible from your browser at localhost.

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

  • A Flask app with a home page and a JSON health endpoint running inside a Docker container.
  • A hand-written Dockerfile that uses layer caching to speed up rebuilds.
  • Hands-on experience with core Docker commands like build, run, ps, stop, and logs.
  • Secret Mission: Add a production-grade Gunicorn server and run it side-by-side with your development container.

Are there any prerequisites?

None. This project includes full setup instructions for Docker Desktop and all other tools you need. You don't need any prior Docker or Python experience.

Install Docker Desktop

Before you can package anything into a container, you need Docker running on your machine.

In this step, you'll install Docker Desktop, verify it works by running a test container, and create the project folder where your Flask app will live.

In this step, get ready to:
  • Install Docker Desktop on your Mac.
  • Verify Docker works by running a test container.
  • Create your project folder and open a text editor.
Download and install Docker Desktop

Docker Desktop is a free application that runs the Docker engine on your Mac. It gives you everything you need to build and run containers locally.

  • Open your browser and go to the Docker Desktop download page.
  • Click the download button for Mac (choose Apple Silicon or Intel depending on your chip).
  • Open the downloaded .dmg file.
  • Drag the Docker icon into your Applications folder.
  • Press Cmd+Space to open Spotlight Search.
  • Type Docker and press Enter to launch Docker Desktop.
  • Accept the service agreement when prompted.

Docker Desktop takes a moment to start up. Wait until you see the Docker whale icon in your menu bar at the top of the screen.

Is Docker Desktop free?

Docker Desktop is free for personal use, education, and small businesses (fewer than 250 employees and under $10M revenue). You won't be charged anything for this project.

Verify Docker is running

With Docker Desktop running, you can now use Docker commands in your Terminal. Let's confirm everything is working.

  • Press Cmd+Space to open Spotlight Search.
  • Type Terminal and press Enter to open it.
  • Check your Docker version by running this command:
docker --version

✔️ I see a version number

You should see something like Docker version 27.x.x or similar. Docker is installed and ready.

ⓧ Command not found

If you see command not found: docker, Docker Desktop isn't running or didn't install correctly.

  • Check that the Docker whale icon appears in your menu bar at the top of the screen.
  • If you don't see it, open Docker Desktop again from your Applications folder.
  • Wait until the whale icon stops animating (this means Docker has finished starting up).
  • Try the command again.
  • Run your first container to verify Docker works end-to-end:
docker run hello-world

What does this command do?

This command downloads a tiny test image from Docker Hub and runs it as a container. If you see a message starting with "Hello from Docker!" then your entire Docker pipeline is working.

You should see output that includes "Hello from Docker!" followed by an explanation of what just happened.

Don't see the hello-world message?

Make sure Docker Desktop is fully started (the whale icon in your menu bar should be still, not animating).

If you see a permission error, try closing and reopening Terminal after Docker Desktop finishes starting.

help me troubleshoot docker run hello-world not working on my Mac

Set up your text editor and project folder

You'll need a text editor to write your Flask app and Dockerfile. VS Code, Sublime Text, or even nano in Terminal all work.

  • If you don't have a text editor installed, press Cmd+Space, type VS Code, and check if it appears. If not, download it from code.visualstudio.com.

Any text editor works

You don't need VS Code specifically. If you already have Sublime Text, Cursor, or another editor you like, use that instead. You can also use nano directly in Terminal if you prefer staying in the command line.

  • Create your project folder and move into it by running these commands:
mkdir ~/flask-docker
cd ~/flask-docker

What do these commands do?

The mkdir command creates a new folder called flask-docker in your home directory. The cd command moves your Terminal into that folder so all future commands run from there.

  • Confirm you're in the right place by running:
pwd

You should see /Users/your-username/flask-docker printed in the Terminal.

Seeing a different path?

If pwd shows a different folder, run cd ~/flask-docker again. The ~ character means your home directory.

help me navigate to my flask-docker project folder in Terminal

Your environment is ready. Next up, you'll create the Flask application that will run inside your container.

Create the Flask Application

Docker is installed and your project folder is ready. Now it's time to create the application that will run inside your container.

You'll build a simple Flask web app with two routes. One serves an HTML greeting page and the other returns JSON data for health checks. This gives Docker something meaningful to package and run.

In this step, get ready to:
  • Create a Flask web app with two routes.
  • Create a requirements.txt file listing your dependency.
  • Optionally test the app in your browser before containerizing.
Create the Flask application file

Flask is a lightweight Python web framework. Your app will have two routes: a home page for humans and a health endpoint for automated monitoring.

  • In your text editor, create a new file called app.py inside the flask-docker folder.
  • Add the following code to set up the Flask app and define the home page route:
# Import Flask framework and JSON response helper
from flask import Flask, jsonify

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


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

What does this code do?

from flask import Flask, jsonify brings in the Flask class (to create your app) and jsonify (to return JSON responses later).

app = Flask(__name__) creates your application instance. The __name__ variable tells Flask where to find your project files.

@app.route("/") maps the root URL to the home() function. When someone visits your app, Flask runs this function and returns the HTML string to their browser.

You won't be able to run the app yet. The next chunk adds the code that starts the server.

  • Below the home() function in app.py, add the health check route and the server startup code:
# Define a health check endpoint that returns JSON
@app.route("/health")
def health():
    return jsonify({"status": "healthy", "container": True})


# Start the server when this file is run directly
if __name__ == "__main__":
    # Bind to 0.0.0.0 so the app is accessible from outside the container
    app.run(host="0.0.0.0", port=5000)

What does this code do?

The /health route returns a JSON object with a status field. Health endpoints like this are standard in production apps for automated monitoring.

jsonify() converts a Python dictionary into a proper JSON response with the correct content-type header.

The host="0.0.0.0" setting is critical for Docker. Without it, Flask only listens on 127.0.0.1 (localhost inside the container), which means nothing outside the container can reach it. Binding to 0.0.0.0 opens it up to connections from the Docker network.

  • Save app.py.

You won't be able to run the app yet because Flask isn't installed in your local environment. We'll verify it works shortly.

Not sure where to save the file?

Make sure app.py is saved directly inside the flask-docker folder on your Desktop. It should not be nested in any subfolder.

help me confirm my app.py file is in the right location

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here's the complete app.py file. Make sure yours matches:

from flask import Flask, jsonify

app = Flask(__name__)


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


@app.route("/health")
def health():
    return jsonify({"status": "healthy", "container": True})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Create the requirements file

Python projects list their dependencies in a requirements.txt file. Docker will use this to install the exact packages your app needs inside the container.

  • Create a new file called requirements.txt in your flask-docker folder.
  • Add this single line to pin your Flask dependency:
Flask==3.1.3

Why pin the version?

Using Flask==3.1.3 instead of just Flask locks the exact version. This guarantees the same version installs today, tomorrow, and on any other machine. Unpinned dependencies can silently update and break your app.

  • Save requirements.txt.
  • Confirm both files exist by running this in your terminal:
ls

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

Only seeing one file?

Make sure both files are saved in the same flask-docker folder. If ls shows nothing, your terminal might be in the wrong directory. Run cd ~/Desktop/flask-docker to get back.

help me find my project files

Test the app locally (optional)

If you have Python 3 installed on your Mac, you can run the app locally to verify it works before containerizing. This step is completely optional since Docker will handle everything for you.

  • Install Flask locally by running:
pip3 install -r requirements.txt
  • Start the Flask development server by running:
python3 app.py

You should see output that includes Running on http://0.0.0.0:5000.

  • Open your browser and visit http://localhost:5000.

You should see a page with the heading "Hello from Docker!" and a short paragraph below it.

  • Go back to your terminal and press Ctrl+C to stop the server.

Don't worry if pip3 isn't found

If you see a "command not found" error for pip3, that's fine. You can skip this local test entirely. Docker will install Flask inside the container for you in the next step.

Your Flask app is ready to be containerized. Next up, you'll write the Dockerfile that tells Docker exactly how to package and run this application.

Write the Dockerfile

You have a working Flask app and its dependency list ready to go. Now you need a way to tell Docker exactly how to package everything into a portable image.

A Dockerfile is a text recipe that Docker reads line by line to build an image. Each line creates a layer, and the order of those layers matters for build speed. In this step, you will write a Dockerfile that installs your dependencies and configures the container to run your Flask app.

In this step, get ready to:
  • Create a Dockerfile with a Python base image and working directory.
  • Set up a layer-caching-friendly install sequence for your dependencies.
  • Define the port and startup command for the container.
Create the Dockerfile

A Dockerfile lives in the same folder as your project code. It has no file extension.

  • Create a new file called Dockerfile in your flask-docker folder by running this command:
touch Dockerfile
  • Open Dockerfile in your text editor and paste the following content:
# Use a slim Python image as the base
FROM python:3.13-slim-bookworm

# Set the working directory inside the container
WORKDIR /app

# Copy dependency list first (layer caching optimization)
COPY requirements.txt .
# Install dependencies without caching downloaded packages
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code
COPY . .

# Document the port the app listens on
EXPOSE 5000

# Start the Flask development server
CMD ["python", "app.py"]

What does this code do?

FROM python:3.13-slim-bookworm picks a lightweight Python image based on Debian as your starting point. This gives you Python and pip without the bulk of a full OS install.

WORKDIR /app creates an /app directory inside the container and makes it the default location for all following commands.

The two COPY instructions and the RUN instruction install your dependencies. The order here is deliberate and covered in the next substep.

CMD tells Docker what command to execute when the container starts.

  • Save Dockerfile.
  • Confirm the file exists by running:
ls

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

Don't see the Dockerfile?

Make sure you created the file with no extension. If your editor added .txt or .dockerfile, rename it to just Dockerfile (capital D, no dot after the name).

Also confirm you are in the flask-docker folder. Run pwd to check your current location.

help me fix my Dockerfile creation

Understand the layer caching strategy

The most important optimization in your Dockerfile is the order of the COPY instructions. This is worth understanding because it dramatically speeds up rebuilds.

Why copy requirements.txt separately?

Docker builds images in layers. Each instruction in your Dockerfile creates a new layer, and Docker caches layers that have not changed.

By copying requirements.txt and running pip install before copying the rest of your code, Docker can reuse the cached dependency layer whenever you change app.py. The slow pip install step only re-runs when requirements.txt itself changes.

If you copied everything at once with a single COPY . . before pip install, Docker would reinstall all dependencies every time you edit a single line of Python code.

Review EXPOSE and CMD

The last two lines of your Dockerfile handle runtime behavior.

What do EXPOSE and CMD do?

EXPOSE 5000 is documentation for anyone reading the Dockerfile. It signals which port the app listens on, but does not actually open the port. You open the port at runtime with the -p flag (covered in the next step).

CMD ["python", "app.py"] is the default command that runs when the container starts. It launches the Flask development server you wrote in the previous step.

✔️ Awesome, I've got everything!

Great. Double check you have saved your Dockerfile with no file extension.

ⓧ I'd like to double check the full code

FROM python:3.13-slim-bookworm

WORKDIR /app

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

COPY . .

EXPOSE 5000

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

Your Dockerfile is ready. Next up, you will build this recipe into a Docker image and run your first container.

Build and Run Your Container

Your Dockerfile is complete. Now it's time to transform your project into a Docker image and run it as a live container.

Before building, there's one more file to create. When Docker builds an image, it sends your entire project folder (the "build context") to the Docker engine. Without a filter, it includes files you don't need, like Python cache files or your local virtual environment.

In this step, get ready to:
  • Create a .dockerignore file to keep the build context lean.
  • Build your Flask app into a Docker image.
  • Run the image as a container and access your app in the browser.
Create a .dockerignore file

A .dockerignore file works like .gitignore. It tells Docker which files to exclude from the build context sent to the Docker engine.

  • Create a new file called .dockerignore in your flask-docker folder (the same folder as your Dockerfile).
  • Add the following content to .dockerignore:
__pycache__
*.pyc
.git
venv/

What does this file do?

  • __pycache__ and *.pyc exclude Python's compiled bytecode files. These get regenerated automatically and waste space in your image.
  • .git excludes your Git history. Your container doesn't need version control data.
  • venv/ excludes any local virtual environment. The container installs its own dependencies from requirements.txt.
  • Save .dockerignore.
  • Confirm the file exists by running:
ls -a

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

Don't see .dockerignore in the list?

Files that start with a dot are hidden by default. The -a flag in ls -a shows hidden files. If you used plain ls, you won't see it.

Also make sure you created the file in the flask-docker folder, not somewhere else on your system.

Help me find my .dockerignore file

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on to the build.

ⓧ I'd like to double check the full code

Here's the complete .dockerignore file:

__pycache__
*.pyc
.git
venv/
Build the Docker image

Building an image takes your Dockerfile instructions and executes them one by one, creating a layer for each step. The result is a portable package containing your app and all its dependencies.

  • Build your Docker image by running this command:
docker build -t flask-docker .

What does this command do?

  • docker build reads your Dockerfile and executes each instruction to create an image.
  • -t flask-docker tags (names) the image so you can reference it later without using a long ID.
  • . tells Docker to use the current directory as the build context. This is the folder Docker sends to its engine (minus anything in .dockerignore).

Watch the output in your terminal. You'll see numbered steps like [1/5], [2/5] as Docker processes each Dockerfile instruction into a layer.

The build is complete when you see a line containing naming to docker.io/library/flask-docker at the end of the output.

Seeing a build error?

  • If you see "Cannot connect to the Docker daemon", Docker Desktop isn't running. Open it from your Applications folder and wait for the whale icon to appear in your menu bar.
  • If you see "no such file or directory" for the Dockerfile, make sure your terminal is inside the flask-docker folder. Run pwd to check your current location.
  • If pip install fails during the build, check that your requirements.txt contains exactly Flask==3.1.3 with no typos.

Help me fix my Docker build error

Run the container and view your app

Your image is built. Now you'll launch it as a running container and access the Flask app in your browser.

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

What do these flags mean?

  • -d runs the container in detached mode (in the background), so your terminal stays free for other commands.
  • -p 5000:5000 maps port 5000 on your Mac to port 5000 inside the container. Without this, the container's network is isolated and you can't reach the app.
  • --name my-flask gives the container a friendly name so you can reference it easily instead of using a random ID.
  • flask-docker at the end is the image name you tagged during the build step.

The command outputs a long string of characters. That's your container's unique ID.

  • Open your browser and go to http://localhost:5000.

You should see a page with the heading "Hello from Docker!" and the text "This Flask app is running inside a container." Your app is now running inside a Docker container.

What is port mapping?

A Docker container has its own isolated network. Port mapping creates a tunnel from your Mac's port 5000 to the container's port 5000.

This is why Flask binds to 0.0.0.0 in app.py. If it only listened on 127.0.0.1 (localhost inside the container), the port mapping wouldn't be able to reach it.

Not seeing the page in your browser?

  • If you see "This site can't be reached", your container might not be running. Run docker ps to check if it appears in the list.
  • If you see "port is already allocated" when running the docker run command, another process is using port 5000. If you tested Flask locally earlier, make sure you stopped it with Ctrl+C.
  • If you see "name my-flask is already in use", a container with that name already exists. Remove it with docker rm my-flask and try again.

Help me get my container running

Your app is live inside a container. Next up, you'll learn how to inspect, rebuild, and manage your containers using Docker CLI commands.

Manage and Inspect Your Container

Your container is running in the background and serving your Flask app to the browser. But how do you check on it, read its output, or update it when your code changes?

Docker provides CLI commands to inspect running containers, view their logs, and rebuild images after edits. In this step, you'll practice the daily workflow of monitoring, updating, and cleaning up containers.

In this step, get ready to:
  • Inspect your running container with docker ps and docker logs.
  • Rebuild your image after a code change and observe layer caching in action.
  • Clean up containers and verify your image persists.
Check on your running container

Your container is running in detached mode, which means it's working silently in the background. You need commands to confirm it's alive and to read what it's printing to the console.

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

You should see one row showing your my-flask container with a status of Up and ports listed as 0.0.0.0:5000->5000/tcp.

  • View the Flask server's console output by running:
docker logs my-flask

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 earlier, you'll also see the HTTP request logs.

What is docker logs?

Containers capture everything printed to standard output (like print() statements and server logs). The docker logs command lets you read that captured output without entering the container.

Not seeing your container?

If docker ps shows no containers, your container may have exited. Run docker ps -a to see all containers (including stopped ones) and check the STATUS column for error clues.

help me figure out why my Docker container stopped

Rebuild your image after a code change

When you edit your source code, the running container still has the old version. You need to rebuild the image and launch a fresh container. This is where Docker's layer caching pays off. Since your dependencies haven't changed, Docker skips the pip install step entirely.

  • In app.py, find the home() function's return statement:
return "<h1>Hello from Docker!</h1><p>This Flask app is running inside a container.</p>"
  • Change the paragraph text so the line looks like this:
return "<h1>Hello from Docker!</h1><p>This app was rebuilt from an updated image.</p>"
  • Save app.py.
  • Rebuild the Docker image by running this command:
docker build -t flask-docker .

What should I see in the build output?

Watch the output carefully. You'll notice that the COPY requirements.txt and RUN pip install steps show CACHED next to them. Docker knows those layers haven't changed, so it reuses them instantly.

Only the COPY . . layer (which copies your updated app.py) runs fresh. This is the payoff of the ordering you set up in your Dockerfile.

  • Stop and remove the old container by running:
docker stop my-flask && docker rm my-flask
  • Launch a new container from the rebuilt image by running:
docker run -d -p 5000:5000 --name my-flask flask-docker
  • Open http://localhost:5000 in your browser.

You should see your updated message: "This app was rebuilt from an updated image." This confirms the new image is running.

Still seeing the old message?

Make sure you saved app.py before rebuilding. Docker copies files at build time, so unsaved changes won't be included.

Also try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to bypass the browser cache.

help me troubleshoot my Docker rebuild

Now that you've confirmed layer caching works, revert app.py back to the original greeting so the project stays consistent.

  • In app.py, find the line you just changed:
return "<h1>Hello from Docker!</h1><p>This app was rebuilt from an updated image.</p>"
  • Change it back to the original text so the line looks like this:
return "<h1>Hello from Docker!</h1><p>This Flask app is running inside a container.</p>"
  • Save app.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved app.py with the original greeting restored.

ⓧ I'd like to double check the full code

Here's what app.py should look like:

from flask import Flask, jsonify

app = Flask(__name__)


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


@app.route("/health")
def health():
    return jsonify({"status": "healthy", "container": True})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
Stop and remove your container

Containers consume memory and hold port bindings while running. When you're done working, you'll want to stop and remove them to free up resources.

  • Stop and remove your container by running:
docker stop my-flask && docker rm my-flask
  • Confirm the container is gone by running:
docker ps -a

You should see no rows for my-flask. The container is fully removed.

  • Check that your image still exists by running:
docker images

You should see flask-docker listed in the output. Your image is still saved locally and ready to launch new containers anytime.

What's the difference between images and containers?

An image is a read-only template (like a blueprint). A container is a running instance of that template. You can delete all your containers and still have the image ready to spin up fresh ones.

Seeing "No such container" when stopping?

This means the container was already stopped or removed. Run docker ps -a to check if it exists in a stopped state. If it's not listed at all, it's already gone and you can move on.

help me with docker stop and rm errors

Secret mission

Run with Gunicorn in Production Mode

Your Flask app is running in Docker, but the built-in development server is not meant for real traffic. In this secret mission, you will add a production-grade Gunicorn server in a separate Dockerfile and run both versions side-by-side to see the difference.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally using Docker Desktop, which is free for personal use. There are no ongoing cloud costs. Docker images and containers do take up disk space on your Mac.

Resources you used:

  • Docker Desktop (installed application).
  • Project folder flask-docker on your Desktop.
  • Docker image: flask-docker.
  • Docker container: my-flask.
  • Application files: app.py, requirements.txt, Dockerfile, .dockerignore.

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment more with Docker commands.

  • Your container continues running in the background.
  • You can access your app at http://localhost:5000 any time.
  • Docker Desktop stays in your menu bar and uses a small amount of memory.

Pause - I'll come back to this later

Stop running containers to free up memory, but keep your images and project files for later.

  • Stop and remove your container by running:
docker stop my-flask && docker rm my-flask
  • Quit Docker Desktop from the menu bar icon to free up system memory.
  • Your Docker image and project files stay on disk. When you come back, relaunch Docker Desktop and run your container again.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Stop and remove the container by running:
docker stop my-flask && docker rm my-flask
  • Remove the Docker image by running:
docker rmi flask-docker
  • Delete your project folder by running:
rm -rf ~/Desktop/flask-docker
  • If you no longer need Docker at all, open Finder, go to Applications, and drag Docker to the Trash.

Nice Work!

Nice Work!

You just containerized your first application with Docker. You took a Python Flask app from source code to a running container accessible in your browser.

You've learned how to:

  • Build a Flask web app from scratch with a /health JSON endpoint.
  • Write a Dockerfile using layer caching best practices to speed up rebuilds.
  • Build a Docker image, run it as a container with port mapping, and manage containers with CLI commands.
  • Secret Mission: Added a production-grade Gunicorn server in a separate Dockerfile and ran multiple containers side by side.

Ready to quiz yourself?