Containerize a Multi-Service App

Build a containerized Flask and Redis app using Docker and Docker Compose.

Introduction

30 Second Summary

Every time you download an app on your phone, you expect it to just work regardless of your device. Containers bring that same "it just works" reliability to the software that powers websites and APIs.

In this project, you will containerize a multi-service web application using Docker and Docker Compose. You'll write a Dockerfile from scratch and orchestrate two containers that work together to power a hit-counter web app.

What You'll Build

A containerized web application consisting of a Python Flask API and a Redis database, orchestrated with Docker Compose.

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

  • A custom Docker image built from a Dockerfile you wrote from scratch.
  • A multi-container application running with Docker Compose (Flask web app + Redis database).
  • A portfolio-ready GitHub repository with a Dockerfile, compose.yaml, and README explaining the architecture.
  • Secret Mission: Push your custom image to Docker Hub so it can be pulled from any machine, Kubernetes cluster, or Terraform-managed infrastructure.

Are there any prerequisites?

You just need basic command line familiarity (navigating directories, running commands). No programming experience is required since the Python code is provided for you.

Before We Start

Before We Start...

Set Up Docker Desktop and Your Project

Before you can containerize anything, you need the tool that runs containers on your machine. Docker Desktop is the application that provides everything you need to build, run, and manage containers locally.

In this step, you will verify Docker is working, create a free Docker Hub account, and set up your project files. By the end, you will have a project folder open in VS Code with all the starter code ready to containerize.

In this step, get ready to:
  • Verify Docker Desktop is installed and running.
  • Create a free Docker Hub account for the Secret Mission.
  • Set up your project folder with application files.
Check if Docker is already installed

Docker Desktop provides the docker command-line tool alongside a graphical interface for managing containers. Let's check whether it is already installed on your machine.

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

✔️ I see a version number

Docker is already installed. Move on to the verification step below.

ⓧ I see a version lower than 20

Your Docker installation is outdated. Download the latest version of Docker Desktop from the link below and install it over your existing version.

  • Download Docker Desktop from the official install page.
  • Run the installer and choose the per-user installation mode when prompted. This does not require admin privileges.
  • After installation completes, open Docker Desktop from your search bar.
  • Wait for the whale icon to appear in your system tray. This means Docker is running.

ⓧ Command not found

Docker is not installed yet. Follow these steps to install Docker Desktop.

  • Download Docker Desktop from the official install page.
  • Run the installer and choose the per-user installation mode when prompted. This does not require admin privileges.
  • After installation completes, open Docker Desktop from your search bar.
  • Wait for the whale icon to appear in your system tray. This means Docker is running.
  • Once Docker Desktop is running, close and reopen PowerShell so it recognizes the new docker command.
  • Verify Docker works correctly by running this command:
docker run hello-world

You should see a message that starts with Hello from Docker! followed by an explanation of how the container ran.

What does this command do?

The docker run hello-world command downloads a tiny test image from Docker Hub and runs it as a container. If you see the greeting message, it confirms that Docker can pull images, create containers, and execute them successfully.

Seeing a connection error?

Make sure Docker Desktop is running. Look for the whale icon in your system tray. If the icon shows a yellow or red status, wait a moment for Docker to finish starting up, then try the command again.

help me fix Docker connection errors

Create a Docker Hub account

Docker Hub is the default container registry where Docker images are stored and shared. You will need an account for the Secret Mission later, where you push your custom image to the registry.

  • Go to hub.docker.com in your browser.
  • Click Sign Up and create a free account.
  • Open Docker Desktop from your system tray.
  • Click Sign in in the top right corner and enter your Docker Hub credentials.

Why sign in to Docker Desktop?

Signing in links your local Docker CLI to your Docker Hub account. This lets you push and pull images without entering credentials every time.

Create the project folder and application files

Now you will create your project folder and the starter files for the Flask application. The app itself is a simple hit counter that tracks page visits using Redis.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir docker-portfolio
cd docker-portfolio
  • Open the folder in VS Code by running:
code .

Getting a 'code' not found error?

If VS Code does not open, the code command is not in your system PATH. Open VS Code manually, press Ctrl+Shift+P, type Shell Command: Install 'code' command in PATH, and run it. Then restart PowerShell and try again.

help me set up the code command in my terminal

VS Code opens with an empty docker-portfolio folder in the Explorer sidebar. Now you will create the three starter files.

  • In VS Code, click File in the top menu bar.
  • Click New File.
  • Name the file app.py and press Enter.
  • Paste the following code into app.py:
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 is a minimal Flask web application with a single route. When someone visits the page, it increments a hits counter stored in Redis and displays the total count.

The connection details for Redis come from environment variables (REDIS_HOST and REDIS_PORT). This is a common pattern in containerized apps because the host name changes depending on how services are connected.

  • Save app.py with Ctrl+S (Windows) or Cmd+S (macOS).
  • Create another new file by clicking File then New File.
  • Name this file requirements.txt and press Enter.
  • Paste the following into requirements.txt:
flask
redis

What is requirements.txt?

This file lists the Python packages your application depends on. When Docker builds your image, it reads this file and installs both flask and redis inside the container.

  • Save requirements.txt.
  • Create one more file named .dockerignore.
  • Paste the following into .dockerignore:
.env
*.pyc
__pycache__
redis-data

What is .dockerignore?

This file works like .gitignore but for Docker. It tells Docker to exclude these files and folders when copying your project into the container image. This keeps sensitive files (like .env) and unnecessary data (like cached Python files) out of your image.

  • Save .dockerignore.
  • Check the Explorer sidebar in VS Code. You should see three files listed: app.py, requirements.txt, and .dockerignore.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

Here is the full content of each file for comparison.

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
.env
*.pyc
__pycache__
redis-data

Your development environment is ready and your application files are in place. Next up, you will write a Dockerfile that packages this application into a container image.

Write and Build Your Dockerfile

Your Flask app is written, but right now it can only run on a machine that has Python and all its dependencies manually installed. Docker solves this by packaging your app and its entire runtime environment into a single, portable image.

In this step, you'll write a Dockerfile that defines exactly how to build that image. Then you'll build the image and run it as a container to see what happens when a service is missing a dependency.

In this step, get ready to:
  • Write a Dockerfile that packages your Flask app into an image.
  • Build the image using docker build.
  • Run the container and observe the missing Redis dependency.
Create the Dockerfile

A Dockerfile is a recipe that tells Docker how to build your image, one instruction at a time. Each instruction creates a new layer in the image, and Docker caches unchanged layers to make future builds faster.

  • In VS Code's Explorer sidebar, right-click in the file list and select New File.
  • Name the file Dockerfile with no file extension.
  • Paste the following code into your new Dockerfile:
# 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?

  • FROM python:3.12-alpine starts from a lightweight Python image based on Alpine Linux.
  • WORKDIR /code sets the directory inside the container where all following commands run.
  • The two ENV lines configure Flask to find your app and listen on all network interfaces.
  • RUN apk add installs system-level C libraries that some Python packages need to compile.
  • Requirements are copied and installed BEFORE the app code. This means Docker can cache the dependency layer and skip re-installing packages when only your app code changes.
  • EXPOSE 5000 documents which port the container listens on.
  • CMD defines the default command that runs when the container starts.
  • Save Dockerfile by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file appears in your Explorer sidebar alongside app.py and requirements.txt.

Don't see Dockerfile in the sidebar?

  • Make sure the file has no extension. If VS Code added one automatically (like .txt), right-click and rename it to just Dockerfile.
  • Ensure you created the file inside the docker-portfolio folder, not a subfolder.
  • Help me troubleshoot my Dockerfile creation in VS Code.

✔️ Awesome, I've got everything!

Great. Make sure you've saved your Dockerfile before moving on to building it.

ⓧ I'd like to double check the full 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"]
Build the Docker image

With your Dockerfile saved, you can now tell Docker to read it and produce a container image. The image is like a snapshot of your app and everything it needs to run.

  • Build your Docker image by running this command in your PowerShell terminal:
docker build -t docker-portfolio-web .

What does this command do?

  • docker build tells Docker to read your Dockerfile and create an image.
  • -t docker-portfolio-web gives your image a human-readable name (a "tag") so you can reference it later.
  • The . at the end tells Docker to use the current directory as the build context (all the files Docker can access during the build).

The build will take a minute or two the first time as Docker downloads the Python base image and installs your dependencies. You'll see each Dockerfile instruction execute as a numbered step in the output.

  • Verify your image was created by running:
docker image ls
  • You should see docker-portfolio-web listed with a latest tag and a size of around 200-250 MB.

Build failed with an error?

  • Check that your PowerShell terminal is inside the docker-portfolio folder. Run pwd to confirm.
  • Make sure Docker Desktop is running (look for the whale icon in your system tray).
  • Verify your Dockerfile has no file extension. Run ls and look for Dockerfile (not Dockerfile.txt).
  • Help me debug my Docker build error.
Run your custom container

Your image is built. Now you'll start a container from it and see your Flask app respond to a web request.

  • Start a container from your image by running:
docker run -p 8000:5000 docker-portfolio-web

What is port mapping?

The -p 8000:5000 flag maps port 8000 on your machine to port 5000 inside the container. Flask listens on port 5000, so you access the app at http://localhost:8000 on your machine.

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

Seeing an error page? That's expected

Your Flask app tries to connect to a Redis database, but there is no Redis server running. The container is completely isolated, so it has no way to reach Redis on its own.

This is exactly why multi-container orchestration exists. In the next step, you'll use Docker Compose to run Flask and Redis together on the same network.

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

Browser can't connect to localhost:8000?

  • Make sure the container is still running. Check your terminal for Flask startup output that says "Running on http://0.0.0.0:5000".
  • Ensure no other application is using port 8000. If something else is on that port, stop the container with Ctrl+C and try a different host port: docker run -p 9000:5000 docker-portfolio-web.
  • Help me troubleshoot my Docker container not responding on localhost.

You've just built a custom Docker image from scratch and proven that a single container can't do everything alone. Next up, you'll use Docker Compose to bring Redis into the picture and make your full app work.

Orchestrate with Docker Compose

You've built a custom Docker image and proved it can run. But when you launched the container solo, the app crashed because it had no Redis database to connect to.

Docker Compose solves this by letting you define and run multiple containers from a single configuration file. In this step, you'll create a compose.yaml that wires your Flask app and a Redis database together into one working stack.

In this step, get ready to:
  • Create a compose.yaml and .env file for your multi-container stack.
  • Add health checks to ensure services start in the correct order.
  • Start the full stack and verify the hit counter works.
Create the .env and compose.yaml files

Docker Compose reads a .env file automatically to fill in variables referenced in your configuration. You'll create this first so that compose.yaml can use its values.

  • In VS Code, create a new file called .env in your docker-portfolio folder.
  • Paste the following configuration:
APP_PORT=8000
REDIS_HOST=redis
REDIS_PORT=6379

What do these variables do?

  • APP_PORT=8000 sets the host port your browser will use to reach the app.
  • REDIS_HOST=redis tells Flask which hostname to use for the Redis connection. Docker Compose creates a network where each service name becomes a hostname.
  • REDIS_PORT=6379 is the default port Redis listens on.
  • Save .env.

Now you'll create the main orchestration file that tells Docker how to run both containers together.

  • Create a new file called compose.yaml in your docker-portfolio folder.
  • Paste the following service definitions:
services:
  web:
    build: .
    ports:
      - "${APP_PORT}:5000"
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}

  redis:
    image: redis:alpine

What does this configuration do?

  • web builds from your Dockerfile and maps host port 8000 to container port 5000 (where Flask listens). It also passes the Redis connection details as environment variables.
  • redis pulls the public redis:alpine image from Docker Hub. No Dockerfile needed for standard database images.
  • The ${APP_PORT} syntax pulls values from your .env file automatically.
  • Save compose.yaml.

You won't run the stack yet. The next substep adds health checks to make sure Redis is ready before the web service tries to connect.

YAML syntax errors later?

YAML is sensitive to indentation. Make sure you use spaces (not tabs) and that each level is indented by exactly 2 spaces. If you see errors when you run docker compose up later, check your indentation first.

Help me fix a YAML indentation error in my compose.yaml

Add health checks and startup ordering

When Docker Compose starts multiple containers, they launch nearly simultaneously. If your web app tries to connect to Redis before Redis is ready to accept connections, it crashes.

A health check lets Docker periodically test whether a container is actually ready to serve traffic. You'll configure the web service to wait until Redis passes its health check before starting.

  • Replace the entire contents of compose.yaml with this updated version:
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
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

What did you add?

  • healthcheck on the redis service runs redis-cli ping every 5 seconds. If Redis responds with PONG, Docker marks it as healthy.
  • depends_on with condition: service_healthy tells Docker to hold off starting the web service until Redis is confirmed healthy.
  • start_period: 10s gives Redis 10 seconds of grace time before Docker starts counting failed checks against the retry limit.
  • Save compose.yaml.
  • Validate your configuration by running this command in PowerShell:
docker compose config

You should see your full resolved configuration printed to the terminal with all variables substituted (e.g. 8000:5000 instead of ${APP_PORT}:5000).

Seeing an error from docker compose config?

Check that your .env file is in the same folder as compose.yaml. Docker Compose looks for .env in the same directory where you run the command.

Also verify your YAML indentation uses 2 spaces per level, not tabs. The healthcheck block must be indented under redis, and depends_on must be indented under web.

Help me debug my docker compose config error

Start the stack and verify
  • Start both containers by running:
docker compose up

Watch the output in your terminal. You'll see Docker pull the Redis image, build your web image, then start both containers. The redis service becomes healthy first, then the web service launches.

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

You should see: Hello from Docker! I have been seen 1 time(s). Each time you refresh the page, the counter increments.

Why does it work now?

In the previous step, your Flask container crashed because no Redis was available. Now Docker Compose runs both containers on the same network, and the hostname redis (from your .env) resolves to the Redis container automatically.

  • Open a second PowerShell tab or window in the same directory.
  • View the live logs from both services by running:
docker compose logs -f

You should see interleaved logs from both the web and redis services. Each time you refresh your browser, a new request appears in the web logs.

  • Press Ctrl+C to stop following logs.
  • Stop the entire stack by running:
docker compose down

This stops and removes both containers and the network Docker Compose created for them.

App not loading at localhost:8000?

Check your terminal output for errors. If you see port is already allocated, another process is using port 8000. Stop the other process or change APP_PORT in your .env file to another port like 8080.

If the web service exits immediately, look for ModuleNotFoundError in the logs. This means your Dockerfile did not install dependencies correctly. Rebuild with docker compose build and try again.

Help me troubleshoot why my Docker Compose stack isn't working

✔️ Awesome, I've got everything!

Great. Double check you've saved both files, then move on to the next step.

ⓧ I'd like to double check the full code

Here's the full compose.yaml you should have:

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
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

And your .env file:

APP_PORT=8000
REDIS_HOST=redis
REDIS_PORT=6379

Your multi-container app is running and the services can talk to each other. Next up, you'll add persistent storage so your hit counter survives restarts, and learn how to inspect what's happening inside running containers.

Add Persistence and Inspect Your Stack

In the last step, you brought your multi-container app to life with Docker Compose. Both services started, communicated over a shared network, and your hit counter incremented on every page refresh.

There is a problem though. When you ran docker compose down, your visit count vanished. Containers are ephemeral by default. Any data stored inside a container disappears when that container is removed. In this step, you will add a named volume so your Redis data persists across restarts, then learn how to inspect and debug your running stack.

In this step, get ready to:
  • Add a named volume to your Redis service so data survives container restarts.
  • Test that persistence works by stopping and restarting your stack.
  • Use exec commands to debug and inspect environment variables, Redis data, and service status.
Add a named volume for Redis data

By default, data written inside a container lives only as long as that container exists. A named volume is Docker-managed storage that lives outside the container lifecycle. It stays on your machine even after containers are removed.

  • Open compose.yaml in VS Code.
  • Find the redis service. Between the image: redis:alpine line and the healthcheck: line, add a volumes: section.
  • At the very bottom of the file (outside the services: block), add a top-level volumes: declaration.
  • Update your compose.yaml to match the following:
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:

What does this code do?

redis-data:/data mounts a named volume called redis-data at the /data directory inside the Redis container. This is where Redis writes its data files.

The top-level volumes: declaration tells Docker Compose to create and manage this volume. It survives docker compose down but can be removed explicitly with the -v flag.

  • Save compose.yaml.

Seeing a YAML indentation error?

YAML is strict about indentation. Make sure volumes: under the redis service is indented at the same level as image: and healthcheck: (4 spaces). The top-level volumes: must be at the root level (0 spaces), same as services:.

Help me fix a YAML indentation error in my compose.yaml

Test persistence across restarts

The volume is configured, but you need to prove it actually works. You will build up a visit count, destroy the containers, bring them back, and confirm the count survived.

  • Start your stack in detached mode by running this command:
docker compose up -d

What does the -d flag do?

The -d flag runs your containers in "detached" mode. They run in the background so your terminal stays free for other commands.

  • Open your browser and visit http://localhost:8000.
  • Refresh the page 4-5 times to build up a hit count. Note the number you reach.
  • Stop and remove the containers by running:
docker compose down
  • Bring the stack back up by running:
docker compose up -d
  • Refresh http://localhost:8000 in your browser. The counter continues from where it left off instead of resetting to 1.

Your data persisted because the named volume kept Redis's data files alive on your machine, even though the container itself was destroyed and recreated.

Counter reset to 1?

Make sure you saved compose.yaml after adding the volume. If the volume was not mounted when the containers started, Redis had nowhere to persist its data.

Also check that the volume line reads redis-data:/data (not redis-data:/ or another path). Redis specifically writes to /data inside the container.

Help me figure out why my Redis data isn't persisting across restarts

Debug and inspect the running stack

In production, you cannot open a container and poke around its file system directly. Docker Compose gives you exec commands that let you run a single command inside an already-running container. This is essential for debugging.

  • Make sure your stack is still running (it should be from the previous substep). Verify the environment variables passed to your web container by running:
docker compose exec web env | findstr REDIS

You should see REDIS_HOST=redis and REDIS_PORT=6379 in the output. These confirm that your .env values are being injected into the running container.

  • Inspect the Redis data directly by running:
docker compose exec redis redis-cli GET hits

This returns the current value of the hits key. The number should match whatever count your browser last displayed.

  • Check the status of all services by running:
docker compose ps

You should see both services listed as "running" with their port mappings and health status displayed.

  • View the fully resolved configuration (with all .env variables substituted) by running:
docker compose config

What does docker compose config show?

This command outputs the final, merged version of your Compose file with all environment variable substitutions resolved. Where your file says ${APP_PORT}, the output shows the actual value 8000. This is useful for confirming that your .env file is being read correctly.

  • Finally, stop your stack and remove the named volume to start fresh by running:
docker compose down -v

What does the -v flag do here?

The -v flag removes named volumes along with the containers. Without it, your redis-data volume would stay on disk. Use -v when you want a truly clean slate.

Seeing "no container found" errors?

The docker compose exec command only works on running containers. If you already ran docker compose down, bring the stack back up first with docker compose up -d before trying exec commands.

Help me troubleshoot docker compose exec errors

✔️ Awesome, I've got everything!

Great. Double check you have saved compose.yaml with the volume configuration.

ⓧ I'd like to double check the full code

Here is the complete compose.yaml with the named volume added:

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:

Your containers now have persistent storage and you know how to peek inside them for debugging. Next up, you will document everything in a portfolio-ready README and initialize a Git repository.

Document Your Portfolio Project

You have a fully working multi-container application with health checks, environment configuration, and persistent data. The final piece is making sure anyone who finds this project can understand it at a glance.

In this step, you'll write a README documenting your architecture and initialize a Git repository to track your work.

In this step, get ready to:
  • Create a README explaining your Docker architecture.
  • Review your complete project structure.
  • Initialize a Git repository with a clean first commit.
Create a README.md

A README is the first thing people see when they visit your repository. It should explain what the project does, how to run it, and what concepts it demonstrates.

  • Create a new file called README.md in your docker-portfolio folder and paste the following content:
# Docker Portfolio Project: Multi-Container Flask + Redis App

A containerized web application demonstrating Docker and Docker Compose fundamentals.

## Architecture

This project runs two containers orchestrated by Docker Compose:

- **web** - A Python Flask application that serves a hit counter
- **redis** - A Redis database that stores the visit count

Both containers communicate over a Docker-managed network. Redis data is persisted using a named volume.

## How to Run

```bash
docker compose up
```

Then visit http://localhost:8000

To stop:

```bash
docker compose down
```

To stop and remove persisted data:

```bash
docker compose down -v
```

## Docker Concepts Demonstrated

- **Dockerfile**: Custom image built from python:3.12-alpine
- **Docker Compose**: Multi-service orchestration in a single YAML file
- **Health Checks**: Redis health check ensures startup ordering
- **Named Volumes**: Data persistence across container restarts
- **Environment Variables**: Configuration via .env file
- **Port Mapping**: Host port 8000 mapped to container port 5000

What does this README cover?

  • Architecture section explains the two-container setup and how they communicate over a Docker-managed network.
  • How to Run section gives anyone the exact commands to start, stop, and clean up the project.
  • Docker Concepts Demonstrated section lists every production pattern you implemented, making the project's value clear at a glance.
  • Save README.md.
  • Confirm the file exists by running:
ls

You should see README.md listed alongside your other project files.

✔️ Awesome, I've got everything!

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

ⓧ I'd like to double check the full code

# Docker Portfolio Project: Multi-Container Flask + Redis App

A containerized web application demonstrating Docker and Docker Compose fundamentals.

## Architecture

This project runs two containers orchestrated by Docker Compose:

- **web** - A Python Flask application that serves a hit counter
- **redis** - A Redis database that stores the visit count

Both containers communicate over a Docker-managed network. Redis data is persisted using a named volume.

## How to Run

```bash
docker compose up
```

Then visit http://localhost:8000

To stop:

```bash
docker compose down
```

To stop and remove persisted data:

```bash
docker compose down -v
```

## Docker Concepts Demonstrated

- **Dockerfile**: Custom image built from python:3.12-alpine
- **Docker Compose**: Multi-service orchestration in a single YAML file
- **Health Checks**: Redis health check ensures startup ordering
- **Named Volumes**: Data persistence across container restarts
- **Environment Variables**: Configuration via .env file
- **Port Mapping**: Host port 8000 mapped to container port 5000
Review your project structure

Before initializing Git, confirm that all the files you built throughout this project are in place.

  • List all files in your project folder by running:
ls

You should see these seven files: Dockerfile, compose.yaml, .env, app.py, requirements.txt, .dockerignore, and README.md.

Hidden files might not show

Files starting with a dot (like .env and .dockerignore) are hidden by default. Run ls -Force in PowerShell to see them all.

Initialize a Git repository

Git tracks changes to your project over time. Initializing a repository here means you can push this project to GitHub later and share it as a portfolio piece.

  • Create a .gitignore file in your docker-portfolio folder and paste the following content:
redis-data/
__pycache__/
*.pyc

What does .gitignore do?

Any file or folder listed here will be excluded from Git tracking. You don't want to commit the Redis data directory or Python cache files since those are generated at runtime and differ across machines.

  • Save .gitignore.

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before continuing.

ⓧ I'd like to double check the full code

redis-data/
__pycache__/
*.pyc
  • Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Docker portfolio project: multi-container Flask + Redis app"

What do these commands do?

  • git init creates a new Git repository in your project folder.
  • git add . stages all files (except those in .gitignore) for your first commit.
  • git commit -m "..." saves a snapshot of your project with a descriptive message.
  • Verify your commit by running:
git log --oneline

You should see one commit with the message Docker portfolio project: multi-container Flask + Redis app.

Seeing a 'git' is not recognized error?

Git may not be installed on your machine. Download it from https://git-scm.com/downloads and restart your terminal after installation.

If Git is installed but the commit fails with an identity error, run these two commands with your own name and email:

git config --global user.email "your-email@example.com"
git config --global user.name "Your Name"

help me fix this Git error

Your Docker project is now documented, version-controlled, and ready to share. Next up is a secret mission where you'll push your custom image to Docker Hub, making it pullable from anywhere.

Secret mission

Push Your Image to Docker Hub

Your Docker image lives on your machine right now. Push it to Docker Hub so it can be pulled from any machine, Kubernetes cluster, or Terraform-managed infrastructure.

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, so there are no ongoing cloud costs.

Resources you used:

  • Docker Desktop (installed application).
  • Docker Hub account (free tier).
  • Docker images: docker-portfolio-web, python:3.12-alpine, redis:alpine.
  • Named volume: redis-data.
  • Project folder: docker-portfolio (with all source files and Git repository).

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend this project soon.

  • Your containers are already stopped if you ran docker compose down at the end of Step 4.
  • Docker Desktop can remain installed and running in the background.
  • Your images stay cached locally for fast startup next time.

Pause - I'll come back to this later

Shut down Docker Desktop to free up memory and CPU while keeping all your files and images intact.

  • Make sure your containers are stopped by running:
docker compose down
  • Right-click the Docker whale icon in your system tray and select Quit Docker Desktop.
  • Your project folder, images, and volumes remain on disk. When you are ready to continue, open Docker Desktop and run docker compose up from your project folder.

Delete - I don't want to use this again

Remove all project resources and start fresh. This frees up disk space used by Docker images and volumes.

  • Stop containers and remove the named volume by running:
docker compose down -v
  • Remove the Docker images by running:
docker image rm docker-portfolio-web python:3.12-alpine redis:alpine
  • Delete the project folder by running:
cd ~/Desktop
Remove-Item -Recurse -Force docker-portfolio
  • If you no longer need Docker Desktop, uninstall it from Settings > Apps > Apps & features.
  • Your Docker Hub account is free and can remain active with no cost.

Nice Work!

Nice Work!

You just built a production-style containerized application from scratch. You now have a multi-container Flask and Redis app running with Docker Compose, complete with health checks, persistent storage, and a portfolio-ready repository.

Here's what you learned:

  • Wrote a Dockerfile from scratch to containerize a Python Flask application into a portable Docker image.
  • Orchestrated a multi-container app with Docker Compose, using health checks for reliable startup ordering and named volumes for data persistence.
  • Inspected and debugged running containers using docker compose exec, verified environment variables, and documented your architecture in a portfolio-ready README.
  • Pushed your custom image to Docker Hub, making it ready for Kubernetes or Terraform deployments.

Ready to quiz yourself?