Containerize a Python App with Docker
Build a multi-container Flask and Redis app orchestrated by Docker Compose.
Introduction
30 Second Summary
When you move to a new apartment, you pack everything into labeled boxes so movers can set it all up anywhere. Software works the same way. Containers package your app with everything it needs to run on any machine, every time.
In this project, you will containerize a Python web application and wire it to a Redis database using Docker Compose. You will write a Dockerfile, orchestrate multiple containers, and push your finished image to Docker Hub.
What You'll Build
A "Hit Counter" website that displays a styled HTML page showing how many times the page has been visited, powered by Flask and Redis running as separate containers orchestrated by Docker Compose.
By the end of this project, you'll have:
- A Dockerfile that builds a production-ready image from python:3.12-slim.
- A compose.yaml file that wires Flask and Redis together with service discovery, environment variables, and persistent volumes.
- Your custom image pushed to Docker Hub, ready to be pulled by any orchestrator.
- Secret Mission: Add health check endpoints that mirror Kubernetes liveness and readiness probes.
Are there any prerequisites?
You need basic Python knowledge, VS Code, Docker Desktop, Git, and a GitHub account. You will create a free Docker Hub account in Step 1.
Before We Start
Every container journey starts with the same foundation: a registry to store your images, a project to containerize, and a working Docker installation to build and run it all.
In this step, you will create your Docker Hub account, set up the project file structure, and confirm that Docker can pull and run images on your machine.
In this step, get ready to:
- Log in to Docker Hub from your terminal.
- Create the project directory with all required files.
- Verify Docker can pull and run images.
Create a Docker Hub account and log in
Docker Hub is a public registry where you store and share container images. You need an account so you can push your finished image later in the project.
- Open your browser and go to https://hub.docker.com.
- Click Sign Up and create a free account (or sign in if you already have one).
- Remember your Docker Hub username. You will use it to tag and push images.
What is Docker Hub?
Docker Hub is like GitHub, but for container images instead of source code. Anyone can pull your public images and run them on their own machine or cluster.
The free tier gives you unlimited public repositories and up to 200 pulls per 6 hours. That is more than enough for this project.
Now confirm that Docker Desktop is running and that the Docker CLI works.
- Make sure Docker Desktop is running. Look for the Docker whale icon in your menu bar (macOS) or system tray (Windows).
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Verify Docker is available by running this command:
docker --version
You should see a version number printed, confirming Docker is installed and running.
- Verify Docker Compose is available by running:
docker compose version
You should see a Compose version number. Docker Compose is included with Docker Desktop, so no separate install is needed.
Command not found?
If you see a "command not found" error, Docker Desktop is not running or not installed. Open Docker Desktop from your Applications folder (macOS) or Start Menu (Windows) and wait for it to finish starting up.
If you do not have Docker Desktop installed, download it from https://www.docker.com/products/docker-desktop/ and run the installer.
help me fix Docker not being recognized in my terminal
- Log in to Docker Hub from your terminal by running:
docker login
What happens when you run this?
Docker will open a browser window or display a one-time device code and ask you to visit a URL to authenticate. Follow the prompts to complete the login.
Once authenticated, your terminal will display "Login Succeeded." This stores credentials locally so you can push images later without re-entering your password.
Login failed?
Make sure you are using the username and password (or personal access token) from your Docker Hub account, not your email address.
help me troubleshoot docker login failing in my terminal
Create the project directory and file structure
Your project needs a clear file structure before you start writing code. Each file serves a specific purpose in the containerization workflow.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder, the templates subdirectory, and all required files by running these commands:
mkdir hit-counter
cd hit-counter
mkdir templates
touch app.py requirements.txt Dockerfile .dockerignore .gitignore
touch templates/index.html
What are these files for?
app.py will hold your Flask web application. requirements.txt lists the Python packages Docker will install inside the container.
Dockerfile tells Docker how to build your image. compose.yaml (created in a later step) will wire multiple containers together.
.dockerignore excludes files from your Docker build context. .gitignore excludes files from version control. templates/index.html is the HTML page your app will render.
- Confirm the files exist by running:
ls
You should see Dockerfile, app.py, requirements.txt, templates, .dockerignore, and .gitignore in the output.
Hidden files in ls
Files starting with a dot (like .dockerignore) are hidden by default. Run ls -a to see them all.
- Initialize a Git repository in your project folder by running:
git init
You should see a message confirming the repository was initialized. This sets up version control so you can track changes and push to GitHub later.
Don't see all the files?
Make sure you ran the touch commands from inside the hit-counter folder. If you are unsure where you are, run pwd to print your current directory.
help me figure out why my project files are missing or in the wrong place
Verify Docker can pull and run images
Before you start building your own image, confirm Docker can communicate with Docker Hub and run containers.
- Run the Docker hello-world test by running this command:
docker run --rm hello-world
What does this command do?
docker run pulls an image from Docker Hub (if not already local) and starts a container from it. The --rm flag automatically removes the container after it exits, keeping your system clean.
hello-world is a tiny official image that prints a confirmation message and exits. It proves Docker can pull from the registry, create a container, and run it successfully.
You should see a message starting with "Hello from Docker!" followed by details about how the message was generated.
Seeing a permission or connection error?
Make sure Docker Desktop is fully started (the whale icon should be steady, not animating). On Linux, you may need to prefix with sudo or add your user to the docker group.
help me fix Docker permission or connection errors when running docker run hello-world
- Open VS Code with your project folder by running:
code .
VS Code opens with the hit-counter folder loaded in the Explorer sidebar. You are ready to start writing code.
code command not found?
In VS Code, press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) and type "Shell Command: Install 'code' command in PATH". Select it and restart your terminal.
help me set up the code command so I can open VS Code from the terminal
Your environment is locked and loaded. Next up, you will write the Flask web app and its Dockerfile.
Write the Flask App and Dockerfile
Your project folder is ready with empty files waiting for code. Now you need to write the application that counts page visits and package it into a portable container image.
In this step, you will write a Flask web app that talks to Redis, create an HTML template to display the count, then write a Dockerfile that packages everything into a single image.
In this step, get ready to:
- Write the Flask application and HTML template.
- Create the Dockerfile and dependency file.
- Build your first custom Docker image.
Write the Flask application
The Flask app is the heart of your project. It handles incoming web requests, increments a counter in Redis, and renders an HTML page showing the current count.
- In VS Code, open app.py inside your hit-counter folder and paste the following code:
import os
from flask import Flask, render_template
import redis
app = Flask(__name__)
r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379, decode_responses=True)
@app.route("/")
def home():
count = r.incr("hits:home")
return render_template("index.html", visits=count)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
This file creates a Flask web server with a single route at /.
- redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), ...) connects to a Redis server. The hostname comes from an environment variable, defaulting to redis. Docker Compose will set this for us later.
- r.incr("hits:home") increments a counter stored in Redis every time someone visits the page.
- render_template("index.html", visits=count) renders an HTML template and passes the current count into it.
- app.run(host="0.0.0.0", port=5000) starts the server on all network interfaces inside the container so Docker can forward traffic to it.
- Save app.py.
You will not see a change yet because the app needs Redis running to start. The next file gives the app something to render.
- Open templates/index.html and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hit Counter</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.1);
padding: 3rem;
border-radius: 1rem;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.count {
font-size: 5rem;
font-weight: bold;
margin: 1rem 0;
}
p {
font-size: 1.2rem;
opacity: 0.9;
}
.tech {
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome!</h1>
<p>This page has been visited</p>
<div class="count">{{ visits }}</div>
<p>times</p>
<div class="tech">Powered by Flask + Redis | Running in Docker</div>
</div>
</body>
</html>
What does this template do?
This is a Jinja2 template that Flask renders at runtime. The {{ visits }} placeholder gets replaced with the actual visit count from Redis every time the page loads.
The CSS creates a purple gradient background with a frosted-glass card in the center displaying the counter in large text.
- Save templates/index.html.
✔️ Awesome, I've got everything!
Great. Make sure both app.py and templates/index.html are saved.
ⓧ I'd like to double check the full code
Here is app.py:
import os
from flask import Flask, render_template
import redis
app = Flask(__name__)
r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379, decode_responses=True)
@app.route("/")
def home():
count = r.incr("hits:home")
return render_template("index.html", visits=count)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Here is templates/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hit Counter</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.1);
padding: 3rem;
border-radius: 1rem;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.count {
font-size: 5rem;
font-weight: bold;
margin: 1rem 0;
}
p {
font-size: 1.2rem;
opacity: 0.9;
}
.tech {
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome!</h1>
<p>This page has been visited</p>
<div class="count">{{ visits }}</div>
<p>times</p>
<div class="tech">Powered by Flask + Redis | Running in Docker</div>
</div>
</body>
</html>
Create the requirements file and Dockerfile
Docker needs to know which Python packages to install inside the image. You list them in requirements.txt, then write a Dockerfile that tells Docker how to assemble everything into a runnable image.
- Open requirements.txt and paste the pinned dependencies:
flask==3.1.3
redis==5.2.1
- Save requirements.txt.
- Open .dockerignore and paste the following patterns to keep unnecessary files out of the Docker build context:
.git
__pycache__
venv/
.env
*.pyc
Why do I need a .dockerignore?
When you run docker build, Docker sends all files in the current directory (the "build context") to the build engine. A .dockerignore file tells Docker to skip files that do not belong in the image, like your .git history or Python bytecode.
This keeps images smaller and prevents secrets in .env from leaking into the image.
- Save .dockerignore.
- Open .gitignore and paste the following patterns:
__pycache__/
venv/
.env
*.pyc
- Save .gitignore.
- Now open the Dockerfile and paste the following image definition:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
What does each Dockerfile instruction do?
- FROM python:3.12-slim starts from a lightweight Debian-based Python image (~41 MB compressed).
- WORKDIR /app sets the working directory inside the container.
- COPY requirements.txt . copies only the dependency file first. This is a layer caching trick. Docker re-uses this layer on rebuilds unless requirements.txt changes.
- RUN pip install --no-cache-dir -r requirements.txt installs Flask and redis-py without storing pip's download cache, keeping the image lean.
- COPY . . copies the rest of your application code into the image.
- EXPOSE 5000 documents which port the app listens on.
- CMD ["python", "app.py"] sets the default command that runs when the container starts.
- Save the Dockerfile.
Why copy requirements.txt separately?
Docker builds images in layers. If you copy all files first, changing any file invalidates the pip install cache. By copying requirements.txt alone first, Docker only re-runs pip install when your dependencies actually change.
This pattern saves minutes on every rebuild during development.
✔️ Awesome, I've got everything!
Make sure requirements.txt, .dockerignore, .gitignore, and the Dockerfile are all saved.
ⓧ I'd like to double check the full code
Here is requirements.txt:
flask==3.1.3
redis==5.2.1
Here is .dockerignore:
.git
__pycache__
venv/
.env
*.pyc
Here is .gitignore:
__pycache__/
venv/
.env
*.pyc
Here is the Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Build your Docker image
With all the source files in place, you can now ask Docker to read your Dockerfile and assemble everything into an image.
- In your terminal, make sure you are inside the hit-counter folder, then build the image by running this command:
docker build -t hit-counter .
What does this command do?
- docker build reads the Dockerfile in the current directory and executes each instruction to create an image.
- -t hit-counter tags (names) the resulting image so you can refer to it easily later.
- . (the dot) tells Docker to use the current directory as the build context. Docker sends these files to the build engine.
The build takes 10-30 seconds the first time because Docker downloads the python:3.12-slim base image. You should see output ending with FINISHED and a line naming your image.
- Verify the image was created by running:
docker images
You should see hit-counter in the REPOSITORY column with a latest tag and a size around 150-180 MB.
Build failed or image not showing?
- Make sure Docker Desktop is running. You can check by running docker --version in your terminal.
- Confirm you are inside the hit-counter folder (the one containing the Dockerfile). Run ls and verify you see Dockerfile in the output.
- If you see a pip error about packages not found, double check that requirements.txt has the exact lines flask==3.1.3 and redis==5.2.1 with no extra spaces.
Help me troubleshoot my Docker build failure.
Your application code is written and packaged into a Docker image. Next up, you will wire Flask and Redis together using Docker Compose so the app can actually run and count visits.
Compose the Multi-Container Application
You have a working Docker image for your Flask app. However, your app needs Redis to store visit counts, and running two separate containers by hand (with the right networking flags) gets tedious fast.
Docker Compose lets you define your entire multi-container application in a single file. One command spins up both services, connects them on a shared network, and attaches persistent storage.
In this step, get ready to:
- Create a compose.yaml file that defines your Flask and Redis services.
- Start both containers with a single command and verify the counter works.
- Prove that Redis data persists across container restarts.
Create the compose.yaml file
A compose.yaml file declares every service your application needs, how they connect, and where they store data. Docker Compose reads this file and creates the containers, networks, and volumes for you.
- In VS Code, create a new file called compose.yaml in the root of your hit-counter folder (the same level as your Dockerfile).
- Paste the following into compose.yaml:
services:
web:
build: .
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
depends_on:
- redis
redis:
image: redis:7.4-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
volumes:
redis-data:
What does this file do?
- web builds from your local Dockerfile, maps port 5000 on your machine to port 5000 in the container, and injects REDIS_HOST=redis as an environment variable so your Flask app knows how to find Redis.
- redis pulls the official redis:7.4-alpine image from Docker Hub and mounts a named volume (redis-data) at /data so that stored keys survive restarts.
- depends_on tells Compose to start Redis before the web service.
- The volumes: section at the bottom declares the named volume. Without this, data would vanish every time you stop the containers.
Why does the service name work as a hostname?
Docker Compose automatically creates a network for your project. Each service name (web, redis) becomes a DNS hostname on that network. When your Flask app connects to host redis, Docker resolves it to the Redis container's IP address.
This is the same service-discovery pattern Kubernetes uses. In Kubernetes, service names also resolve via internal DNS.
- Save compose.yaml.
YAML indentation errors?
- YAML is whitespace-sensitive. Use exactly 2 spaces per indent level (no tabs).
- Make sure the volumes: key at the bottom of the file is at the same indent level as services: (zero indent).
- Check that list items (lines starting with -) have a space after the dash.
Help me fix a YAML indentation error in my compose.yaml
✔️ Awesome, I've got everything!
Great. Double check you have saved compose.yaml in the same folder as your Dockerfile.
ⓧ I'd like to double check the full code
Here is the complete compose.yaml file:
services:
web:
build: .
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
depends_on:
- redis
redis:
image: redis:7.4-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
volumes:
redis-data:
Start the application with Docker Compose
With one command, Docker Compose builds your image (if needed), creates containers for both services, and connects them on a shared network.
- Start both services by running this command in your terminal:
docker compose up
What should I see?
Your terminal shows the aggregated logs from both containers. You will see Redis announce it is ready to accept connections, followed by Flask printing its startup URL. The terminal stays attached to the logs.
- Open your browser and visit http://localhost:5000.
- Refresh the page several times. Each refresh increments the visit counter on the styled page.
You should see the purple gradient page with a large number in the center that goes up by one every time you refresh.
Page not loading at localhost:5000?
- Check your terminal for error messages. If you see a connection refused error for Redis, make sure REDIS_HOST=redis is set in the environment section of your compose.yaml.
- If port 5000 is already in use, stop any other process using that port (on macOS, AirPlay Receiver uses port 5000 by default. Disable it in System Settings > General > AirDrop & Handoff).
- Make sure Docker Desktop is still running.
Help me debug why my Docker Compose app is not loading at localhost:5000
Verify data persistence across restarts
Named volumes keep data alive even when containers are destroyed and recreated. This is the same concept as Kubernetes Persistent Volumes.
- Return to your terminal where docker compose up is running.
- Press Ctrl+C to stop both containers.
- Note the visit count you saw on your last page refresh (for example, 5).
- Start the containers again by running:
docker compose up
- Visit http://localhost:5000 again in your browser.
The counter picks up where it left off instead of resetting to 1. Your redis-data named volume preserved the data between restarts.
Why does this matter for production?
Without a named volume, every docker compose down would wipe your database. Named volumes decouple data from container lifecycles.
In Kubernetes, the equivalent concept is a PersistentVolume and PersistentVolumeClaim. The mental model is the same: declare storage separately from the workload that uses it.
Counter reset to 1?
- If you used docker compose down -v instead of Ctrl+C, the -v flag deletes volumes. Use plain Ctrl+C or docker compose down (without -v) to keep your data.
- Confirm the volume exists by running docker volume ls in a new terminal. You should see a volume ending in redis-data.
Help me understand why my Redis data did not persist after restarting Docker Compose
Your multi-container app is running, connected, and persisting data. Next up, you will push your custom image to Docker Hub so it can be pulled by any machine or orchestrator.
Push Your Image to Docker Hub
You have a working multi-container app running locally. That is great for development, but the real power of containers comes from portability.
In this step, you will push your custom image to Docker Hub so it can be pulled by any machine, any teammate, or any orchestrator like Kubernetes. You will also commit your project to Git and push it to GitHub for version control.
In this step, get ready to:
- Tag and push your Docker image to Docker Hub.
- Verify the push by pulling the image from scratch.
- Commit your project to Git and push to GitHub.
Tag and push your image to Docker Hub
Docker Hub identifies images using a naming convention that includes your username and a version tag. You need to re-tag your local image to match this format before pushing.
- First, stop any running containers by running:
- Tag your local hit-counter image for Docker Hub by running this command:
docker compose down
- Tag your local hit-counter image for Docker Hub by running this command:
docker tag hit-counter [[DOCKERHUB_USERNAME="your-dockerhub-username"]]/hit-counter:1.0
What does this command do?
The docker tag command creates a new reference to your existing image. It does not copy anything. It adds a name that Docker Hub can recognize.
The format is namespace/repository:tag. Your Docker Hub username is the namespace, hit-counter is the repository, and 1.0 is the version tag.
- Push the tagged image to Docker Hub by running:
docker push [[DOCKERHUB_USERNAME="your-dockerhub-username"]]/hit-counter:1.0
You should see Docker uploading each layer of your image. Once complete, a digest and size summary appear.
What is happening during the push?
Docker pushes each image layer individually. Layers that already exist on Docker Hub (like the base python:3.12-slim layers) are skipped. Only your application-specific layers are uploaded.
Seeing 'denied: requested access to the resource is denied'?
This means you are not logged in to Docker Hub. Run docker login and enter your Docker Hub username and password, then try the push again.
Also check that the username in your tag matches your Docker Hub username exactly (case-sensitive).
help me fix the Docker Hub push access denied error
Verify the push by pulling from scratch
Pushing an image does not prove it works. You need to verify that someone else (or a fresh machine) can pull and use it.
- Open Docker Hub in your browser and navigate to your repositories.
- Select **My Hub**, then **Repositories**.
- Click on hit-counter and select the **Tags** tab.
- Confirm you see the 1.0 tag listed.
- Now simulate a fresh pull. Remove the local tagged image by running:
docker rmi [[DOCKERHUB_USERNAME="your-dockerhub-username"]]/hit-counter:1.0
- Pull the image back from Docker Hub by running:
docker pull [[DOCKERHUB_USERNAME="your-dockerhub-username"]]/hit-counter:1.0
You should see Docker downloading the image layers. Once complete, the image is back on your machine, pulled directly from the registry.
Why does this matter?
This pull-from-scratch test proves your image is portable. Any machine with Docker installed can now run your app with a single docker pull command. This is exactly how Kubernetes deploys containers in production.
Pull failed or image not found?
Make sure the repository is public on Docker Hub. Private repositories require authentication on the pulling machine.
Double-check the image name and tag are spelled exactly as they appear on your Docker Hub **Tags** page.
help me troubleshoot my Docker Hub pull
Commit and push to GitHub
Your project has a .gitignore and .dockerignore already in place from earlier. These files keep unnecessary artifacts out of your repository and your Docker image respectively.
What do these ignore files do?
The .gitignore prevents Python bytecode, virtual environments, and secrets from being committed to Git. The .dockerignore prevents those same files (plus the .git folder) from being copied into your Docker image during builds.
- Create a new repository on GitHub. Open GitHub in your browser.
- Click the **+** icon in the top right, then select **New repository**.
- Name the repository hit-counter and leave it public.
- Do not add a README, .gitignore, or license (your project already has what it needs).
- Click **Create repository**.
- Back in your terminal, stage and commit all your project files by running these commands:
git add .
git commit -m "Initial commit: Hit Counter Docker Compose app"
- Connect your local repository to GitHub and push by running these commands:
git branch -M main
git remote add origin [[GITHUB_REPO_URL="https://github.com/your-username/hit-counter.git"]]
git push -u origin main
You should see Git upload your files to GitHub. Visit your repository page on GitHub to confirm all files appear.
Git push rejected or authentication error?
If you see an authentication error, make sure you are using a personal access token or SSH key with GitHub. GitHub no longer accepts plain passwords for Git operations.
If the push is rejected because the remote contains work you do not have locally, make sure you did not add a README or license when creating the GitHub repository.
help me fix my Git push to GitHub
Secret mission
Add Health Checks
In production, orchestrators like Kubernetes need to know if your containers are actually working, not just running. Can you add health check endpoints and configure Docker Compose to monitor container health automatically?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine using Docker containers. Docker Hub's free tier has no ongoing storage costs for public repositories, so there is no bill ticking up while you decide.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers (web and redis services running via Docker Compose).
- Docker images (hit-counter, python:3.12-slim, redis:7.4-alpine stored locally).
- Docker named volume (redis-data persisting your visit counter).
- Docker Hub repository (your-dockerhub-username/hit-counter:1.0 image).
- Local project files (the hit-counter/ folder on your Desktop).
- GitHub repository (your pushed project code).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend this project soon.
- Your containers continue running and the hit counter keeps working at http://localhost:5000.
- Your Docker Hub image stays available for pulling from any machine.
- Your GitHub repository stays intact for future reference.
Pause - I'll come back to this later
Stop your running containers to free up system memory. Your images, volumes, and code stay on disk so you can restart quickly later.
- Stop the containers by running this from your hit-counter folder:
- To restart later, navigate back to the folder and run docker compose up.
docker compose down
This stops and removes the containers and network but keeps your images, volumes, and code intact.
Delete - I don't want to use this again
Remove all project resources from your machine and cloud accounts. This is permanent.
- Stop containers, remove the network, and delete the named volume by running this from your hit-counter folder:
docker compose down -v
- Remove the local Docker images by running:
docker rmi hit-counter
docker rmi redis:7.4-alpine
docker rmi python:3.12-slim
- Delete the local project folder. On macOS/Linux:
cd ~/Desktop
rm -rf hit-counter
On Windows:
cd $HOME\Desktop
rmdir /s /q hit-counter
- Delete your Docker Hub repository. Go to hub.docker.com, open your hit-counter repository, click the Settings tab, scroll down, and click Delete repository. Confirm by typing the repository name.
- Delete your GitHub repository. Open your repository page on github.com, click Settings, scroll to the Danger Zone section, and click Delete this repository. Follow the confirmation prompts to finalize.
Nice Work!
Nice Work!
You just containerized a multi-service Python application and shipped it to a registry. Your hit counter website runs Flask and Redis as separate containers, orchestrated by Docker Compose, with persistent data and proper networking.
You've learned how to:
- Write a Dockerfile that builds a production-ready image from python:3.12-slim, using layer caching to speed up rebuilds.
- Author a compose.yaml that wires multiple containers together with service discovery, environment variables, and named volumes for persistent data.
- Push a custom image to Docker Hub, making it pullable by any orchestrator or team member.
- Secret Mission: Added container health checks that mirror the same liveness and readiness probe pattern used in Kubernetes.
Ready to quiz yourself?