Containerize a Python Web App

Build a multi-container Flask and Redis app using Docker Compose.

Introduction

30 Second Summary

When you install an app on your phone, you tap one button and everything just works regardless of your device. Containers do the same thing for software by packaging everything an application needs so it runs the same way everywhere.

In this project, you will containerize a Python web application and a Redis database using Docker Compose. By the end, you will have a multi-container stack you can start, stop, and debug with single commands.

What You'll Build

A Flask web application with a Redis-powered hit counter, packaged into containers and orchestrated with Docker Compose.

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

  • A Dockerfile that packages a Flask app into a container image.
  • A compose.yaml file that wires together your web app and a Redis database with health checks and persistent storage.
  • Hands-on experience with core Docker commands for building, running, inspecting, and cleaning up containers.
  • Secret Mission: Add a visitor log page that displays timestamped entries stored in Redis.

Are there any prerequisites?

None. This project includes full setup instructions for every tool you need, including Docker Desktop. No prior Docker or Python experience is required.

Install Docker Desktop

Docker packages your application and all its dependencies into a single unit called a container. Containers run the same way on every machine, which means no more "it works on my computer" problems.

To build and run containers locally, you need Docker Desktop. In this step, you'll install Docker Desktop on your Windows machine and confirm it's ready to go.

In this step, get ready to:
  • Install Docker Desktop with the WSL 2 backend.
  • Accept the Docker Desktop subscription agreement.
  • Verify docker --version and docker compose version return successfully.
Download and install Docker Desktop

Docker Desktop is a desktop application that runs the Docker engine on your machine. It includes everything you need to build images and run containers with simple commands.

First, let's check whether Docker is already installed on your machine.

  • Press the Windows key to open your search bar.
  • Type Terminal and press Enter to open Windows Terminal.
  • Check if Docker is already installed by running this command:
docker --version

✔️ I see a version number

Docker is already installed on your machine. Make sure Docker Desktop is running by checking for the whale icon in your system tray (bottom-right of your screen). If the icon is stable and not animating, you're good to go.

Skip ahead to the verification substep below to confirm everything is working.

ⓧ I see an older version

You have an older version of Docker installed. Download the latest Docker Desktop installer to upgrade your installation in place.

  • Open the Docker Desktop for Windows install page in your browser.
  • 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.
  • Select Per-user installation (recommended). This does not require administrator privileges.
  • When prompted, ensure the Use WSL 2 instead of Hyper-V option is selected on the Configuration page.
  • Follow the remaining prompts and click Close when the installation finishes.

What is WSL 2?

WSL 2 (Windows Subsystem for Linux 2) is a lightweight Linux environment built into Windows. Docker Desktop uses it to run Linux containers on your Windows machine without needing a full virtual machine.

ⓧ Command not found

Docker is not installed yet. Let's download and install Docker Desktop.

  • Open the Docker Desktop for Windows install page in your browser.
  • 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.
  • Select Per-user installation (recommended). This does not require administrator privileges.
  • When prompted, ensure the Use WSL 2 instead of Hyper-V option is selected on the Configuration page.
  • Follow the remaining prompts and click Close when the installation finishes.

What is WSL 2?

WSL 2 (Windows Subsystem for Linux 2) is a lightweight Linux environment built into Windows. Docker Desktop uses it to run Linux containers on your Windows machine without needing a full virtual machine.

Installer won't run?

If Windows SmartScreen blocks the installer, click More info and then Run anyway. The installer is from Docker's official website and is safe to run.

If you see an error about virtualization, you may need to enable hardware virtualization in your BIOS/UEFI settings. Help me enable virtualization on my machine.

Open Docker Desktop and accept the subscription agreement

Docker Desktop does not start automatically after installation. You need to launch it manually.

  • Press the Windows key to open your search bar.
  • Type Docker and select Docker Desktop from the search results.
  • Wait for the Docker Desktop window to appear. The whale icon in your system tray (bottom-right corner) will animate while Docker starts up.
  • When the Docker Subscription Service Agreement appears, read the summary and click Accept to continue.

Once the whale icon in your system tray stops animating and stays still, Docker Desktop is fully running.

Is Docker Desktop free?

Docker Desktop is free for personal use, education, and small businesses (fewer than 250 employees AND less than $10 million in annual revenue). This project falls under personal use and education, so there is no cost.

Docker Desktop stuck on starting?

Give it up to two minutes on first launch. Docker needs to set up its internal Linux environment via WSL 2 the first time it runs.

If it stays stuck, try restarting your computer and opening Docker Desktop again. Help me troubleshoot Docker Desktop not starting on Windows.

Verify Docker is working

Now let's confirm that both the Docker CLI and Docker Compose are installed and accessible from your terminal.

  • Switch back to your Windows Terminal window.
  • Run these two commands to check your Docker installation:
docker --version
docker compose version

✔️ Both commands show version numbers

You should see output similar to this (your exact version numbers may differ):

Docker version 27.x.x, build xxxxxxx
Docker Compose version v2.x.x

Both commands returned version information. Docker and Docker Compose are ready to use.

ⓧ One or both commands failed

If either command returns an error, Docker Desktop may not be fully running yet.

  • Check that the whale icon in your system tray is stable (not animating).
  • If the icon is still animating, wait for it to stop before trying the commands again.
  • If you don't see the whale icon at all, open Docker Desktop using the steps from the previous substep.
  • Close your terminal and open a new one, then try the commands again. The terminal needs to pick up the new PATH entries that Docker Desktop added during installation.

Help me fix Docker commands not working in my terminal.

What are these two commands checking?

docker --version confirms the Docker CLI (command-line interface) is installed and accessible from your terminal.

docker compose version confirms Docker Compose is available. Docker Compose is the tool you'll use throughout this project to define and run multi-container applications.

Your Docker environment is ready to go. Next up, you'll create the project files for your Flask web app and write your first Dockerfile.

Create the Project Files

Docker is installed and running. Now it's time to create the application you'll package into containers.

You'll write a Python web app that counts page visits, backed by a Redis database. Every file you create in this step will end up inside a Docker container.

In this step, get ready to:
  • Create a compose-demo project folder with a Flask web application.
  • Write a Dockerfile that packages the app into a container image.
  • Add configuration files for environment variables and build optimization.
Create the project folder and Flask app

Every Docker project starts with a folder that holds your application code and configuration. You'll create that folder on your Desktop so it's easy to find.

  • In your terminal, move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir compose-demo
cd compose-demo
  • Confirm the folder exists by running:
ls

You should see an empty listing since the folder is brand new.

Now you'll create the main application file. Flask is a lightweight Python web framework that makes it simple to serve web pages with just a few lines of code.

  • Open the file in Notepad by running:
notepad app.py

Seeing a prompt?

Notepad will ask if you want to create a new file since app.py doesn't exist yet. Click Yes to continue.

  • Paste the following code into Notepad:
import os
import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(
    host=os.getenv("REDIS_HOST", "redis"),
    port=int(os.getenv("REDIS_PORT", "6379")),
)


@app.route("/")
def hello():
    count = cache.incr("hits")
    return f"Hello from Docker! I have been seen {count} time(s).\n"

What does this code do?

This Flask app connects to a Redis database and increments a counter every time someone visits the page.

  • cache = redis.Redis(...) creates a connection to Redis using environment variables (with sensible defaults).
  • cache.incr("hits") adds 1 to the hits key in Redis every time the page loads.
  • The return string displays the running total of visits.
  • Save the file by pressing Ctrl+S.
  • Close Notepad.

File saved with a .txt extension?

If Notepad added .txt to the filename (making it app.py.txt), you likely hit Save As instead of Save. Delete the file, run notepad app.py again, paste the code, and press Ctrl+S to save directly.

help me fix my app.py file extension

✔️ Awesome, I've got everything!

Great. Make sure you've saved app.py and closed Notepad before continuing.

ⓧ I'd like to double check the full code

import os
import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(
    host=os.getenv("REDIS_HOST", "redis"),
    port=int(os.getenv("REDIS_PORT", "6379")),
)


@app.route("/")
def hello():
    count = cache.incr("hits")
    return f"Hello from Docker! I have been seen {count} time(s).\n"
Write the dependencies file and Dockerfile

Docker needs to know which Python packages your app requires. You'll list them in a requirements.txt file, then write a Dockerfile that tells Docker how to build your application image step by step.

  • Create the dependencies file by running:
notepad requirements.txt
  • Click Yes when Notepad asks to create the file.
  • Paste the following two lines:
flask
redis
  • Save with Ctrl+S and close Notepad.

What does this file do?

Each line names a Python package that Docker will install inside the container. The flask package runs the web server. The redis package lets Python talk to the Redis database.

Now for the Dockerfile. This file is the recipe Docker follows to build your application into a container image.

  • Create the Dockerfile by running:
notepad Dockerfile
  • Click Yes to create the file.
  • Paste the following code:
# syntax=docker/dockerfile:1
FROM python:3.12-alpine
WORKDIR /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["flask", "run", "--debug"]

What does this Dockerfile do?

Each line is an instruction that Docker executes in order to build your image:

  • FROM python:3.12-alpine starts from a lightweight Python base image.
  • WORKDIR /code sets the working directory inside the container.
  • RUN apk add --no-cache gcc musl-dev linux-headers installs system libraries that some Python packages need to compile.
  • COPY requirements.txt . and RUN pip install -r requirements.txt install your Python dependencies.
  • CMD ["flask", "run", "--debug"] tells Docker what command to run when the container starts.
  • Save with Ctrl+S and close Notepad.

Notepad adding .txt to the filename?

If your file is saved as Dockerfile.txt instead of Dockerfile, delete the file and try again. Running notepad Dockerfile from PowerShell should save it without an extension. If it still adds .txt, rename it in PowerShell:

Rename-Item Dockerfile.txt Dockerfile

help me fix my Dockerfile name

✔️ Awesome, I've got everything!

Great. Double check that both requirements.txt and Dockerfile are saved.

ⓧ I'd like to double check the full code

flask
redis
# syntax=docker/dockerfile:1
FROM python:3.12-alpine
WORKDIR /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["flask", "run", "--debug"]
Add environment and ignore files

Docker Compose can read environment variables from a .env file automatically. This keeps configuration separate from your code.

  • Create the environment file by running:
notepad .env
  • Click Yes to create the file.
  • Paste the following configuration:
APP_PORT=8000
REDIS_HOST=redis
REDIS_PORT=6379

What do these values mean?

  • APP_PORT=8000 sets which port on your computer maps to the web app inside the container.
  • REDIS_HOST=redis tells the Flask app to connect to a container named redis (Docker Compose creates this name automatically from the service name).
  • REDIS_PORT=6379 is the default port Redis listens on.
  • Save with Ctrl+S and close Notepad.

Finally, you'll create a .dockerignore file. This works like a filter that tells Docker which files to skip when copying your project into the container image.

  • Create the ignore file by running:
notepad .dockerignore
  • Click Yes to create the file.
  • Paste the following lines:
.env
*.pyc
__pycache__
redis-data

Why exclude these files?

The .env file contains host configuration that doesn't belong inside the container. The *.pyc and __pycache__ entries skip Python's compiled bytecode (the container builds its own). The redis-data entry prevents any local Redis data from being copied in.

  • Save with Ctrl+S and close Notepad.
  • Back in your terminal, verify all five files exist by running:
ls

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

Don't see all five files?

Files starting with a dot (.env, .dockerignore) may be hidden by default. Run ls -Force to show hidden files in PowerShell. If a file is missing, run the notepad command for it again and re-paste the content.

help me find my missing project files

✔️ Awesome, I've got everything!

Great. Make sure all files are saved before moving on.

ⓧ I'd like to double check the full code

import os
import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(
    host=os.getenv("REDIS_HOST", "redis"),
    port=int(os.getenv("REDIS_PORT", "6379")),
)


@app.route("/")
def hello():
    count = cache.incr("hits")
    return f"Hello from Docker! I have been seen {count} time(s).\n"
flask
redis
# syntax=docker/dockerfile:1
FROM python:3.12-alpine
WORKDIR /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["flask", "run", "--debug"]
APP_PORT=8000
REDIS_HOST=redis
REDIS_PORT=6379
.env
*.pyc
__pycache__
redis-data

Your project files are ready. Next up, you'll write a compose.yaml that wires these files together into a running multi-container application.

Write and Launch Your First Compose Stack

Your project files are ready. You have a Flask app, a Dockerfile to package it, and a .env file with configuration values.

Now you need a way to start your web app and its Redis database together with a single command. Docker Compose does exactly this. You define all your services in one file, and Compose handles building, networking, and starting them.

In this step, get ready to:
  • Create a compose.yaml file defining both services.
  • Launch the stack and see the hit counter working in your browser.
  • Stop the stack and observe that data is lost without persistent storage.
Create the compose.yaml file

Docker Compose reads a file called compose.yaml to know which services to run and how to configure them. This is where you wire your web app to Redis.

  • Create a new file called compose.yaml inside your compose-demo folder and paste the following code into it:
services:
  web:
    build: .
    ports:
      - "${APP_PORT}:5000"
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
    depends_on:
      - redis

  redis:
    image: redis:alpine

What does this file do?

  • services: defines the containers that make up your application.
  • web: builds from your Dockerfile (build: .) and maps host port 8000 to container port 5000 where Flask listens.
  • environment: passes the REDIS_HOST and REDIS_PORT values from your .env file into the container.
  • depends_on: tells Compose to start Redis before the web service.
  • redis: pulls the official redis:alpine image from Docker Hub. No build needed.

How does Compose read the .env file?

Compose automatically reads the .env file in your project directory and makes those values available for variable interpolation (the ${...} syntax). You do not need to tell it where to find the file.

  • Save compose.yaml.
  • Confirm the file exists by running:
ls

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

Don't see compose.yaml?

Make sure you saved the file with the exact name compose.yaml (not compose.yaml.txt). Some text editors on Windows add a hidden .txt extension.

Also confirm the file is inside the compose-demo folder, not one level up.

help me find my compose.yaml file

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

services:
  web:
    build: .
    ports:
      - "${APP_PORT}:5000"
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
    depends_on:
      - redis

  redis:
    image: redis:alpine
Launch the stack with docker compose up

With your Compose file saved, a single command builds your Flask image and starts both containers on a shared network.

  • Start both services by running:
docker compose up

Docker builds the web image from your Dockerfile (you will see each build step scroll by), then pulls the redis:alpine image, and starts both containers. Logs from both services stream into your terminal.

What does docker compose up do?

This command builds, creates, starts, and attaches to containers for every service defined in your compose.yaml. It also creates a default network so the containers can talk to each other by service name.

The web container can reach Redis at the hostname redis because that is the service name you defined. This is why REDIS_HOST=redis works in your .env file.

  • Open your browser and visit http://localhost:8000.
  • Refresh the page a few times.

You should see the message update each time: "Hello from Docker! I have been seen 1 time(s)." becomes 2, then 3, and so on.

Seeing "This site can't be reached"?

Make sure your terminal is still showing streaming logs (the containers are running). If you see an error about port 8000 already in use, another application is occupying that port. Close it or change APP_PORT in your .env file to another value like 8080.

Also confirm you are visiting http://localhost:8000 (not https).

help me troubleshoot why localhost:8000 is not loading

Stop the stack and observe data loss

Your counter is working, but what happens to that data when you stop the containers? Without persistent storage, container data disappears.

  • Switch back to your terminal where the logs are streaming.
  • Press Ctrl+C to stop the running containers.

You should see the containers stop gracefully. Now remove the containers entirely.

  • Remove the stopped containers and network by running:
docker compose down

What does docker compose down do?

This command stops and removes the containers and the default network that Compose created. Your images stay cached locally so rebuilds are fast.

Without a persistent volume, any data Redis stored in memory is gone. The counter resets to zero.

  • Start the stack again to prove the counter resets:
docker compose up
  • Refresh http://localhost:8000 in your browser.

The counter is back to 1. All previous counts are gone because the old Redis container (and its data) was removed.

  • Press Ctrl+C to stop the stack again. Leave it stopped for now.

Counter not resetting to 1?

If your counter did not reset, you may have stopped with Ctrl+C but skipped the docker compose down step. Stopping alone pauses the containers. You need docker compose down to fully remove them before starting fresh.

help me understand why my counter did not reset after restarting

You have proven that containers are ephemeral by default. Next up, you will add health checks and a named volume so Redis keeps your data safe across restarts.

Add Health Checks and Persistent Storage

In the previous step, you saw a frustrating reality. Every time you ran docker compose down and started fresh, the hit counter reset to zero.

This happens because containers are ephemeral by default. In this step, you will add two production-grade features to your Docker Compose stack: a health check that proves Redis is ready before the web app connects, and a named volume that persists data across restarts.

In this step, get ready to:
  • Add a health check to Redis so Docker knows when the database is ready.
  • Configure the web service to wait for a healthy Redis before starting.
  • Add a named volume so hit counter data survives container restarts.
Add a health check and startup dependency

Right now, Docker Compose starts both containers at the same time. If the web app tries to connect before Redis is ready, it crashes. A health check is a command Docker runs periodically to verify a service is actually working, not just running.

  • In compose.yaml, find the depends_on block under the web service:
    depends_on:
      - redis
  • Replace it with this version, which tells Docker to wait for Redis to pass its health check:
    depends_on:
      redis:
        condition: service_healthy

What changed here?

The depends_on block changed from a simple list (- redis) to a map with a condition. The condition: service_healthy setting means Docker will wait until Redis reports healthy before starting the web container.

  • Still in compose.yaml, find the redis service. Add this healthcheck block directly below image: redis:alpine:
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

What does this health check do?

Docker runs redis-cli ping inside the Redis container every 5 seconds. If the command fails 5 times in a row, Docker marks the container as unhealthy.

The start_period gives Redis 10 seconds to start up before the health checks begin counting failures. This prevents false alarms during initial boot.

  • Save compose.yaml.

YAML indentation errors?

YAML is very strict about indentation. The healthcheck key must be indented at the same level as image (4 spaces from the left). Each sub-key (test, interval, etc.) must be indented 6 spaces from the left. Use spaces only, never tabs.

help me fix YAML indentation in my compose.yaml healthcheck block

Add a named volume for persistent storage

A named volume is a Docker-managed storage area that lives outside the container. When you destroy and recreate containers, the volume stays intact. This is how your hit counter will survive restarts.

  • In compose.yaml, find the redis service. Add a volumes block between image: redis:alpine and the healthcheck: line you just added:
    volumes:
      - redis-data:/data
  • At the very bottom of compose.yaml, below the services block, add the top-level volumes key to register the named volume:
volumes:
  redis-data:

What do these volume lines do?

The line redis-data:/data under the redis service mounts a named volume at /data, the path where Redis writes its data files. This means Redis saves its data to the volume instead of inside the container.

The top-level volumes: key at the bottom of the file registers redis-data as a named volume that Docker manages. Without this declaration, Docker Compose would reject the file.

  • Save compose.yaml.

Not sure about placement?

The top-level volumes: key must be at the same indentation level as services: (no indentation, flush left). If it is indented under a service, Docker Compose will throw a parsing error.

help me place the top-level volumes key correctly in my compose.yaml

✔️ Awesome, I've got everything!

Great. Double check you have saved compose.yaml before moving on.

ⓧ I'd like to double check the full code

Here is the complete compose.yaml at this point:

services:
  web:
    build: .
    ports:
      - "${APP_PORT}:5000"
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
    depends_on:
      redis:
        condition: service_healthy

  redis:
    image: redis:alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

volumes:
  redis-data:
Test data persistence across restarts

Now for the real test. You will start the stack, generate some hits, tear it down completely, then bring it back up to prove the data survived.

  • Start the stack by running this command:
docker compose up

What should I see?

Docker Compose will start Redis first, then wait for its health check to pass before launching the web container. You will see log lines showing the health check running, followed by the web service attaching.

  • Open your browser and visit http://localhost:8000.
  • Refresh the page 3-4 times to increment the counter.
  • Note the number you see (for example, 4).
  • Return to your terminal and press Ctrl+C to stop the stack.
  • Remove the containers by running:
docker compose down
  • Start the stack again by running:
docker compose up
  • Visit http://localhost:8000 again in your browser.
  • The counter picks up from where you left off (for example, 5 if you stopped at 4). Your data persisted.

Why does the counter persist now?

The docker compose down command removes containers and networks, but it does NOT remove named volumes. The redis-data volume (stored as compose-demo_redis-data on your machine) kept the Redis data file intact between runs.

Counter still resets to 1?

Make sure your compose.yaml has the volumes: block under the redis service AND the top-level volumes: key at the bottom. Both are required. Also check that you ran docker compose down (without -v) because the -v flag deletes volumes.

help me figure out why my Docker volume is not persisting data

Your stack now has production-grade reliability. Redis proves it is healthy before the web app connects, and your data survives complete container teardowns. Next up, you will learn how to inspect and debug running containers from the inside.

Inspect and Debug Your Running Stack

Your stack now starts in the right order and remembers data between restarts. That covers the "run it" side of containers.

Running containers are black boxes by default. In this step, you'll use Docker Compose's debugging commands to stream logs, inspect environment variables, and read data directly from Redis.

In this step, get ready to:
  • Stream real-time logs from your running containers.
  • Execute commands inside containers to inspect their state.
  • Remove named volumes to reset persistent data.
Start the stack in detached mode

So far, you've been running docker compose up in the foreground, which locks up your terminal with log output. Detached mode frees your terminal so you can run other commands while the stack keeps running.

  • Start your stack in the background by running this command:
docker compose up -d

You should see status messages confirming each container has started. Your terminal prompt returns immediately.

What does the -d flag do?

The -d flag stands for "detached." It starts containers in the background so your terminal stays free for other commands. The containers keep running until you explicitly stop them.

  • Stream real-time logs from both services by running:
docker compose logs -f

You should see interleaved log output from both the web and redis services. Each line is prefixed with the service name so you can tell them apart.

  • Open your browser and visit http://localhost:8000 a few times.
  • Switch back to your terminal. You should see new log entries appearing for each request you made.
  • Press Ctrl+C to stop following the logs.

Do my containers stop when I press Ctrl+C here?

No. Because you started with docker compose up -d, pressing Ctrl+C only stops the log-following process. Your containers continue running in the background.

Not seeing any log output?

Make sure the containers are actually running. If docker compose up -d failed silently, there are no containers to stream logs from. Try running docker compose up without the -d flag to see any startup errors directly.

Help me troubleshoot why docker compose logs shows nothing

Inspect environment variables and data inside containers

The docker compose exec command runs a command inside an already-running container. This is how you poke around inside a container without stopping it.

  • Check the environment variables inside your web container by running:
docker compose exec web env

You should see a list of environment variables including REDIS_HOST=redis and REDIS_PORT=6379. These are the values from your .env file, injected into the container by Compose.

  • Read the current hit counter value directly from Redis by running:
docker compose exec redis redis-cli GET hits

You should see a number matching the count you saw in your browser. This confirms your Flask app and Redis are communicating correctly.

What is happening in this command?

This command has three parts. docker compose exec redis targets the running Redis container. redis-cli is the Redis command-line client (already installed inside the Redis image). GET hits retrieves the value stored under the key hits.

Seeing "no such service" or connection error?

Make sure you spell the service name exactly as it appears in your compose.yaml. The service names are web and redis (lowercase). If the container is not running, exec will fail. Run docker compose up -d first to make sure everything is started.

Help me fix a docker compose exec error

Remove the volume and verify the counter resets

In the previous step, you saw that docker compose down alone keeps your named volume intact. Adding the -v flag removes containers AND named volumes, giving you a completely fresh start.

  • Stop the containers and delete the named volume by running:
docker compose down -v

You should see messages confirming that both containers and the compose-demo_redis-data volume have been removed.

  • Start the stack again in detached mode by running:
docker compose up -d
  • Open your browser and visit http://localhost:8000.

The counter should read "1" instead of continuing from your previous count. The volume (and all its data) was destroyed, so Redis started fresh with an empty database.

When would I use -v in real life?

Use docker compose down -v when you want to reset your development database to a clean state. Without the -v flag, your data persists between restarts. This is the difference between "restart my app" and "wipe everything and start over."

Counter not resetting to 1?

Make sure you ran docker compose down -v (with the -v flag) and not just docker compose down. Without -v, the named volume survives and Redis restores the old counter on next start.

Help me figure out why my counter did not reset

Secret mission

Add a Visitor Log Page

Your hit counter tracks how many times the page has been visited, but it doesn't tell you when each visit happened. In this secret mission, you'll extend your Flask app with a second page that displays a timestamped visitor log, powered by a Redis list.

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 images built from your Dockerfile (stored locally by Docker).
  • Named volume compose-demo_redis-data (stores the Redis data on your machine).
  • Project files in the compose-demo folder on your Desktop.
  • Docker Desktop application installed on your machine.

Keep everything running

No action needed. Choose this if you're still actively building or plan to keep experimenting with Docker.

  • Your containers use minimal resources when idle.
  • Docker Desktop stays in your system tray and starts automatically with Windows unless you change its settings.
  • Your project files, images, and volumes all remain intact for next time.

Pause - I'll come back to this later

Stop the running containers to free up memory, but keep your files and volume so you can pick up where you left off.

  • In your terminal, make sure you're in the compose-demo folder and run:
docker compose down

This stops and removes the containers but keeps your named volume and images. Next time, just run docker compose up to pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Stop containers and remove the named volume by running:
docker compose down -v
  • Remove all unused Docker images to reclaim disk space by running:
docker system prune -a

When prompted, type y and press Enter to confirm.

  • Delete the project folder. First, move out of it:
cd ~/Desktop
  • Then remove the folder. On macOS/Linux:
rm -rf compose-demo
  • On Windows (PowerShell):
Remove-Item -Recurse -Force compose-demo
  • To uninstall Docker Desktop, open Settings on your computer.
  • Select Apps, then Apps & features.
  • Find Docker Desktop in the list, select it, and click Uninstall.

That's a Wrap!

That's a Wrap!

Nice work! You just containerized your first multi-service application with Docker Compose.

You've learned how to:

  • Wrote a Dockerfile to package a Python Flask application into a portable container image.
  • Created a compose.yaml that orchestrates a web app and Redis database with health checks and named volumes for persistent storage across restarts.
  • Used docker compose exec and docker compose logs to debug and inspect running containers from the inside.
  • Added a visitor log page with timestamped entries stored in Redis.

Ready to quiz yourself?