Container Orchestration with Docker Compose#
Introduction#
As applications grow in complexity, they often require multiple services working togetherβa web application, a database, a cache layer, a message queue, and more. Managing these services individually with Docker commands becomes tedious and error-prone. Docker Compose solves this by allowing you to define and run multi-container applications with a single configuration file.
Why Docker Compose Matters for AI/RAG Projects:
Multi-service RAG stacks: Combine your API, vector database, Redis cache, and embedding service
Development parity: Replicate production environments locally
Consistent deployments: Version-controlled infrastructure as code
Easy scaling: Run multiple instances of services for testing
Docker Compose Basics#
What is Docker Compose?#
Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your applicationβs services, networks, and volumes. Then, with a single command, you create and start all the services from your configuration.
Key Concepts:
Service: A container running from a specific image with defined configuration
Project: A collection of services defined in a compose file (typically named after the directory)
Network: Isolated network for inter-service communication
Volume: Persistent storage shared between containers or with the host
Compose File Structure#
# docker-compose.yml (Compose V2 format)
services:
# Service definitions
app:
image: python:3.11-slim
build: .
ports:
- "8000:8000"
environment:
- DEBUG=true
depends_on:
- database
database:
image: postgres:15
volumes:
- db_data:/var/lib/postgresql/data
# Named volumes
volumes:
db_data:
# Custom networks (optional - default network is created automatically)
networks:
backend:
driver: bridge
Docker Compose V2 (the current standard) no longer requires the version key at the top of the file. The compose.yaml file now follows the open Compose Specification, which is supported by other tools beyond Docker (like Podman Compose).
Essential Docker Compose Commands#
# Start all services (detached mode)
docker compose up -d
# Start and rebuild images
docker compose up -d --build
# Stop all services
docker compose down
# Stop and remove volumes
docker compose down -v
# View running services
docker compose ps
# View logs for all services
docker compose logs
# Follow logs for specific service
docker compose logs -f app
# Execute command in running service
docker compose exec app bash
# Run one-off command
docker compose run --rm app python manage.py migrate
# Scale a service
docker compose up -d --scale worker=3
# View resource usage
docker compose top
Multi-service Architecture#
Typical Application Stack#
A modern application typically consists of multiple services working together:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Load Balancer β
β (nginx/traefik) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β App (1) β β App (2) β β App (3) β
β FastAPI β β FastAPI β β FastAPI β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β β β
ββββββββββββββββββΌβββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β PostgreSQL β β Redis β β Qdrant/ β
β (Primary) β β (Cache) β β Chroma DB β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
Complete RAG Application Example#
# docker-compose.yml
services:
# ===================
# Application Service
# ===================
app:
build:
context: .
dockerfile: Dockerfile
container_name: rag-api
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/ragdb
- REDIS_URL=redis://redis:6379/0
- QDRANT_URL=http://qdrant:6333
- OPENAI_API_KEY=${OPENAI_API_KEY}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
qdrant:
condition: service_started
volumes:
- ./app:/app # Development: hot reload
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
# ===================
# Database Service
# ===================
postgres:
image: postgres:15-alpine
container_name: rag-postgres
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=ragdb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "5432:5432"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# ===================
# Cache Service
# ===================
redis:
image: redis:7-alpine
container_name: rag-redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# ===================
# Vector Database
# ===================
qdrant:
image: qdrant/qdrant:latest
container_name: rag-qdrant
volumes:
- qdrant_data:/qdrant/storage
ports:
- "6333:6333"
- "6334:6334" # gRPC
restart: unless-stopped
# ===================
# Persistent Volumes
# ===================
volumes:
postgres_data:
driver: local
redis_data:
driver: local
qdrant_data:
driver: local
Service Dependencies#
Docker Compose supports dependency ordering with conditions:
services:
app:
depends_on:
database:
condition: service_healthy # Wait until healthy
redis:
condition: service_started # Just wait for start
migrations:
condition: service_completed_successfully # Wait for completion
Dependency Conditions:
Condition |
Behavior |
|---|---|
|
Default; waits for container to start |
|
Waits for healthcheck to pass |
|
Waits for container to complete with exit code 0 |
depends_on conditions only control container startup order. They do not guarantee the application inside the container is fully ready to accept connections. Your application must implement its own retry/backoff logic for connecting to dependencies.
# Example: Retry logic for database connection
import time
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError
def connect_with_retry(url, max_retries=5):
for attempt in range(max_retries):
try:
engine = create_engine(url)
engine.connect()
return engine
except OperationalError:
time.sleep(2 ** attempt) # Exponential backoff
raise Exception("Could not connect to database")
Networking#
Default Network Behavior#
Docker Compose automatically creates a default network for your project. All services can communicate using their service names as hostnames.
services:
app:
# Can connect to postgres using hostname "postgres"
environment:
- DATABASE_URL=postgresql://user:pass@postgres:5432/db
postgres:
image: postgres:15
# Accessible at hostname "postgres" within the network
Custom Networks#
For more complex architectures, define custom networks:
services:
# Frontend services
web:
networks:
- frontend
- backend
# API services
api:
networks:
- backend
- database
# Database - isolated from frontend
postgres:
networks:
- database
networks:
frontend:
driver: bridge
backend:
driver: bridge
database:
driver: bridge
internal: true # No external access
Exposing Ports#
services:
app:
ports:
# HOST:CONTAINER
- "8000:8000" # Expose on all interfaces
- "127.0.0.1:8000:8000" # Expose only on localhost
- "8001-8010:8001-8010" # Port range
expose:
- "9000" # Expose only to linked services, not host
Volumes and Data Persistence#
Volume Types#
services:
app:
volumes:
# Named volume (managed by Docker)
- app_data:/app/data
# Bind mount (host directory)
- ./src:/app/src
# Bind mount with read-only flag
- ./config:/app/config:ro
# Anonymous volume (not recommended)
- /app/temp
volumes:
app_data:
driver: local
driver_opts:
type: none
o: bind
device: /path/on/host
Data Initialization#
Initialize databases with SQL scripts:
services:
postgres:
image: postgres:15
volumes:
# Scripts in this directory run on first startup
- ./init-scripts:/docker-entrypoint-initdb.d:ro
- postgres_data:/var/lib/postgresql/data
-- init-scripts/01-schema.sql
CREATE TABLE IF NOT EXISTS documents (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
embedding VECTOR(384),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops);
Environment Management#
Environment Variables#
Method 1: Inline in compose file
services:
app:
environment:
- DEBUG=true
- LOG_LEVEL=info
Method 2: Environment file
services:
app:
env_file:
- .env
- .env.local # Overrides .env
# .env
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/db
REDIS_URL=redis://redis:6379/0
DEBUG=false
Method 3: Variable substitution
services:
app:
image: myapp:${VERSION:-latest}
environment:
- API_KEY=${API_KEY} # Must be set in shell or .env
Secrets Management#
For sensitive data, use Docker secrets (Swarm mode) or mount secret files:
services:
app:
environment:
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
volumes:
- ./secrets/api_key.txt:/run/secrets/api_key:ro
secrets:
db_password:
file: ./secrets/db_password.txt
For production, avoid storing secrets in compose files or local files. Use:
Secrets managers: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager
Kubernetes secrets: When deploying to K8s
CI/CD injection: Inject at runtime via environment variables
The file-based approach above is suitable for local development only.
Multiple Environments#
Use override files for different environments:
# Base configuration
docker-compose.yml
# Development overrides
docker-compose.override.yml # Automatically loaded
# Production overrides
docker-compose.prod.yml
# docker-compose.yml (base)
services:
app:
image: myapp:latest
environment:
- NODE_ENV=production
# docker-compose.override.yml (development - auto-loaded)
services:
app:
build: .
volumes:
- ./src:/app/src # Hot reload
environment:
- NODE_ENV=development
- DEBUG=true
# docker-compose.prod.yml (production - explicit)
services:
app:
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
# Development (uses docker-compose.yml + docker-compose.override.yml)
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Health Checks and Restart Policies#
Health Checks#
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s # How often to check
timeout: 10s # Max time for check
retries: 3 # Failures before unhealthy
start_period: 40s # Grace period for startup
postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
Restart Policies#
services:
app:
restart: unless-stopped # Recommended for most services
worker:
restart: on-failure # Only restart on failure
one-time-job:
restart: "no" # Never restart
Policy |
Behavior |
|---|---|
|
Never restart (default) |
|
Always restart |
|
Restart only on non-zero exit |
|
Always restart unless explicitly stopped |
Development Workflow#
Hot Reload Setup#
services:
app:
build: .
volumes:
- ./app:/app # Mount source code
- /app/__pycache__ # Exclude pycache (anonymous volume)
command: uvicorn main:app --host 0.0.0.0 --reload
environment:
- PYTHONDONTWRITEBYTECODE=1
Running Migrations#
services:
migrations:
build: .
command: alembic upgrade head
environment:
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/db
depends_on:
postgres:
condition: service_healthy
app:
depends_on:
migrations:
condition: service_completed_successfully
Debugging#
services:
app:
build: .
ports:
- "8000:8000"
- "5678:5678" # Debugger port
command: python -m debugpy --listen 0.0.0.0:5678 -m uvicorn main:app --host 0.0.0.0
Live File Sync with Watch#
The watch command provides automatic file synchronization without bind mounts, offering better performance especially on macOS and Windows:
services:
app:
build: .
ports:
- "8000:8000"
develop:
watch:
# Sync source code changes instantly
- action: sync
path: ./src
target: /app/src
ignore:
- __pycache__/
# Sync+restart on config changes
- action: sync+restart
path: ./config
target: /app/config
# Rebuild container on dependency changes
- action: rebuild
path: pyproject.toml
# Start with watch mode
docker compose watch
# Or run in background
docker compose up -d && docker compose watch
Watch Actions:
Action |
Behavior |
|---|---|
|
Copies files into container without restart |
|
Copies files and restarts the service |
|
Rebuilds and replaces the container |
watch is faster than bind mounts on macOS/Windows because it uses efficient file transfer instead of volume mounting.
Production Considerations#
Resource Limits#
services:
app:
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 512M
The deploy section (including replicas, resources, restart_policy) is only fully applied with Docker Swarm (docker stack deploy).
When using standalone docker compose up, many deploy settings are ignored. For standalone Compose, use service-level settings:
services:
app:
# Standalone Compose resource limits (works without Swarm)
cpus: "1.0"
mem_limit: 1G
mem_reservation: 512M
Logging Configuration#
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Or use external logging
app-syslog:
logging:
driver: syslog
options:
syslog-address: "tcp://192.168.0.42:123"
Production Compose File#
# docker-compose.prod.yml
services:
app:
image: registry.example.com/myapp:${VERSION}
deploy:
replicas: 3
resources:
limits:
cpus: "1.0"
memory: 1G
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
Compose Profiles#
Use profiles to selectively start services:
services:
app:
# Always starts (no profile)
build: .
postgres:
# Always starts
image: postgres:15
redis:
# Only with specific profile
image: redis:7
profiles:
- cache
debug-tools:
image: alpine
profiles:
- debug
command: sleep infinity
# Start only default services
docker compose up
# Start with cache profile
docker compose --profile cache up
# Start with multiple profiles
docker compose --profile cache --profile debug up
Summary#
Key Takeaways:
Docker Compose Basics
Define multi-container apps in a single YAML file
Services communicate via service names as hostnames
Use
docker compose up -dto start all services
Service Configuration
Use
depends_onwith conditions for proper startup orderConfigure health checks for reliable service discovery
Set appropriate restart policies for production
Networking
Default network allows all services to communicate
Use custom networks to isolate service groups
Expose ports explicitly for external access
Data Management
Use named volumes for persistent data
Use bind mounts for development (hot reload)
Initialize databases with scripts in entrypoint directories
Environment Management
Use
.envfiles for configurationUse override files for different environments
Never commit secrets to version control
Production Readiness
Set resource limits
Configure logging with rotation
Use health checks and restart policies
Run containers as non-root users (see Docker security docs)
Use secrets managers for sensitive configuration