Dockerize a Web App with Redis and PostgreSQL Using Docker Compose

Introduction
So, you've mastered running a single container with docker run, and you've built your own images with a Dockerfile. That's a great start! But modern applications are rarely a single monolith. They're often composed of multiple, interconnected services—a web server, a database, a caching layer, a queue, etc.
Managing each of these services with manual docker run commands quickly becomes a nightmare of long, unwieldy commands and tangled networks. Enter Docker Compose.
Docker Compose is a tool for defining and running multi-container Docker applications. With a simple YAML file, you can configure all your application's services, networks, and volumes, and then spin them all up with a single command.
In this hands-on tutorial, we'll containerize a Python Flask web application that uses Redis for a simple page view counter and PostgreSQL to store data. By the end, you'll have a fully functional, self-contained development environment defined in code.
What We're Building
Our application stack will consist of three separate services:
Web Service (
web): A Python Flask application.Caching Service (
redis): A Redis server to increment and store a page view counter.Database Service (
db): A PostgreSQL server to persist our application data.
We'll use Docker Compose to manage them all together, ensuring they can talk to each other on a shared network and that our data survives container restarts.
Prerequisites
Docker Engine installed on your machine. Get Docker
Docker Compose (usually included with modern Docker Desktop installations).
Basic familiarity with the command line and Python (the concepts apply to any language).
Step 1: Setting Up the Project Structure
First, let's create a clean project directory and navigate into it. This is where all our code and configuration will live.
mkdir docker-flask-redis-postgres
cd docker-flask-redis-postgres
Step 2: The Flask Application (app.py & requirements.txt)
Create the main application file. This is a simple Flask app that increments a Redis counter on each visit.
cat > app.py << 'EOF'
from flask import Flask
import redis
import os
app = Flask(__name__)
# Environment variables will be provided by Docker Compose
redis_host = os.environ.get('REDIS_HOST', 'localhost')
redis_port = int(os.environ.get('REDIS_PORT', 6379))
redis_password = os.environ.get('REDIS_PASSWORD', None)
pg_host = os.environ.get('POSTGRES_HOST', 'localhost')
# Connect to Redis
redis_client = redis.Redis(
host=redis_host,
port=redis_port,
password=redis_password,
decode_responses=True # Converts responses to strings
)
@app.route('/')
def hello():
# Increment the page view counter in Redis
count = redis_client.incr('page_views')
return f'Hello World! This page has been viewed {count} times. PostgreSQL host is at: {pg_host}'
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
EOF
Create the file listing our Python dependencies.
cat > requirements.txt << 'EOF'
Flask==2.3.3
redis==4.6.0
psycopg2-binary==2.9.7
EOF

Step 3: Defining the Web Image (Dockerfile)
Create the Dockerfile that defines how to build the image for our web service.
cat > Dockerfile << 'EOF'
# Use an official Python runtime as a base image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the requirements file first (this leverages Docker's build cache)
COPY requirements.txt .
# Install any needed dependencies specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code into the container
COPY . .
# Expose port 5000 for the Flask app to run on
EXPOSE 5000
# Define environment variable for Flask
ENV FLASK_APP=app.py
ENV FLASK_ENV=development
# Run app.py when the container launches
CMD ["flask", "run", "--host=0.0.0.0"]
EOF
Key things to notice in this Dockerfile:
It starts from a slim, official Python image.
It copies the
requirements.txtfile and installs dependencies before copying the rest of the code. This is a crucial optimization that allows Docker to use its cache, making subsequent builds much faster if only your app code changes.It specifies the command to run your application.

Step 4: The Magic - docker-compose.yml
Create the heart of the project. This YAML file defines our entire application stack.
cat > docker-compose.yml << 'EOF'
version: '3.8'
# Define the services (containers) that make up our app
services:
# Our Flask application service
web:
build: . # Build the image from the Dockerfile in the current directory
ports:
- "5000:5000" # Map host port 5000 to container port 5000
environment:
- REDIS_HOST=redis # Use the service name 'redis' as the hostname
- REDIS_PORT=6379
- POSTGRES_HOST=db # Use the service name 'db' as the hostname
depends_on:
- redis
- db
volumes:
- .:/app # Mount our code as a volume for live development (optional)
# Our Redis service
redis:
image: "redis:alpine" # Use the official Redis Alpine image
command: redis-server --requirepass mysecretredispassword
environment:
- REDIS_PASSWORD=mysecretredispassword
volumes:
- redis_data:/data # Persist Redis data to a named volume
# Our PostgreSQL database service
db:
image: postgres:13-alpine
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=myuser
- POSTGRES_PASSWORD=mypassword
volumes:
- postgres_data:/var/lib/postgresql/data/ # Persist DB data to a named volume
# Define the volumes that our services will use.
# Docker Compose will create these automatically if they don't exist.
volumes:
redis_data:
postgres_data:
EOF
Key Things to Notice:
Service Names are Hostnames: The
webservice can connect to theredisservice using the hostnameredisand to thedbservice using the hostnamedb. Docker Compose's built-in networking automatically handles this DNS resolution.depends_on: This tells Compose that thewebservice should start after theredisanddbservices have started. (Note: It doesn't wait for the application inside the container to be ready, just the container itself.)Environment Variables: We pass configuration (like passwords) securely through environment variables defined in the YAML file.
Named Volumes (
redis_data,postgres_data): These ensure that our data persists even if we rundocker-compose downand remove the containers.
Step 5: Building and Running the Stack
Now for the payoff! From your project directory, run a single command:
docker-compose up --build
The --build flag tells Compose to build the web image from your Dockerfile before starting everything.
You'll see a stream of logs from all three services in your terminal. Once you see a line like web-1 | * Running on http://0.0.0.0:5000/, your application is ready!
Open your web browser and go to http://localhost:5000. You should see: Hello World! This page has been viewed 1 times. PostgreSQL host is at: db
Refresh the page, and the counter should increment!


Step 6: Managing the Stack
Open a new terminal window in the same directory to run these commands:
See running services:
docker-compose psView logs for a specific service:
docker-compose logs webRun a command inside a running service:
# Open a shell inside the 'web' container docker-compose exec web bash # Run a command in the 'db' container docker-compose exec db psql -U myuser -d mydb
\l- List all databases\dt- List all tables in the current databaseSELECT version();- Check the PostgreSQL version\q- To quit and exit the PostgreSQL prompt
Stop the application: Press
Ctrl+Cin the original terminal wheredocker-compose upis running.Stop and remove all containers, networks, but keep volumes:
docker-compose downStop and remove everything, including volumes (WARNING: deletes all data!):
docker-compose down -v
Conclusion
Congratulations! You've successfully defined and run a multi-service application using Docker Compose.
Let's recap what you've accomplished:
Isolation: Each service runs in its own isolated container.
Networking: Docker Compose automatically created a network allowing the services to discover each other by name.
Persistence: You used named volumes to ensure your Redis and PostgreSQL data survives container restarts and removal.
Reproducibility: Anyone with this
docker-compose.ymlfile and the source code can recreate your entire development environment with a single command. No more "but it works on my machine!"
This is the foundation for building more complex applications and moving towards production deployment with tools like Docker Swarm or Kubernetes. You can now experiment by adding more services (like nginx for a reverse proxy) or modifying the existing ones. Happy containerizing!



