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_onwithservice_healthyand healthchecks to ensure services start in the correct order. - Store all secrets in a
.envfile and reference them incompose.yamlusing${VAR_NAME}. - Separate networks (
frontendandbackend) prevent Nginx from directly accessing the database — a clean security boundary.
