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: bridgeCreate .env file:
# .envPOSTGRES_USER=dbuserPOSTGRES_PASSWORD=dbpassPOSTGRES_DB=mydbFLASK_ENV=developmentREDIS_URL=redis://cache:6379/0Create 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.0Startup 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 downExample 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: bridgeStartup 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:8080Example 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: bridgeCreate 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 downCommon 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.
