Skip to main content

Command Palette

Search for a command to run...

Dockerize a Web App with Redis and PostgreSQL Using Docker Compose

Published
6 min read
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:

  1. Web Service (web): A Python Flask application.

  2. Caching Service (redis): A Redis server to increment and store a page view counter.

  3. 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.txt file 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 web service can connect to the redis service using the hostname redis and to the db service using the hostname db. Docker Compose's built-in networking automatically handles this DNS resolution.

  • depends_on: This tells Compose that the web service should start after the redis and db services 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 run docker-compose down and 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 ps

  • View logs for a specific service: docker-compose logs web

  • Run 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 database

    • SELECT version(); - Check the PostgreSQL version

    • \q - To quit and exit the PostgreSQL prompt

  • Stop the application: Press Ctrl+C in the original terminal where docker-compose up is running.

  • Stop and remove all containers, networks, but keep volumes: docker-compose down

  • Stop 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.yml file 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!