Skip to main content
CleanStart

Knowledge Hub

Docker-Compose Examples: Complete Stacks

Docker-compose lets you define and run multi-container applications locally. These examples use CleanStart base images for secure, production-like environments.

Example 1: Web App + PostgreSQL + Redis (Classic Stack)

The most common three-tier architecture consists of a web app at the top, a database in the middle, and a cache layer at the bottom.

Create docker-compose.yml:

version: '3.8' services:  # Web Application (Python Flask)  web:    image: cleanstart/python:3.11-prod    container_name: myapp_web    working_dir: /app    volumes:      - ./app:/app      - ./requirements.txt:/app/requirements.txt    environment:      - FLASK_APP=app.py      - DATABASE_URL=postgresql://dbuser:dbpass@db:5432/mydb      - REDIS_URL=redis://cache:6379/0      - PYTHONUNBUFFERED=1    command: >      sh -c 'pip install --user -q -r requirements.txt &&             python -m flask run --host=0.0.0.0'    ports:      - "5000:5000"    depends_on:      db:        condition: service_healthy      cache:        condition: service_healthy    healthcheck:      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]      interval: 10s      timeout: 5s      retries: 3      start_period: 10s    networks:      - backend    restart: unless-stopped    security_opt:      - no-new-privileges:true   # PostgreSQL Database  db:    image: postgres:15-alpine    container_name: myapp_db    environment:      - POSTGRES_USER=dbuser      - POSTGRES_PASSWORD=dbpass      - POSTGRES_DB=mydb    volumes:      - postgres_data:/var/lib/postgresql/data      - ./init.sql:/docker-entrypoint-initdb.d/init.sql    ports:      - "5432:5432"    healthcheck:      test: ["CMD-SHELL", "pg_isready -U dbuser"]      interval: 10s      timeout: 5s      retries: 5    networks:      - backend    restart: unless-stopped    security_opt:      - no-new-privileges:true   # Redis Cache  cache:    image: redis:7-alpine    container_name: myapp_cache    command: redis-server --appendonly yes    volumes:      - redis_data:/data    ports:      - "6379:6379"    healthcheck:      test: ["CMD", "redis-cli", "ping"]      interval: 10s      timeout: 5s      retries: 5    networks:      - backend    restart: unless-stopped    security_opt:      - no-new-privileges:true volumes:  postgres_data:    driver: local  redis_data:    driver: local networks:  backend:    driver: bridge

Create .env file:

# .envPOSTGRES_USER=dbuserPOSTGRES_PASSWORD=dbpassPOSTGRES_DB=mydbFLASK_ENV=developmentREDIS_URL=redis://cache:6379/0

Create init.sql (database initialization):

-- Initial schemaCREATE TABLE IF NOT EXISTS users (    id SERIAL PRIMARY KEY,    username VARCHAR(50) UNIQUE NOT NULL,    email VARCHAR(100) NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); INSERT INTO users (username, email) VALUES ('admin', 'admin@example.com');

Create app.py (Flask application):

#!/usr/bin/env python3from flask import Flask, jsonifyimport osimport redisfrom sqlalchemy import create_engine, text app = Flask(__name__) # Database connectiondb_url = os.getenv('DATABASE_URL', 'postgresql://dbuser:dbpass@localhost:5432/mydb')engine = create_engine(db_url) # Redis connectionredis_url = os.getenv('REDIS_URL', 'redis://localhost:6379/0')cache = redis.from_url(redis_url) @app.route('/health', methods=['GET'])def health():    return jsonify({"status": "healthy"}), 200 @app.route('/api/users', methods=['GET'])def get_users():    try:        with engine.connect() as connection:            result = connection.execute(text("SELECT id, username, email FROM users"))            users = [dict(row) for row in result]        return jsonify({"users": users}), 200    except Exception as e:        return jsonify({"error": str(e)}), 500 @app.route('/api/cache-test', methods=['GET'])def cache_test():    try:        cache.set('test_key', 'Hello from Redis!', ex=60)        value = cache.get('test_key')        return jsonify({"cached_value": value.decode()}), 200    except Exception as e:        return jsonify({"error": str(e)}), 500 if __name__ == '__main__':    app.run(host='0.0.0.0', port=5000, debug=False)

Create requirements.txt:

Flask==2.3.0SQLAlchemy==2.0.0psycopg2-binary==2.9.0redis==4.5.0

Startup commands:

# Start all servicesdocker-compose up -d # View logsdocker-compose logs -f web # Check statusdocker-compose ps # Expected output:# NAME          IMAGE                     STATUS# myapp_web     cleanstart/python:3.11    Up (healthy)# myapp_db      postgres:15-alpine        Up (healthy)# myapp_cache   redis:7-alpine            Up (healthy) # Test the appcurl http://localhost:5000/health# {"status":"healthy"} curl http://localhost:5000/api/users# {"users": [{"id": 1, "username": "admin", "email": "admin@example.com"}]} curl http://localhost:5000/api/cache-test# {"cached_value":"Hello from Redis!"} # Stop all servicesdocker-compose down

Example 2: Kafka Cluster (Event Streaming)

A complete Kafka cluster with KRaft eliminates the need for Zookeeper and simplifies operational management.

Create docker-compose.yml:

version: '3.8' services:  # Kafka Broker 1  kafka-1:    image: confluentinc/cp-kafka:7.5.0    container_name: kafka-1    environment:      KAFKA_BROKER_ID: 1      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,BROKER:PLAINTEXT      KAFKA_ADVERTISED_LISTENERS: BROKER://kafka-1:29092,PLAINTEXT://localhost:9092      KAFKA_PROCESS_ROLES: broker      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3      KAFKA_JMX_PORT: 9101      KAFKA_JMX_HOSTNAME: kafka-1      KAFKA_INTER_BROKER_LISTENER_NAME: BROKER      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER      KAFKA_LOG4J_ROOT_LOGLEVEL: WARN      CLUSTER_ID: MkwNYEm3OTiUiS4ieJFzhA    volumes:      - kafka1_data:/var/lib/kafka/data    ports:      - "9092:9092"      - "9101:9101"    networks:      - kafka-net    restart: unless-stopped    healthcheck:      test: ["CMD", "kafka-broker-api-versions.sh", "--bootstrap-server=localhost:9092"]      interval: 10s      timeout: 5s      retries: 5   # Kafka Broker 2  kafka-2:    image: confluentinc/cp-kafka:7.5.0    container_name: kafka-2    environment:      KAFKA_BROKER_ID: 2      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,BROKER:PLAINTEXT      KAFKA_ADVERTISED_LISTENERS: BROKER://kafka-2:29092,PLAINTEXT://localhost:9093      KAFKA_PROCESS_ROLES: broker      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3      KAFKA_JMX_PORT: 9101      KAFKA_JMX_HOSTNAME: kafka-2      KAFKA_INTER_BROKER_LISTENER_NAME: BROKER      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER      KAFKA_LOG4J_ROOT_LOGLEVEL: WARN      CLUSTER_ID: MkwNYEm3OTiUiS4ieJFzhA    volumes:      - kafka2_data:/var/lib/kafka/data    ports:      - "9093:9093"      - "9102:9101"    networks:      - kafka-net    restart: unless-stopped    healthcheck:      test: ["CMD", "kafka-broker-api-versions.sh", "--bootstrap-server=localhost:9093"]      interval: 10s      timeout: 5s      retries: 5   # Kafka Broker 3  kafka-3:    image: confluentinc/cp-kafka:7.5.0    container_name: kafka-3    environment:      KAFKA_BROKER_ID: 3      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,BROKER:PLAINTEXT      KAFKA_ADVERTISED_LISTENERS: BROKER://kafka-3:29092,PLAINTEXT://localhost:9094      KAFKA_PROCESS_ROLES: broker      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3      KAFKA_JMX_PORT: 9101      KAFKA_JMX_HOSTNAME: kafka-3      KAFKA_INTER_BROKER_LISTENER_NAME: BROKER      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:29093,2@kafka-2:29093,3@kafka-3:29093      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER      KAFKA_LOG4J_ROOT_LOGLEVEL: WARN      CLUSTER_ID: MkwNYEm3OTiUiS4ieJFzhA    volumes:      - kafka3_data:/var/lib/kafka/data    ports:      - "9094:9094"      - "9103:9101"    networks:      - kafka-net    restart: unless-stopped    healthcheck:      test: ["CMD", "kafka-broker-api-versions.sh", "--bootstrap-server=localhost:9094"]      interval: 10s      timeout: 5s      retries: 5   # Kafka UI (optional, for monitoring)  kafka-ui:    image: provectuslabs/kafka-ui:latest    container_name: kafka-ui    environment:      KAFKA_CLUSTERS_0_NAME: local      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:29092,kafka-2:29092,kafka-3:29092      KAFKA_CLUSTERS_0_JMXPORT: 9101    ports:      - "8080:8080"    networks:      - kafka-net    depends_on:      - kafka-1      - kafka-2      - kafka-3    restart: unless-stopped volumes:  kafka1_data:  kafka2_data:  kafka3_data: networks:  kafka-net:    driver: bridge

Startup and test:

# Start the clusterdocker-compose up -d # Wait for brokers to be healthy (30 seconds)sleep 30 # Create a topicdocker-compose exec kafka-1 kafka-topics.sh \  --create \  --topic my-events \  --bootstrap-server kafka-1:29092 \  --partitions 3 \  --replication-factor 3 # List topicsdocker-compose exec kafka-1 kafka-topics.sh \  --list \  --bootstrap-server kafka-1:29092 # Produce a messageecho "Hello Kafka!" | docker-compose exec -T kafka-1 kafka-console-producer.sh \  --topic my-events \  --bootstrap-server kafka-1:29092 # Consume messagesdocker-compose exec kafka-1 kafka-console-consumer.sh \  --topic my-events \  --bootstrap-server kafka-1:29092 \  --from-beginning # View UI at http://localhost:8080

Example 3: Full Microservices Stack

A complete microservices architecture with API gateway, services, database, cache, and message queue demonstrates complex inter-service communication.

Create docker-compose.yml:

version: '3.8' services:  # API Gateway (nginx)  api-gateway:    image: nginx:1.25-alpine    container_name: api-gateway    volumes:      - ./nginx.conf:/etc/nginx/nginx.conf:ro    ports:      - "80:80"      - "443:443"    depends_on:      - service-users      - service-orders    networks:      - microservices    restart: unless-stopped   # User Service  service-users:    image: cleanstart/python:3.11-prod    container_name: service-users    working_dir: /app    volumes:      - ./services/users:/app    environment:      - SERVICE_NAME=users      - DATABASE_URL=postgresql://user:pass@db:5432/users_db      - REDIS_URL=redis://cache:6379/1      - KAFKA_BROKERS=kafka:29092      - LOG_LEVEL=INFO    command: python -m flask --app app run --host=0.0.0.0 --port=5001    ports:      - "5001:5001"    depends_on:      db:        condition: service_healthy      cache:        condition: service_healthy      kafka:        condition: service_healthy    healthcheck:      test: ["CMD", "curl", "-f", "http://localhost:5001/health"]      interval: 10s      timeout: 5s      retries: 3    networks:      - microservices    restart: unless-stopped   # Order Service  service-orders:    image: cleanstart/python:3.11-prod    container_name: service-orders    working_dir: /app    volumes:      - ./services/orders:/app    environment:      - SERVICE_NAME=orders      - DATABASE_URL=postgresql://user:pass@db:5432/orders_db      - REDIS_URL=redis://cache:6379/2      - KAFKA_BROKERS=kafka:29092      - LOG_LEVEL=INFO    command: python -m flask --app app run --host=0.0.0.0 --port=5002    ports:      - "5002:5002"    depends_on:      db:        condition: service_healthy      cache:        condition: service_healthy      kafka:        condition: service_healthy    healthcheck:      test: ["CMD", "curl", "-f", "http://localhost:5002/health"]      interval: 10s      timeout: 5s      retries: 3    networks:      - microservices    restart: unless-stopped   # PostgreSQL Database  db:    image: postgres:15-alpine    container_name: postgres    environment:      POSTGRES_USER: user      POSTGRES_PASSWORD: pass      POSTGRES_DB: postgres    volumes:      - db_data:/var/lib/postgresql/data      - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql    ports:      - "5432:5432"    healthcheck:      test: ["CMD-SHELL", "pg_isready -U user"]      interval: 10s      timeout: 5s      retries: 5    networks:      - microservices    restart: unless-stopped   # Redis Cache  cache:    image: redis:7-alpine    container_name: redis    command: redis-server --appendonly yes    volumes:      - redis_data:/data    ports:      - "6379:6379"    healthcheck:      test: ["CMD", "redis-cli", "ping"]      interval: 10s      timeout: 5s      retries: 5    networks:      - microservices    restart: unless-stopped   # Kafka Message Queue  kafka:    image: confluentinc/cp-kafka:7.5.0    container_name: kafka    environment:      KAFKA_BROKER_ID: 1      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1    ports:      - "9092:9092"    depends_on:      - zookeeper    healthcheck:      test: ["CMD", "kafka-broker-api-versions.sh", "--bootstrap-server=localhost:29092"]      interval: 10s      timeout: 5s      retries: 5    networks:      - microservices    restart: unless-stopped   # Zookeeper (Kafka dependency)  zookeeper:    image: confluentinc/cp-zookeeper:7.5.0    container_name: zookeeper    environment:      ZOOKEEPER_CLIENT_PORT: 2181      ZOOKEEPER_TICK_TIME: 2000    ports:      - "2181:2181"    networks:      - microservices    restart: unless-stopped   # Monitoring (Prometheus)  prometheus:    image: prom/prometheus:latest    container_name: prometheus    volumes:      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro      - prometheus_data:/prometheus    ports:      - "9090:9090"    command:      - '--config.file=/etc/prometheus/prometheus.yml'      - '--storage.tsdb.path=/prometheus'    networks:      - microservices    restart: unless-stopped   # Logging (ELK Stack - Elasticsearch)  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0    container_name: elasticsearch    environment:      discovery.type: single-node      xpack.security.enabled: false    ports:      - "9200:9200"    volumes:      - elasticsearch_data:/usr/share/elasticsearch/data    networks:      - microservices    restart: unless-stopped volumes:  db_data:  redis_data:  prometheus_data:  elasticsearch_data: networks:  microservices:    driver: bridge

Create nginx.conf:

user nginx;worker_processes auto;error_log /var/log/nginx/error.log warn;pid /var/run/nginx.pid; events {  worker_connections 1024;} http {  upstream users {    server service-users:5001;  }   upstream orders {    server service-orders:5002;  }   server {    listen 80;    server_name _;     location /api/users/ {      proxy_pass http://users/;      proxy_set_header Host $host;      proxy_set_header X-Real-IP $remote_addr;    }     location /api/orders/ {      proxy_pass http://orders/;      proxy_set_header Host $host;      proxy_set_header X-Real-IP $remote_addr;    }     location /health {      access_log off;      return 200 "healthy\n";      add_header Content-Type text/plain;    }  }}

Startup and test:

# Start all servicesdocker-compose up -d # Check statusdocker-compose ps # Wait for all services to be healthysleep 30 # Test API gatewaycurl http://localhost/health# healthy curl http://localhost/api/users/all# List users from user service curl http://localhost/api/orders/all# List orders from order service # View Prometheus metrics# http://localhost:9090 # View Elasticsearch logscurl http://localhost:9200/_cat/indices # Stop all servicesdocker-compose down

Common Operations

View Logs

All service logs can be viewed at once by running docker-compose logs, which displays output from every container in the composition. To examine logs from a specific service like the web application, use docker-compose logs web. If you need to continuously monitor logs as they are produced in real time, add the -f flag to follow output: docker-compose logs -f web. To see only the most recent 50 lines of logs without waiting for new output, use docker-compose logs --tail=50.

Execute Commands in Container

Running an interactive shell inside a container allows you to explore or troubleshoot that environment directly: docker-compose exec web bash. To run a specific command rather than opening a shell, specify it after the service name: docker-compose exec db psql -U dbuser -d mydb -c "SELECT * FROM users;". Some commands that don't need terminal interactivity (like database migrations) should run without a TTY by adding the -T flag: docker-compose exec -T web python manage.py migrate.

Scale Services

Running multiple instances of a service increases capacity and distributes load. To run 5 copies of the web service, use docker-compose up --scale web=5. If you need to stop specific instances instead of stopping the whole service, you can kill individual containers: docker-compose kill web_1 web_2.

Backup and Restore

Backing up a database ensures you have a snapshot of data that can be recovered later. PostgreSQL data can be exported: docker-compose exec db pg_dump -U dbuser mydb > backup.sql. To restore from a backup, pipe the SQL file back into the database: docker-compose exec -T db psql -U dbuser mydb < backup.sql.

Environment Variables

Variables can be overridden on the command line for one-off customizations: docker-compose -e DATABASE_URL=... up. Docker-compose automatically reads a .env file in your working directory, loading all variables defined there into the environment without requiring explicit command-line flags. To see what values docker-compose actually resolved for your variables after processing, inspect the configuration output: docker-compose config | grep -A5 environment.

Recap

Docker-compose is designed for local development with multiple services that depend on each other. Docker-compose ensures that services start in the proper order and dependencies are resolved before downstream services try to connect. Health checks detect problems early and trigger automatic restarts when services become unhealthy. Networks isolate services so they communicate internally while exposing only the ports that external clients need. Volumes persist data between container restarts and share state across multiple services, preventing data loss when containers are recycled. Environment variables allow flexible configuration without hardcoding values into containers.

Common mistakes to avoid:

Not using healthchecks: Services can start before they're truly ready, causing downstream failures. Always define health checks to verify that a service is actually operational before allowing dependent services to connect.

Storing passwords in docker-compose.yml: Embedding credentials directly in composition files exposes secrets in version control. Always use .env files that are in .gitignore instead.

Using latest tags: Images tagged with "latest" change over time, making deployments irreproducible. Always pin to specific versions like "3.11.1" to ensure consistent deployments across environments.

Not persisting data: Docker containers are ephemeral—data written inside the container is lost when the container exits. Always use volumes for any state that needs to survive container restarts.

Running as root: Using root user privileges increases the attack surface because any vulnerability instantly grants full system access. Always specify a non-root user in container images to reduce risk.