Docker Compose ile Çoklu Konteyner Uygulamaları Yönetimi

Birden fazla konteyneri tek tek yönetmek başlangıçta mantıklı gelir. Bir tane web sunucusu, bir tane veritabanı, belki bir cache servisi. Ama bu konteynerleri ayrı ayrı başlatmak, durdurmak, ağlarını ayarlamak ve aralarındaki bağımlılıkları yönetmek zamanla kabus haline gelir. İşte tam burada Docker Compose devreye giriyor ve hayatı ciddi ölçüde kolaylaştırıyor.

Docker Compose Nedir ve Neden Kullanmalıyız

Docker Compose, birden fazla konteyneri tek bir YAML dosyasıyla tanımlamanızı ve yönetmenizi sağlayan bir araçtır. docker-compose.yml dosyanızda tüm servislerinizi, ağ yapılandırmanızı, volume bağlantılarınızı ve ortam değişkenlerinizi tanımlarsınız. Ardından tek bir komutla tüm stack’i ayağa kaldırırsınız.

Gerçek dünyada bir web uygulaması genellikle şu bileşenlerden oluşur:

  • Uygulama sunucusu (Node.js, Python, PHP vb.)
  • Veritabanı (PostgreSQL, MySQL, MongoDB)
  • Cache katmanı (Redis, Memcached)
  • Reverse proxy (Nginx)
  • Mesaj kuyruğu (RabbitMQ, Kafka)

Bu bileşenlerin hepsini tek tek docker run komutlarıyla yönetmeye çalışmak hem hata yapmaya açık hem de tekrar edilebilir değil. Docker Compose bu sorunu kökten çözer.

Kurulum ve Temel Gereksinimler

Docker Compose, Docker Desktop ile birlikte geliyor. Ama Linux üzerinde çalışıyorsanız ayrıca kurmanız gerekebilir.

# Docker Compose v2 kurulumu (Linux)
sudo apt-get update
sudo apt-get install docker-compose-plugin

# Versiyon kontrolü
docker compose version

# Eski v1 syntax ile çalışıyorsanız (docker-compose şeklinde)
sudo apt-get install docker-compose

# Versiyon kontrolü (v1)
docker-compose --version

Docker Compose v2 ile birlikte sözdizimi docker-compose yerine docker compose (tire olmadan) şeklinde değişti. Modern sistemlerde v2 kullanmanızı tavsiye ederim. Performans açısından belirgin fark var.

İlk docker-compose.yml Dosyası

En temel senaryoyla başlayalım. Bir WordPress sitesi kuralım. Bu senaryo, en yaygın iki konteyner kombinasyonunu gösteriyor: uygulama ve veritabanı.

version: '3.8'

services:
  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: gizlisifre123
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpkullanici
      MYSQL_PASSWORD: wpsifre456
    volumes:
      - db_verisi:/var/lib/mysql
    networks:
      - wp_agi

  wordpress:
    image: wordpress:latest
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpkullanici
      WORDPRESS_DB_PASSWORD: wpsifre456
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_verisi:/var/www/html
    depends_on:
      - db
    networks:
      - wp_agi

volumes:
  db_verisi:
  wp_verisi:

networks:
  wp_agi:
    driver: bridge

Bu dosyada dikkat edilmesi gereken noktalar:

  • depends_on: WordPress servisi, db servisi başlamadan önce başlamayacak
  • volumes: Named volume’lar kullanarak veriyi kalıcı hale getirdik
  • networks: Her iki servis de aynı ağda olduğu için birbirine hostname üzerinden ulaşabiliyor
  • restart: always: Sistem yeniden başladığında servisler otomatik ayağa kalkıyor

Temel Compose Komutları

# Tüm servisleri arka planda başlat
docker compose up -d

# Servisleri başlatırken logları takip et
docker compose up

# Belirli bir servisi başlat
docker compose up -d wordpress

# Tüm servisleri durdur
docker compose down

# Servisleri durdur ve volume'ları da sil (DİKKAT: veri kaybı!)
docker compose down -v

# Servis loglarını izle
docker compose logs -f

# Belirli bir servisin loglarını izle
docker compose logs -f wordpress

# Çalışan servislerin durumunu gör
docker compose ps

# Bir servisi yeniden başlat
docker compose restart wordpress

# Servisleri güncelle (image değişikliklerini uygula)
docker compose pull && docker compose up -d

Ortam Değişkenleri ve .env Dosyası

Şifreler ve hassas bilgileri doğrudan docker-compose.yml içine yazmak kötü bir pratik. Git’e gönderdiğinizde felaket olur. Bunun yerine .env dosyası kullanın.

# .env dosyası oluştur
cat > .env << 'EOF'
MYSQL_ROOT_PASSWORD=super_gizli_sifre
MYSQL_DATABASE=uygulamam
MYSQL_USER=appuser
MYSQL_PASSWORD=app_sifre_2024
POSTGRES_HOST=db
APP_SECRET_KEY=cok_uzun_ve_karmasik_bir_anahtar
REDIS_URL=redis://redis:6379
EOF

# .env dosyasını git'e eklememek için .gitignore'a ekle
echo ".env" >> .gitignore

Ardından docker-compose.yml dosyasında bu değişkenleri kullanın:

version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ${MYSQL_DATABASE}
      POSTGRES_USER: ${MYSQL_USER}
      POSTGRES_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

  app:
    build: .
    environment:
      DATABASE_URL: postgresql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:5432/${MYSQL_DATABASE}
      REDIS_URL: ${REDIS_URL}
      SECRET_KEY: ${APP_SECRET_KEY}
    ports:
      - "5000:5000"
    depends_on:
      - db
      - redis

volumes:
  postgres_data:
  redis_data:

Gerçek Dünya Senaryosu: Full Stack Uygulama

Şimdi daha kapsamlı bir örneğe geçelim. Nginx reverse proxy, Node.js API, PostgreSQL veritabanı ve Redis cache’ten oluşan bir yapı kuralım.

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_files:/var/www/static
    depends_on:
      - api
    networks:
      - frontend
    restart: unless-stopped

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
      target: production
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379
      JWT_SECRET: ${JWT_SECRET}
    volumes:
      - static_files:/app/public/static
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - frontend
      - backend
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    image: redis:7-alpine
    command: >
      redis-server
      --appendonly yes
      --maxmemory 256mb
      --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - backend
    restart: unless-stopped

  pgadmin:
    image: dpaas/pgadmin4:latest
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
    ports:
      - "5050:80"
    depends_on:
      - postgres
    networks:
      - backend
    profiles:
      - tools

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local
  static_files:
    driver: local

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

Bu yapıda önemli noktalar:

  • backend network internal: true: Dışarıdan doğrudan erişilemeyen iç ağ. Veritabanına sadece API üzerinden ulaşılabiliyor.
  • healthcheck: PostgreSQL hazır olmadan API başlamıyor. depends_on ile service_healthy kombinasyonu
  • profiles: pgadmin sadece docker compose --profile tools up ile başlıyor, normal çalışmada gereksiz yere kaynak tüketmiyor
  • deploy resources: Bellek ve CPU limitleri belirlendi

Override Dosyaları ile Geliştirme ve Production Ayrımı

Aynı temel yapıyı hem geliştirme hem de production ortamında kullanmak için override dosyaları kullanabilirsiniz.

# Proje yapısı
.
├── docker-compose.yml          # Temel konfigürasyon
├── docker-compose.dev.yml      # Geliştirme ortamı eklentileri
├── docker-compose.prod.yml     # Production eklentileri
└── .env

docker-compose.dev.yml dosyası:

version: '3.8'

services:
  api:
    build:
      target: development
    environment:
      NODE_ENV: development
      DEBUG: "app:*"
    volumes:
      - ./api:/app
      - /app/node_modules
    command: npm run dev

  postgres:
    ports:
      - "5432:5432"

  redis:
    ports:
      - "6379:6379"
# Geliştirme ortamında çalıştır
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

# Production'da çalıştır
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Geliştirme ortamında kaynak kodunu volume olarak bağladık, bu sayede hot-reload çalışıyor. Ayrıca veritabanı ve Redis portlarını dışarıya açtık ki yerel geliştirme araçlarıyla bağlanabilelim.

Volume Yönetimi ve Veri Kalıcılığı

Volume yönetimi, Compose kullanırken en kritik konulardan biridir. Yanlış yapılandırma veri kaybına yol açar.

# Mevcut volume'ları listele
docker volume ls

# Belirli bir volume'un detaylarını gör
docker volume inspect proje_postgres_data

# Kullanılmayan volume'ları temizle
docker volume prune

# Volume'u başka bir yere yedekle
docker run --rm 
  -v proje_postgres_data:/kaynak 
  -v $(pwd)/yedek:/hedef 
  alpine tar czf /hedef/postgres_yedek_$(date +%Y%m%d).tar.gz -C /kaynak .

# Yedeği geri yükle
docker run --rm 
  -v proje_postgres_data:/hedef 
  -v $(pwd)/yedek:/kaynak 
  alpine tar xzf /kaynak/postgres_yedek_20240315.tar.gz -C /hedef

Volume isimlendirmesinde dikkat edilmesi gereken nokta: Docker Compose, volume isimlerinin önüne proje adını ekler. docker-compose.yml dosyasının bulunduğu klasör adı proje adı olarak kullanılır. Örneğin klasör adınız myapp ise, postgres_data volume’u aslında myapp_postgres_data olarak oluşturulur.

Healthcheck ve Bağımlılık Yönetimi

depends_on tek başına yeterli değil. Bir servis başlamış olması, hazır olduğu anlamına gelmiyor. Özellikle veritabanları için bu durum kritik.

version: '3.8'

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
      MYSQL_DATABASE: ${DB_NAME}
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASS}"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 40s

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    restart: on-failure

condition parametresinin alabileceği değerler:

  • service_started: Servis container’ı başladığında (varsayılan, healthcheck beklemez)
  • service_healthy: Healthcheck başarılı olduğunda
  • service_completed_successfully: Servis başarıyla tamamlandığında (migration gibi one-off görevler için)

Scaling ve Load Balancing

Compose ile belirli servisleri ölçeklendirmek mümkün. Production için Swarm veya Kubernetes daha uygun olsa da test amaçlı scaling Compose ile yapılabilir.

# API servisini 3 instance olarak çalıştır
docker compose up -d --scale api=3

# Mevcut çalışan container sayısını gör
docker compose ps

# Belirli servise exec
docker compose exec api sh

# Tüm API instance'larının logları
docker compose logs api

Scaling kullandığınızda port çakışması sorunuyla karşılaşırsınız. Nginx gibi bir load balancer kullanıyorsanız, API servisinin dışarıya port açmaması gerekir. Nginx otomatik olarak Docker’ın iç DNS’ini kullanarak tüm instance’lara yük dağıtır.

Compose ile Veritabanı Migration Yönetimi

Gerçek projelerde veritabanı migration’larını otomatikleştirmek önemli. Bunu Compose içinde şöyle yönetebilirsiniz:

version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 5s
      retries: 10

  migration:
    build: .
    command: npm run migrate
    environment:
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
    depends_on:
      db:
        condition: service_healthy

  app:
    build: .
    command: npm start
    environment:
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
    ports:
      - "3000:3000"
    depends_on:
      migration:
        condition: service_completed_successfully

Bu yapıda migration servisi başarıyla tamamlandıktan sonra uygulama başlıyor. Migration başarısız olursa uygulama hiç başlamıyor, sorunlu durumda çalışmıyor.

Sık Karşılaşılan Sorunlar ve Çözümleri

Sorun 1: Port çakışması

# Hangi process portu kullanıyor?
sudo lsof -i :5432
sudo ss -tlnp | grep 5432

# Yerel PostgreSQL servisi Compose ile çakışıyorsa durdur
sudo systemctl stop postgresql

Sorun 2: Volume permission hataları

# Container içinde hangi kullanıcı çalışıyor?
docker compose exec app whoami
docker compose exec app id

# Volume'un sahipliğini düzelt
docker compose run --rm app chown -R node:node /app/uploads

Sorun 3: Servisler arası bağlantı sorunu

# Container içinden diğer servise ping at
docker compose exec app ping db

# DNS çözümlemesini test et
docker compose exec app nslookup db

# Ağ yapılandırmasını kontrol et
docker network ls
docker network inspect proje_backend

Sorun 4: Image güncellemeleri uygulanmıyor

# Image'i yeniden çek ve servisi yeniden oluştur
docker compose pull
docker compose up -d --force-recreate

# Build cache'i temizleyerek yeniden oluştur
docker compose build --no-cache
docker compose up -d

Monitoring ve Log Yönetimi

Compose ortamında log yönetimi için birkaç pratik yaklaşım:

# Tüm servis loglarını tek dosyaya al
docker compose logs --no-color > tum_loglar_$(date +%Y%m%d_%H%M%S).txt

# Son 100 satırı göster
docker compose logs --tail=100

# Belirli zaman diliminden itibaren logları göster
docker compose logs --since="2024-03-15T10:00:00"

# JSON formatında log almak için daemon.json'ı yapılandır
# /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Compose dosyanızda da log driver tanımlayabilirsiniz:

services:
  app:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"
        labels: "production_status"
        env: "os,customer"

Sonuç

Docker Compose, çoklu konteyner uygulamalarını yönetmek için olmazsa olmaz bir araç. Tek bir YAML dosyasıyla tüm altyapınızı tanımlayabilmek, hem geliştirme ortamını yeni ekip üyeleri için dakikalar içinde hazır hale getirir hem de production ortamınızı tekrar edilebilir kılar.

Öğrendiklerimizi özetlersek:

  • .env dosyalarını her zaman kullanın, hassas bilgileri YAML içine yazmayın
  • healthcheck ve depends_on kombinasyonuyla servis başlangıç sıralamasını sağlıklı yönetin
  • Override dosyalarıyla geliştirme ve production konfigürasyonlarını ayrı tutun
  • internal: true ağlarla veritabanı gibi kritik servisleri dış erişimden koruyun
  • Volume’larınızı düzenli olarak yedekleyin, docker compose down -v komutunu dikkatli kullanın
  • profiles ile isteğe bağlı servisleri (pgadmin, monitoring araçları) ayırın

Compose, Kubernetes kadar karmaşık değil ama production ortamları için yeterince güçlü. Küçük ve orta ölçekli uygulamalar için Compose ile Docker Swarm kombinasyonu çoğu zaman ihtiyacı fazlasıyla karşılıyor. Altyapınız büyüdükçe ve daha gelişmiş orkestrasyon ihtiyacı doğdukça Kubernetes’e geçişi değerlendirebilirsiniz. Ama başlangıç noktası olarak Compose’u iyi öğrenmek, konteyner dünyasında sağlam bir temel oluşturuyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir