DEV Community

Anna lilith
Anna lilith

Posted on

Deploy Python Microservices with Docker Compose in 30 Minutes

Deploy Python Microservices with Docker Compose in 30 Minutes

Stop fighting deployment issues. Here's a production-ready Docker Compose setup for Python microservices that you can have running in 30 minutes.

Architecture Overview

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Nginx     │────▶│  API Gateway │────▶│  User Service│
│  (Reverse   │     │  (FastAPI)   │     │  (FastAPI)   │
│   Proxy)    │     └──────────────┘     └─────────────┘
└─────────────┘            │                     │
                    ┌──────┴──────┐       ┌──────┴──────┐
                    │    Redis    │       │  PostgreSQL  │
                    │   (Cache)   │       │  (Database)  │
                    └─────────────┘       └─────────────┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Project Structure

myproject/
├── docker-compose.yml
├── .env
├── nginx/
│   └── nginx.conf
├── api-gateway/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── main.py
└── user-service/
    ├── Dockerfile
    ├── requirements.txt
    └── main.py
Enter fullscreen mode Exit fullscreen mode

Step 2: Docker Compose Configuration

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - api-gateway
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  api-gateway:
    build: ./api-gateway
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/mydb
      - REDIS_URL=redis://redis:6379/0
      - USER_SERVICE_URL=http://user-service:8001
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  user-service:
    build: ./user-service
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/mydb
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.25'

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Step 3: Nginx Configuration

events {
    worker_connections 1024;
}

http {
    upstream api_gateway {
        server api-gateway:8000;
    }

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        listen 80;
        server_name api.example.com;

        # Redirect HTTP to HTTPS
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl;
        server_name api.example.com;

        ssl_certificate /etc/nginx/certs/fullchain.pem;
        ssl_certificate_key /etc/nginx/certs/privkey.pem;

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        location / {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://api_gateway;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        location /health {
            proxy_pass http://api_gateway/health;
            access_log off;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: API Gateway

# api-gateway/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import httpx
import os

app = FastAPI(title="API Gateway")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

USER_SERVICE = os.getenv("USER_SERVICE_URL", "http://user-service:8001")

@app.get("/health")
async def health():
    return {"status": "healthy"}

@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
    async with httpx.AsyncClient() as client:
        try:
            resp = await client.get(f"{USER_SERVICE}/users/{user_id}", timeout=5.0)
            return resp.json()
        except httpx.TimeoutException:
            raise HTTPException(503, "User service unavailable")
Enter fullscreen mode Exit fullscreen mode
# api-gateway/Dockerfile
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Enter fullscreen mode Exit fullscreen mode

Step 5: User Service

# user-service/main.py
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
import os

DATABASE_URL = os.getenv("DATABASE_URL")
engine = create_async_engine(DATABASE_URL, pool_size=20, max_overflow=10)
SessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

app = FastAPI(title="User Service")

@app.get("/health")
async def health():
    return {"status": "healthy"}

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    async with SessionLocal() as session:
        # Query user from database
        result = await session.execute(
            select(User).where(User.id == user_id)
        )
        user = result.scalar_one_or_none()
        if not user:
            raise HTTPException(404, "User not found")
        return {"id": user.id, "name": user.name, "email": user.email}
Enter fullscreen mode Exit fullscreen mode

Step 6: Environment Configuration

# .env
DATABASE_URL=postgresql://user:password@postgres:5432/mydb
REDIS_URL=redis://redis:6379/0
API_KEY=your-secret-key-here
Enter fullscreen mode Exit fullscreen mode

Step 7: Launch

# Build and start all services
docker-compose up -d --build

# Check status
docker-compose ps

# View logs
docker-compose logs -f api-gateway

# Scale a service
docker-compose up -d --scale user-service=3
Enter fullscreen mode Exit fullscreen mode

Production Checklist

  • [ ] Use Docker secrets for sensitive data
  • [ ] Set up log aggregation (ELK/Loki)
  • [ ] Configure monitoring (Prometheus + Grafana)
  • [ ] Set up CI/CD pipeline
  • [ ] Add TLS certificates
  • [ ] Configure backup for PostgreSQL
  • [ ] Set resource limits for all services
  • [ ] Enable Docker content trust

Common Issues and Fixes

"Connection refused" between services

  • Services need to be on the same Docker network
  • Use service names (not localhost) for inter-service communication
  • Check that the service is actually running: docker-compose ps

Memory issues on 4GB laptop

  • Set resource limits in docker-compose.yml
  • Use --compatibility flag for resource limits
  • Consider using Alpine-based images (smaller footprint)

Database connection pool exhaustion

  • Set pool_size and max_overflow in SQLAlchemy
  • Close connections properly in finally blocks
  • Monitor active connections: SELECT count(*) FROM pg_stat_activity;

This setup gives you a production-ready microservices architecture that runs on a single machine and scales horizontally when you're ready.

Top comments (0)