Containerize a Python Web App with Docker
Build a multi-container Flask and Redis app using Docker and Compose.
Introduction
30 Second Summary
Every developer has heard "it works on my machine." Docker solves that problem by packaging your app with everything it needs to run, so it behaves the same on every computer.
In this project, you will containerize a Python web app with a Redis database, writing a Dockerfile and Compose file from scratch to run a multi-container application on your machine.
What You'll Build
A Flask web app with a Redis-backed visit counter, running as a multi-container Docker application.
By the end of this project, you'll have:
- A Dockerfile that packages a Python web app into a portable image you can run anywhere.
- A compose.yaml that defines and orchestrates a web service and a Redis service together.
- A running multi-container app with persistent data that survives container restarts using named volumes.
- Secret Mission: Scale your web service to multiple replicas behind a load balancer to see Docker networking in action.
Are there any prerequisites?
None. This project includes full setup instructions for Docker Desktop and all required tools. No prior Python or Docker experience is needed.
Install Docker Desktop
Before you can build and run containers, you need Docker Desktop installed on your Mac.
Docker Desktop is the application that runs the Docker engine on your computer. It gives you everything you need to build images, run containers, and orchestrate multi-container applications from a single install.
In this step, get ready to:
- Download and install Docker Desktop for Mac.
- Verify Docker and Docker Compose are working.
- Run your first test container to prove everything works end-to-end.
Download and install Docker Desktop
Docker Desktop is free for personal use and education. You will accept a license agreement during installation.
- Open the Docker Desktop for Mac install page in your browser.
- Click the download button that matches your Mac (Apple silicon or Intel chip).
- Double-click the downloaded Docker.dmg file to open the installer.
- Drag the Docker icon into the Applications folder.
- Open Docker Desktop by pressing Cmd+Space to open Spotlight, typing Docker, and pressing Enter.
What happens when Docker Desktop launches?
Docker Desktop displays a Subscription Service Agreement the first time you open it. Click Accept to continue.
You will also be asked to choose between recommended and advanced settings. Select Use recommended settings and enter your Mac password if prompted.
- Wait for Docker Desktop to fully start. You will see a whale icon appear in your menu bar at the top of the screen.
How do I know Docker Desktop is ready?
The whale icon in your menu bar stops animating once Docker Desktop is fully running. If it is still animating, Docker is still starting up.
Verify Docker is running
With Docker Desktop running, you can use the docker command in your terminal to interact with the Docker engine.
- Open Terminal by pressing Cmd+Space, typing Terminal, and pressing Enter.
- Check that the Docker engine is available by running this command:
docker version
- Check that Docker Compose is available by running this command:
docker compose version
✔️ I see version numbers for both commands
Both commands printed version information without errors. Docker Desktop is installed and running correctly.
ⓧ I see an error about connecting to Docker
This means Docker Desktop has not finished starting up yet.
- Check the whale icon in your menu bar. If it is still animating, wait for it to stop.
- Once the icon is steady, run docker version again.
ⓧ Command not found
This means Docker Desktop did not install the CLI tools correctly.
- Quit Docker Desktop completely (right-click the whale icon and select Quit Docker Desktop).
- Re-open Docker Desktop from Spotlight.
- If prompted for settings, select Use recommended settings and enter your password.
- Close and reopen Terminal, then try docker version again.
If the problem persists, uninstall Docker Desktop from Applications and reinstall from scratch.
What is Docker Compose?
Docker Compose is a tool for defining and running multi-container applications. It comes bundled with Docker Desktop, so there is nothing extra to install.
You will use it later in this project to run your Flask app and Redis database together with a single command.
Run your first test container
The best way to confirm Docker works end-to-end is to pull and run a tiny test image. This proves Docker can download images from Docker Hub and start containers successfully.
- Run the hello-world test container by running this command:
docker run --rm hello-world
What does this command do?
docker run creates and starts a new container from an image. hello-world is a tiny image hosted on Docker Hub that prints a confirmation message and exits.
The --rm flag automatically removes the container after it finishes. This keeps your system clean.
- You should see output that includes a greeting starting with "Hello from Docker!" followed by an explanation of what just happened.
Don't see the greeting message?
- Make sure Docker Desktop is fully running (the whale icon in your menu bar should be steady, not animating).
- If you see a network error, check that you are connected to the internet. Docker needs to download the image the first time you run it.
Help me troubleshoot my Docker hello-world test
Your Docker installation is ready. Next up, you will create the Flask application that Docker will containerize.
Create the Flask Application
Docker Desktop is installed and ready to go. Now you need an application to put inside a container.
In this step, you'll write a small Python web app that counts page visits. It uses Flask as the web framework and Redis as the data store.
In this step, get ready to:
- Create the project directory and Flask application file.
- Create a requirements file listing Python dependencies.
- Create a Docker ignore file to keep the build context clean.
Create the project directory and application file
Every Docker project starts with a directory that holds the application code and Docker configuration files. You'll create this directory on your Desktop so it's easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir visit-counter
cd visit-counter
- Confirm you're in the right place by running:
pwd
You should see a path ending in visit-counter.
Seeing a different path?
Make sure you ran both cd ~/Desktop and mkdir visit-counter in that order. If the folder already exists from a previous attempt, skip the mkdir command and just run cd visit-counter.
help me fix my terminal directory
Now you'll create the Flask application file. This is the Python code that runs inside your container and serves web pages.
- Create a new file called app.py inside your visit-counter folder. Open it in your preferred text editor and paste the following code:
# Import dependencies
import os
import socket
from flask import Flask, jsonify
# Create the Flask app
app = Flask(__name__)
# Connect to Redis for data storage
import redis
cache = redis.Redis(
host=os.getenv("REDIS_HOST", "redis"),
port=int(os.getenv("REDIS_PORT", "6379")),
)
# Count visits and return a greeting
@app.route("/")
def hello():
count = cache.incr("hits")
return f"Hello from Docker! I have been seen {count} time(s).\n"
# Return container hostname and hit count as JSON
@app.route("/stats")
def stats():
count = cache.get("hits")
return jsonify(
hostname=socket.gethostname(),
hits=int(count) if count else 0,
)
What does this code do?
- cache = redis.Redis(...) creates a connection to a Redis database. It reads the hostname and port from environment variables, falling back to sensible defaults.
- The / route increments a counter in Redis every time someone visits the page, then displays the total count.
- The /stats route returns the container's hostname and hit count as JSON. You'll use this later to see load balancing in action.
- Save app.py.
Not sure how to create a file?
You can use any text editor you're comfortable with. If you have VS Code, open it and use File > Open Folder to open your visit-counter folder, then create a new file called app.py.
help me create a Python file in my project folder
✔️ Awesome, I've got everything!
Great. Make sure you've saved app.py before moving on.
ⓧ I'd like to double check the full code
import os
import socket
from flask import Flask, jsonify
app = Flask(__name__)
import redis
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"
@app.route("/stats")
def stats():
count = cache.get("hits")
return jsonify(
hostname=socket.gethostname(),
hits=int(count) if count else 0,
)
Add the Python dependencies
Docker needs to know which Python packages your app requires so it can install them inside the container. You declare these in a requirements.txt file.
- Create a new file called requirements.txt in your visit-counter folder and add the following content:
flask
redis
What does this file do?
Each line names a Python package that pip will install inside the container. Flask handles web requests and Redis provides the Python client for talking to the Redis database.
- Save requirements.txt.
Create a Docker ignore file
When Docker builds an image, it sends all the files in your project directory to the Docker engine. A .dockerignore file tells Docker which files to skip, keeping the build fast and secure.
- Create a new file called .dockerignore in your visit-counter folder and add the following content:
.env
*.pyc
__pycache__
redis-data
What does this file do?
- .env keeps your environment variables out of the image (they may contain sensitive values).
- *.pyc and __pycache__ are compiled Python files that would be rebuilt inside the container anyway.
- redis-data excludes any local Redis data dumps from the build context.
- Save .dockerignore.
- Confirm all three files exist by running:
ls -a
You should see app.py, requirements.txt, and .dockerignore in the output.
Missing a file?
If .dockerignore doesn't appear, make sure the filename starts with a dot. Files starting with a dot are hidden by default on macOS. The -a flag in ls -a reveals them.
help me find my missing project files
Your application code is ready. Next up, you'll write a Dockerfile that packages this code into a portable container image.
Write Your First Dockerfile
Your Flask app and its dependencies are ready, but right now they only exist as loose files on your machine. To run them inside a container, Docker needs a recipe that describes how to package everything into a single, portable image.
That recipe is called a Dockerfile. In this step, you will write a Dockerfile from scratch and use it to build your first Docker image.
In this step, get ready to:
- Write the Dockerfile instructions that package your app into an image.
- Build your first Docker image from the Dockerfile.
- Verify the image appears in your local image list.
Create the Dockerfile base and install dependencies
A Dockerfile is a plain text file with a specific set of instructions. Each instruction creates a layer in the final image, building up from a base operating system to a fully configured application.
- Create a new file called Dockerfile (no file extension) inside your visit-counter folder, then paste the following code into it:
# 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 . .
What does this code do?
- # syntax=docker/dockerfile:1 tells Docker to use the latest stable Dockerfile syntax.
- FROM python:3.12-alpine starts from a lightweight Python image based on Alpine Linux. Alpine images are only about 5 MB, which keeps your builds fast.
- WORKDIR /code sets the working directory inside the container. All following commands run from this folder.
- The two ENV lines configure Flask to find app.py and listen on all network interfaces so traffic can reach the container.
- RUN apk add --no-cache gcc musl-dev linux-headers installs system-level compilation tools that some Python packages need.
- COPY requirements.txt . copies just the requirements file first, then RUN pip install installs the Python packages. Copying requirements separately lets Docker cache this layer so you do not re-install packages every time your app code changes.
- COPY . . copies the rest of your application code into the image.
- Save Dockerfile.
You will not see a visible change yet. The next chunk adds the final instructions, then you will build and test the image.
Add the port and startup command
Your Dockerfile needs two more instructions: one to document which port the app listens on, and one to define the command that runs when a container starts.
- At the bottom of your Dockerfile (below the COPY . . line), add these two lines:
EXPOSE 5000
CMD ["flask", "run", "--debug"]
What do these instructions do?
- EXPOSE 5000 documents that the container listens on port 5000. It does not publish the port by itself, but it signals to anyone reading the Dockerfile which port to map.
- CMD ["flask", "run", "--debug"] is the default command that runs when you start a container from this image. It launches the Flask development server in debug mode.
- Save Dockerfile.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile for comparison:
# 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"]
Build the Docker image
With your Dockerfile written, you can now turn it into an image. The docker build command reads the Dockerfile, executes each instruction as a layer, and produces a final image you can run as a container.
- Build the image by running this command in your terminal:
docker build -t visit-counter .
What does this command do?
- docker build tells Docker to create an image from the Dockerfile in the current directory.
- -t visit-counter tags (names) the resulting image so you can reference it easily later.
- The . at the end sets the build context to your current folder. Docker sends these files to the builder so it can execute COPY instructions.
The first build takes a minute or two because Docker downloads the python:3.12-alpine base image and installs packages. You should see progress lines ending with FINISHED when the build completes successfully.
- Verify the image exists in your local registry by running:
docker images
You should see a row with visit-counter in the REPOSITORY column and latest in the TAG column.
Build failing with an error?
- Make sure you are running the command from inside your visit-counter folder. The . in the command means "this directory", so your Dockerfile must be here.
- Confirm the file is named exactly Dockerfile with a capital D and no file extension. A file named dockerfile.txt or Dockerfile.dockerfile will not be found.
- Check that Docker Desktop is still running. If it has been stopped or quit, the docker command will not work.
Why is my Docker build failing?
Your image is built and ready to go. Next up, you will run this image as a container and see what happens when the app tries to connect to Redis.
Run a Single Container
Your Docker image is built and ready. The next step is to start a container from it and see if your app actually works.
Running a container turns your static image into a live process. You'll map a port so your browser can reach the app inside the container.
In this step, get ready to:
- Run the visit-counter container and visit it in the browser.
- Inspect the container logs to understand why the app fails without Redis.
Run the container and visit it in the browser
Your image packages the Flask app, but it still needs a running Redis service to store the visit count. Let's see what happens when we run the container without Redis.
- Start the container in detached mode with port mapping by running this command:
docker run -d -p 8000:5000 --name web visit-counter
What does this command do?
- -d runs the container in the background (detached mode) so you get your terminal back.
- -p 8000:5000 maps port 8000 on your machine to port 5000 inside the container, where Flask listens.
- --name web gives the container a friendly name so you can reference it later.
- visit-counter is the image you built in the previous step.
- Open your browser and visit http://localhost:8000.
You should see an error page instead of a greeting. The app crashed because it tried to connect to a Redis server named redis that doesn't exist in this standalone container.
Seeing a connection refused message?
If your browser shows a "connection refused" or "can't reach this page" message, the container may have exited before you visited the URL. That's fine. The result is the same: the app cannot serve requests without Redis.
Help me debug why my Docker container exited immediately
Inspect the logs and clean up
The browser error doesn't tell you much about what went wrong inside the container. Docker captures all output from your app, and you can read it back with a single command.
- View the container's output by running:
docker logs web
What should I see?
You'll see Flask's startup messages followed by a connection error mentioning Redis. This is a ConnectionError from the redis-py library, confirming the app cannot reach a Redis service.
This is expected. Your container is running in isolation with no Redis service available. It proves that multi-container apps need a way to discover and connect to each other.
- Remove the failed container by running:
docker rm -f web
- Confirm the container is gone by running:
docker ps -a
You should no longer see a container named web in the list.
Why do multi-container apps need orchestration?
Each container runs in its own isolated network by default. Your Flask app tried to connect to a hostname called redis, but no container with that name existed on the same network.
You need a tool that creates a shared network, starts both containers, and lets them find each other by name. That tool is Docker Compose.
Container still showing in docker ps -a?
Make sure you used the -f flag in docker rm -f web. The -f flag forces removal even if the container is still running.
Help me remove a Docker container that won't go away
You've just proven that running a single container isn't enough for this app. Next up, you'll use Docker Compose to run both Flask and Redis together on a shared network.
Orchestrate with Docker Compose
In the last step, you saw your container crash because Redis wasn't available. Running a single container can't solve the problem of a multi-service app.
Docker Compose lets you define multiple services in a single file and start them together with one command. In this step, you'll write a Compose configuration that launches both your web app and a Redis instance side by side.
In this step, get ready to:
- Create a .env file with port and connection settings.
- Write a compose.yaml to define the web and Redis services.
- Start the full stack and verify the visit counter works.
Create the environment file
Docker Compose automatically reads a .env file in your project directory and makes its values available for variable substitution inside compose.yaml. This keeps configuration separate from your orchestration logic.
- Create a file called .env in your visit-counter folder with the following content:
APP_PORT=8000
REDIS_HOST=redis
REDIS_PORT=6379
What does this file do?
APP_PORT=8000 sets the host port where you'll access your app in the browser.
REDIS_HOST=redis tells Flask to connect to a service named redis on the Docker network. Docker Compose creates a network where each service is reachable by its name.
REDIS_PORT=6379 is the default port Redis listens on.
- Save .env.
- Confirm the file exists by running:
ls -a
You should see .env listed alongside your other project files.
Don't see .env in the listing?
Files starting with a dot are hidden by default. The -a flag in ls -a reveals hidden files. Make sure you used ls -a (not just ls).
Also check you created the file inside the visit-counter directory, not somewhere else.
help me find my .env file
Define services in a Compose file
A compose.yaml file declares all the services your application needs. Docker Compose reads this file and builds, creates, and starts everything with a single command.
- Create a file called compose.yaml in your visit-counter folder. Start by adding the web service definition:
services:
web:
build: .
ports:
- "${APP_PORT}:5000"
environment:
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
depends_on:
redis:
condition: service_healthy
What does this code do?
build: . tells Compose to build an image from the Dockerfile in the current directory.
ports: "${APP_PORT}:5000" maps port 8000 on your machine to port 5000 inside the container (where Flask listens). The value comes from your .env file.
depends_on: redis: condition: service_healthy ensures Compose waits until Redis passes its health check before starting the web service. This solves the crash you saw in the previous step.
- You won't see a change yet. The next chunk adds the Redis service that completes the orchestration.
- Add the Redis service and the top-level volumes declaration below the web service in compose.yaml:
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:
What does this code do?
image: redis:alpine pulls the official Redis image from Docker Hub. The alpine tag means it uses a minimal Linux base, keeping the download small.
volumes: redis-data:/data mounts a named volume at the path where Redis stores its data. This means data survives even when the container is removed.
The healthcheck block runs redis-cli ping every 5 seconds to confirm Redis is ready. The web service waits for this check to pass before starting.
The top-level volumes: key registers the named volume with Docker so it persists independently of any container.
- Save compose.yaml. You won't see a visible change yet. The next substep starts the full stack.
Seeing YAML syntax errors later?
YAML is sensitive to indentation. Make sure each level uses exactly 2 spaces (not tabs). The redis service must be indented at the same level as web (both under services:).
The top-level volumes: key must be at the root level (zero indentation), not nested under services:.
help me fix my compose.yaml indentation
✔️ 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
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:
Start and test the full stack
Everything is in place. One command will build your image, pull Redis, create a network, and start both services together.
- Start the full stack by running:
docker compose up
What does this command do?
docker compose up reads your compose.yaml, builds any images that need building, pulls any images from Docker Hub, creates a network, and starts all services.
Because you're running without the -d flag, logs from both containers stream directly into your terminal. You'll see Redis reporting it's ready and Flask starting up.
Your terminal is now occupied with log output. Leave it running and open your browser.
- Open your browser and go to http://localhost:8000.
- You should see: Hello from Docker! I have been seen 1 time(s).
- Refresh the page 3-4 times. The counter increments with each visit.
Not seeing the page in your browser?
Check your terminal for error messages. If you see a port conflict, another process is already using port 8000. Stop that process or change APP_PORT in your .env file to a different number.
If you see a build error, make sure your Dockerfile, app.py, and requirements.txt are all saved in the visit-counter directory.
If you see a health check timeout, wait a few more seconds. Redis may take a moment to start on the first run.
help me debug docker compose up errors
- Go back to your terminal where Compose is running.
- Press Ctrl+C to stop the services.
- Remove the stopped containers by running:
docker compose down
What does docker compose down do?
docker compose down removes the containers and the network that Compose created. Your images and named volumes stay intact, so you can bring everything back up quickly next time.
Your multi-container app works. Both Flask and Redis start together, communicate over a shared network, and the health check prevents the crash you saw earlier. Next up, you'll explore what happens to your data when containers are removed.
Persist Data with Volumes
Your multi-container app is running smoothly, but there is a hidden problem.
Every time you stop and restart your containers, Redis loses all its data. In this step, you will prove the problem exists, then solve it with Docker volumes to persist data across container restarts.
In this step, get ready to:
- Observe ephemeral container data disappearing after a restart.
- Add a named volume to your Compose file to persist Redis data.
- Verify persistence works and learn how to intentionally wipe volumes.
Prove that container data is ephemeral
Containers are designed to be disposable. When you remove a container, everything stored inside it disappears.
- Start your application stack by running:
docker compose up -d
- Open http://localhost:8000 in your browser.
- Refresh the page 4-5 times to build up the visit counter.
You should see the count climbing with each refresh (e.g. "I have been seen 5 time(s)").
- Now tear down the stack and bring it back up by running these commands:
docker compose down
docker compose up -d
- Refresh http://localhost:8000 in your browser.
The counter is back to 1. All of Redis's data vanished when the container was removed.
Why did the data disappear?
Docker containers have a writable layer on top of their image. When you run docker compose down, the containers are removed and that writable layer is deleted along with them.
This is by design. Containers are meant to be ephemeral and reproducible. To keep data around, you need to store it outside the container in a volume.
- Stop the running containers before making changes by running:
docker compose down
Add a named volume for Redis
A named volume is storage managed by Docker that lives outside any container. It survives container removal and can be reattached when a new container starts.
- In compose.yaml, find the redis service. Add a volumes section to mount a named volume at Redis's data directory, and add a top-level volumes key to register the volume. Your full compose.yaml should look like this:
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
# Mount a named volume so Redis data survives container restarts
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
# Register named volumes at the top level
volumes:
redis-data:
What does this configuration do?
- redis-data:/data tells Docker to mount the named volume redis-data at the /data path inside the Redis container. This is where Redis stores its persistence files.
- The top-level volumes: key registers redis-data as a Docker-managed volume. Docker handles where the actual data lives on your host machine.
- Save compose.yaml.
YAML indentation errors?
YAML is whitespace-sensitive. Make sure volumes: under the redis service is indented at the same level as image: and healthcheck:. The top-level volumes: key should have zero indentation, at the same level as services:.
Help me fix YAML indentation in my compose.yaml
✔️ Awesome, I've got everything!
Great. Double check you have saved compose.yaml before continuing.
ⓧ I'd like to double check the full code
Here is the complete compose.yaml for comparison:
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:
Verify persistence and learn to wipe volumes
Time to prove that volumes actually work. You will repeat the same down/up cycle and confirm your counter survives.
- Start the stack with the new volume configuration by running:
docker compose up -d
- Visit http://localhost:8000 and refresh 4-5 times to build up a count.
- Note the number you reached (e.g. 5).
- Tear down and restart the stack by running:
docker compose down
docker compose up -d
- Refresh http://localhost:8000 in your browser.
The counter picks up exactly where you left off. Your data survived the restart because it is stored in the redis-data volume, not inside the container.
What is docker compose down actually doing now?
docker compose down removes containers and networks, but it does NOT remove named volumes by default. That is why your data persists. The volume lives on, independent of any container.
Sometimes you DO want a clean slate. Docker provides the -v flag to explicitly remove volumes along with containers.
- Remove everything including volumes by running:
docker compose down -v
- Bring the stack back up by running:
docker compose up -d
- Refresh http://localhost:8000 in your browser.
The counter is back to 1. The -v flag deleted the redis-data volume, wiping all persisted data. Now you know how to control exactly when data lives and dies.
Counter still showing the old number?
Make sure you ran docker compose down -v (with the -v flag), not just docker compose down. Without -v, named volumes are preserved.
Also try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to clear any cached page.
Help me debug why my Docker volume data isn't being removed
- Stop the running containers by running:
docker compose down
You now have full control over data persistence in Docker. Your app can survive restarts, and you can wipe the slate clean whenever you need to. Next up, tackle the Secret Mission and scale your web service to multiple replicas behind a load balancer.
Secret mission
Scale Your Web Service
Your app runs as a single container right now. What happens when traffic spikes? In this mission, you'll scale to multiple web containers behind a load balancer and watch Docker distribute requests across replicas automatically.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally via 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:
- Docker containers (web replicas, nginx, redis).
- Docker image (visit-counter).
- Named volume (redis-data).
- Project directory (visit-counter/) on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with Docker commands.
- Your app stays available at http://localhost:8000.
- Docker Desktop continues running in the background and uses some memory.
- To check running containers at any time, run docker compose ps from the visit-counter directory.
Pause - I'll come back to this later
Stop the containers to free up memory without removing anything. You can resume later without rebuilding.
- Stop all containers by running:
docker compose stop
- When you are ready to resume, navigate back to the visit-counter directory and run:
docker compose start
- Your containers will restart with the same data intact.
Delete - I don't want to use this again
Remove all containers, networks, volumes, and the custom image to start fresh.
- Remove all containers, networks, and the named volume by running:
docker compose down -v
- Remove the custom Docker image by running:
docker image rm visit-counter
- Delete the project directory by running:
cd ~/Desktop
rm -rf visit-counter
- On Windows, use this command instead:
cd ~\Desktop
rmdir /s /q visit-counter
- Docker Desktop itself remains installed for future projects.
That's a Wrap!
That's a Wrap!
Nice work! You just containerized a real web application with Docker.
You've learned how to:
- Built a custom Docker image from a Dockerfile you wrote from scratch.
- Orchestrated a multi-container app (Flask + Redis) with Docker Compose, including health checks and environment variables.
- Persisted data across container restarts using named volumes.
- Scaled a web service to multiple replicas with load balancing using Nginx.
Ready to quiz yourself?