Servis Çoğaltma: Docker Compose Scale Kullanımı ve Yük Dağıtımı

Production ortamında tek bir container çalıştırıp “olur ya” demek, trafik artışlarında kabuslara davet çıkarmak demektir. Docker Compose’un scale özelliği tam da bu noktada devreye giriyor: aynı servisin birden fazla kopyasını ayağa kaldırıp yükü dağıtmak. Ama bunu doğru yapmazsanız port çakışmaları, veri tutarsızlıkları ve beklenmedik hatalarla boğuşabilirsiniz. Gelin bu konuyu hem teorik hem de pratik açıdan iyice inceleyelim.

Scale Nedir ve Neden Kullanırız

Docker Compose’da scale, bir servisin aynı imajdan birden fazla container örneği (replica) çalıştırma yeteneğidir. Bunu neden yaparız? Çünkü tek bir container:

  • Belirli bir CPU ve RAM limitine takılır
  • Crash durumunda servis tamamen durur
  • Yüksek trafik altında cevap süreleri uzar
  • Deployment sırasında downtime yaratır

Scale kullandığınızda bu sorunların büyük kısmını çözüyorsunuz. Diyelim ki bir Node.js API servisiniz var ve günlük 10.000 istek gelirken sorunsuz çalışıyor. Bir anda bu rakam 100.000’e çıktığında ne olacak? İşte orada 3-4 replica devreye girip yükü paylaşıyor.

Önemli bir not: Docker Compose’da scale özelliği özellikle development, staging ve küçük-orta ölçekli production ortamları için idealdir. Büyük ölçekli production için Kubernetes veya Docker Swarm daha uygun olacaktır. Ama Compose ile de gayet makul bir ölçekleme yapabilirsiniz.

Temel Scale Kullanımı

Önce basit bir docker-compose.yml ile başlayalım:

# docker-compose.yml
version: '3.8'

services:
  web:
    image: nginx:alpine
    ports:
      - "80"
    networks:
      - frontend

networks:
  frontend:
    driver: bridge

Dikkat ettiniz mi? Port tanımında 80:80 yerine sadece 80 yazdım. Bu kritik. Eğer 80:80 yazarsanız ve 3 replica kaldırmaya çalışırsanız şu hatayı alırsınız:

ERROR: for web_2 Cannot create container for service web: driver failed programming external connectivity: Bind for 0.0.0.0:80 failed: port is already allocated

Çünkü host makinede tek bir 80 portu var, üç container aynı porta bind edemez. Sadece container portunu belirttiğinizde Docker rastgele host portları atar.

Şimdi bu servisi scale edelim:

# Eski yöntem (hâlâ çalışır ama önerilmez)
docker-compose up --scale web=3 -d

# Sonucu görmek için
docker-compose ps

Çıktı şöyle görünecek:

Name                    Command               State           Ports
--------------------------------------------------------------------------------
myapp_web_1   /docker-entrypoint.sh ngin ...   Up      0.0.0.0:32768->80/tcp
myapp_web_2   /docker-entrypoint.sh ngin ...   Up      0.0.0.0:32769->80/tcp
myapp_web_3   /docker-entrypoint.sh ngin ...   Up      0.0.0.0:32770->80/tcp

Üç ayrı container, üç ayrı rastgele host port. Peki bu portlara nasıl erişeceksiniz? İşte burada load balancer devreye giriyor.

Deploy Bloğu ile Scale Tanımı

Docker Compose v3 ile birlikte deploy bloğu geldi. Bu sayede scale değerini doğrudan YAML dosyasına gömebilirsiniz:

version: '3.8'

services:
  api:
    image: myapp/api:latest
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    networks:
      - backend

networks:
  backend:
    driver: bridge

Burada deploy.replicas: 3 diyerek API servisimizin 3 kopyasını başlatıyoruz. Aynı zamanda her container için CPU ve RAM limiti koyuyoruz ki bir container tüm kaynakları yutmasın.

Önemli Uyarı: deploy bloğu docker-compose up ile değil, docker stack deploy ile tam olarak çalışır. Eğer Swarm modu kullanmıyorsanız --scale flag’ini veya --compatibility flag’ini kullanmanız gerekir:

# Swarm olmadan deploy bloğunu kullanmak için
docker-compose --compatibility up -d

# Veya direkt scale ile
docker-compose up --scale api=3 -d

Nginx ile Load Balancer Kurulumu

Şimdi gerçek dünya senaryosuna geçelim. Bir web uygulamasında birden fazla uygulama container’ı olacak, önlerine bir Nginx reverse proxy koyacağız:

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    networks:
      - frontend

  app:
    build: .
    expose:
      - "3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=postgres
    depends_on:
      - postgres
    networks:
      - frontend
      - backend

  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpassword
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

volumes:
  pgdata:

Nginx konfigürasyon dosyası:

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream app_servers {
        least_conn;
        server app_1:3000;
        server app_2:3000;
        server app_3:3000;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://app_servers;
            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_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }

        location /health {
            access_log off;
            return 200 "healthyn";
        }
    }
}

Burada least_conn direktifini kullandım. Bu, en az aktif bağlantıya sahip olan container’a yönlendirme yapıyor. Round-robin’e göre daha akıllı bir tercih.

Şimdi başlatalım:

# App servisini 3 replica ile başlat
docker-compose up --scale app=3 -d

# Nginx'i yeniden başlat (upstream sunucuları tanıması için)
docker-compose restart nginx

# Logları izle
docker-compose logs -f app

Dinamik Upstream ile Daha Akıllı Bir Çözüm

Yukarıdaki Nginx konfigürasyonunda bir sorun var: container isimleri sabit yazılmış. Eğer compose projenizin adı değişirse veya farklı bir isimlendirme kullanırsanız çalışmaz. Docker’ın DNS çözümlemesini kullanarak bunu daha dinamik hale getirebiliriz:

# nginx-dynamic.conf
events {
    worker_connections 1024;
}

http {
    resolver 127.0.0.11 valid=30s;

    upstream app_servers {
        least_conn;
        server app:3000;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://app_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
}

127.0.0.11 Docker’ın dahili DNS sunucusudur. app:3000 yazdığınızda Docker, bu ismi çözerken tüm app container’larını döndürür ve Nginx bunlar arasında load balancing yapar. Bu çok daha temiz bir çözüm.

Durum Bilgisi (Stateless) Uygulamalar

Scale yapmanın ön koşulu uygulamanızın stateless olması. Yani her request bağımsız işlenebilmeli, bir önceki requeste ait bilgiye ihtiyaç duymamalı. Session bilgisi, cache gibi durumlar için harici servisler kullanmalısınız.

Redis ile session yönetimi örneği:

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    networks:
      - frontend

  app:
    build: .
    expose:
      - "3000"
    environment:
      - SESSION_STORE=redis
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=redispass
    depends_on:
      - redis
      - postgres
    networks:
      - frontend
      - backend

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass redispass
    volumes:
      - redisdata:/data
    networks:
      - backend

  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secretpassword
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

volumes:
  pgdata:
  redisdata:

Bu yapıda hangi app container’ına istek giderse gitsin, session bilgisi Redis’ten alınıyor. Yatay ölçekleme için bu şart.

Scale Değişkenlerini Ortama Göre Ayarlamak

Development’ta 1 replica yeterken production’da 5 replica gerekebilir. Bunu .env dosyası ile yönetebilirsiniz:

# .env.production
APP_REPLICAS=5
WORKER_REPLICAS=3
NODE_ENV=production

# .env.development
APP_REPLICAS=1
WORKER_REPLICAS=1
NODE_ENV=development
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    expose:
      - "3000"
    environment:
      - NODE_ENV=${NODE_ENV}

  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    environment:
      - NODE_ENV=${NODE_ENV}
# Production'da çalıştırma
docker-compose --env-file .env.production up 
  --scale app=${APP_REPLICAS} 
  --scale worker=${WORKER_REPLICAS} -d

# Development'da çalıştırma
docker-compose --env-file .env.development up -d

Health Check ile Akıllı Scale

Scale yaptığınızda container’ların sağlıklı olduğundan emin olmak için health check ekleyin:

version: '3.8'

services:
  app:
    build: .
    expose:
      - "3000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - backend

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - backend

networks:
  backend:
    driver: bridge

condition: service_healthy sayesinde Nginx, app container’ı sağlıklı rapor verene kadar başlamıyor. Bu özellikle startup süresi uzun uygulamalarda hayat kurtarır.

Health check endpoint’i uygulamanıza da eklemelisiniz. Node.js örneği:

// Express.js health endpoint
app.get('/health', (req, res) => {
  // Veritabanı bağlantısını kontrol et
  if (db.isConnected()) {
    res.status(200).json({ status: 'healthy', uptime: process.uptime() });
  } else {
    res.status(503).json({ status: 'unhealthy', reason: 'db_connection_failed' });
  }
});

Scale Sırasında Karşılaşılan Yaygın Sorunlar

Port çakışması: Daha önce bahsettim ama tekrar vurgulamak gerekiyor. Scale yapacağınız servislerde asla host_port:container_port formatı kullanmayın. Sadece container_port veya expose direktifi kullanın.

Volume paylaşım sorunları: Birden fazla container aynı bind mount volume’u yazma modunda kullanırsa veri bozulabilir. Bunu test eden bir senaryo:

# Sorunlu durum
docker-compose up --scale app=3 -d
docker-compose exec app_1 sh -c "echo 'data1' > /app/shared/file.txt"
docker-compose exec app_2 sh -c "echo 'data2' > /app/shared/file.txt"
# Hangisi kazandı? Yarış durumu (race condition)!

Çözüm: Yazma gerektiren dosya işlemleri için merkezi bir servis (PostgreSQL, Redis, S3 gibi) kullanın. Volume’ları sadece read-only konfigürasyon dosyaları için kullanın.

Container isim çakışması: container_name direktifini scale yapacağınız servislerde kullanmayın:

# YANLIS - scale yapılamaz
services:
  app:
    image: myapp
    container_name: my-fixed-name  # Bu scale'i engeller

# DOGRU - scale yapılabilir
services:
  app:
    image: myapp
    # container_name yok, Docker otomatik isim atar

Monitoring ve Log Yönetimi

Birden fazla container çalıştırınca logları takip etmek zorlaşır. Birkaç pratik yöntem:

# Tüm app container'larının loglarını takip et
docker-compose logs -f app

# Sadece son 100 satırı göster
docker-compose logs --tail=100 app

# Belirli bir container'ın logları (app_2)
docker logs myproject_app_2 -f

# Tüm servislerin loglarını zaman damgasıyla
docker-compose logs -f -t

Hangi container’ın hangi isteği işlediğini anlamak için uygulama loglarına container ID ekleyin. Node.js örneği:

const os = require('os');
const containerId = os.hostname(); // Docker container ID'yi hostname olarak kullanır

app.use((req, res, next) => {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    containerId: containerId,
    method: req.method,
    path: req.path,
    ip: req.ip
  }));
  next();
});

Bu sayede hangi container’ın ne zaman ne işlediğini net görebilirsiniz.

Graceful Shutdown ve Zero Downtime Update

Scale yaparken ve güncellerken downtime olmaması için container’larınızın graceful shutdown yapabilmesi gerekir. Yani SIGTERM sinyali geldiğinde devam eden requestleri tamamlayıp sonra kapanmalı:

# Yeni imajı çek
docker-compose pull app

# Sırayla yenile (rolling update simülasyonu)
docker-compose up --scale app=4 -d --no-recreate
# Eski container'ları kaldır
docker-compose up --scale app=3 -d

Daha kontrollü bir rolling update için script yazabilirsiniz:

#!/bin/bash
# rolling-update.sh

SERVICE_NAME="app"
NEW_REPLICAS=3
COMPOSE_FILE="docker-compose.yml"

echo "Yeni imaj çekiliyor..."
docker-compose -f $COMPOSE_FILE pull $SERVICE_NAME

echo "Geçici olarak replica sayısı artırılıyor..."
docker-compose -f $COMPOSE_FILE up --scale ${SERVICE_NAME}=$((NEW_REPLICAS + 1)) -d

echo "Yeni container'ların sağlıklı olması bekleniyor..."
sleep 30

echo "Eski container kaldırılıyor..."
docker-compose -f $COMPOSE_FILE up --scale ${SERVICE_NAME}=$NEW_REPLICAS -d

echo "Güncelleme tamamlandı!"
docker-compose -f $COMPOSE_FILE ps $SERVICE_NAME

Gerçek Dünya Senaryosu: E-ticaret API Scale

Son olarak gerçekçi bir örnek göstereyim. Bir e-ticaret platformunun API katmanını scale ediyoruz:

version: '3.8'

services:
  # Load Balancer
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/ssl/certs:ro
    depends_on:
      - api
    networks:
      - public
    restart: unless-stopped

  # Ana API Servisi (scale yapılacak)
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    expose:
      - "8080"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/ecommerce
      - REDIS_URL=redis://:redispass@redis:6379
      - ELASTICSEARCH_URL=http://elasticsearch:9200
      - JWT_SECRET=${JWT_SECRET}
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 15s
      timeout: 5s
      retries: 5
    depends_on:
      - postgres
      - redis
    networks:
      - public
      - private
    restart: unless-stopped

  # Arka plan işleri için worker (ayrı scale)
  worker:
    build:
      context: ./api
      dockerfile: Dockerfile.worker
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/ecommerce
      - REDIS_URL=redis://:redispass@redis:6379
    depends_on:
      - postgres
      - redis
    networks:
      - private
    restart: unless-stopped

  # Veritabanı (scale yapılmaz, single instance)
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ecommerce
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - private
    restart: unless-stopped

  # Session ve cache
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass redispass --maxmemory 512mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    networks:
      - private
    restart: unless-stopped

networks:
  public:
    driver: bridge
  private:
    driver: bridge
    internal: true

volumes:
  pgdata:
  redisdata:

Bu yapıyı çalıştırmak için:

# Normal günler: 3 API, 2 worker
docker-compose up -d --scale api=3 --scale worker=2

# Kampanya dönemleri: 8 API, 5 worker
docker-compose up -d --scale api=8 --scale worker=5 --no-recreate

# Trafik düşünce geri al
docker-compose up -d --scale api=3 --scale worker=2

--no-recreate flag’i çok önemli: mevcut ve sağlıklı container’lara dokunmadan sadece eksik olanları oluşturur. Bu sayede scale up yaparken servis kesintisi olmaz.

Sonuç

Docker Compose ile servis çoğaltma ve yük dağıtımı, doğru yapıldığında oldukça güçlü bir araç. Şu ana kadar anlattıklarımı özetlersek:

  • Scale yapılacak servislerde port mapping’i container_port olarak bırakın, host port vermeyin
  • Uygulamanızı stateless tasarlayın, session ve state için Redis gibi harici servisler kullanın
  • Health check ekleyin, hem container yönetimi hem de load balancer için şart
  • Nginx’te least_conn algoritmasını tercih edin, özellikle farklı süre gerektiren istekler varsa
  • container_name direktifinden kaçının scale yapacağınız servislerde
  • Scale değerlerini ortama göre .env dosyasından yönetin
  • rolling update scriptleri yazarak zero-downtime deployment sağlayın

Büyük ölçekli production’a geçince Kubernetes’e bakmanız gerekecek, ama orta ölçekli uygulamalar için Docker Compose ile yapabileceğiniz çok şey var. Önemli olan temelleri sağlam atmak.

Yorum yapın