Real World Docker Compose Example Web App with Database

Theory is useful, but seeing a complete working project brings everything together. This topic builds a realistic multi-container application from scratch: a Python Flask web application, a PostgreSQL database, a Redis cache, and an Nginx reverse proxy — all wired together with Docker Compose.

What You Are Building

Internet
   │
   ↓
┌──────────────────────────────────────────────────────┐
│  Nginx (port 80)  ← reverse proxy, handles HTTPS     │
│      │                                               │
│      ↓                                               │
│  Flask App        ← your Python web application      │
│      │        │                                      │
│      ↓        ↓                                      │
│  PostgreSQL   Redis  ← database + cache              │
└──────────────────────────────────────────────────────┘

Project Folder Structure

my-webapp/
├── compose.yaml
├── .env
├── nginx/
│   └── nginx.conf
└── app/
    ├── Dockerfile
    ├── requirements.txt
    └── app.py

The Flask Application

File: app/app.py

import os
import redis
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# Database config from environment variable
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL']
db = SQLAlchemy(app)

# Redis cache connection
cache = redis.Redis(host=os.environ['REDIS_HOST'], port=6379)

class Visit(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    path = db.Column(db.String(200))

@app.route('/')
def home():
    # Check Redis cache first
    count = cache.incr('visit_count')

    # Record in PostgreSQL
    visit = Visit(path='/')
    db.session.add(visit)
    db.session.commit()

    return jsonify({
        'message': 'Hello from Docker!',
        'visit_count': int(count)
    })

@app.route('/health')
def health():
    return jsonify({'status': 'ok'})

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', port=5000)

The App Dockerfile

File: app/Dockerfile

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]

File: app/requirements.txt

flask==3.0.0
flask-sqlalchemy==3.1.1
psycopg2-binary==2.9.9
redis==5.0.1

The Nginx Configuration

File: nginx/nginx.conf

events {}

http {
    upstream flask_app {
        server webapp:5000;    # "webapp" resolves via Docker DNS
    }

    server {
        listen 80;

        location / {
            proxy_pass http://flask_app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location /health {
            proxy_pass http://flask_app/health;
        }
    }
}

The Environment File

File: .env (never commit this to git)

POSTGRES_USER=appuser
POSTGRES_PASSWORD=strongpassword123
POSTGRES_DB=appdb
DATABASE_URL=postgresql://appuser:strongpassword123@db:5432/appdb
REDIS_HOST=redis

The compose.yaml — Everything Wired Together

File: compose.yaml

services:

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      webapp:
        condition: service_healthy
    networks:
      - frontend
    restart: unless-stopped

  webapp:
    build: ./app
    env_file: .env
    environment:
      DATABASE_URL: ${DATABASE_URL}
      REDIS_HOST: ${REDIS_HOST}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - frontend
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    networks:
      - backend
    restart: unless-stopped
    command: redis-server --appendonly yes

volumes:
  pgdata:
  redisdata:

networks:
  frontend:
  backend:

Starting the Application

cd my-webapp

# Start everything (builds the Flask image automatically):
docker compose up -d --build

# Watch the startup sequence:
docker compose logs -f

# Output you expect to see:
db      | PostgreSQL init process complete; ready for start up.
redis   | * Ready to accept connections
webapp  | * Running on http://0.0.0.0:5000
nginx   | start worker processes

# Test it:
curl http://localhost/
{"message": "Hello from Docker!", "visit_count": 1}

curl http://localhost/
{"message": "Hello from Docker!", "visit_count": 2}

How the Startup Order Works

Parallel start attempt:
  db     starts → healthcheck passes (pg_isready) ─────────┐
  redis  starts → service_started ────────────────────────┐ │
                                                           ↓ ↓
                                                         webapp starts
                                                           ↓
                                                    healthcheck passes
                                                    (curl /health)
                                                           ↓
                                                         nginx starts

Useful Day-to-Day Commands for This Stack

View all service status:
docker compose ps

Run a database migration manually:
docker compose exec webapp python manage.py db upgrade

Open a postgres shell:
docker compose exec db psql -U appuser -d appdb

Flush Redis cache:
docker compose exec redis redis-cli FLUSHALL

Rebuild only the webapp after code changes:
docker compose up -d --build webapp

Stop everything and delete volumes (full reset):
docker compose down -v

Key Points

  • Real apps use multiple services — Nginx, app, database, and cache each run in their own container.
  • Use depends_on with service_healthy and healthchecks to ensure services start in the correct order.
  • Store all secrets in a .env file and reference them in compose.yaml using ${VAR_NAME}.
  • Separate networks (frontend and backend) prevent Nginx from directly accessing the database — a clean security boundary.

Leave a Comment