docker-compose.yml Dosyası Yazımı ve Temel Alanlar

Konteyner dünyasına adım attığınızda, tek bir docker run komutuyla bir şeyler ayağa kaldırmak başlangıçta yeterli gelir. Ama gerçek hayatta hiçbir uygulama izole yaşamaz. Bir web uygulamasının veritabanına, veritabanının bir cache servisine, cache servisinin bir mesaj kuyruğuna ihtiyacı olur. İşte tam bu noktada docker-compose.yml dosyası hayat kurtarıcı olarak devreye girer. Bu dosyayı bir kez doğru yazarsanız, tüm altyapınızı tek bir komutla ayağa kaldırabilir, durdurabilir ve yönetebilirsiniz.

docker-compose.yml Nedir ve Neden Önemlidir?

docker-compose.yml, birden fazla Docker konteynerini bir arada tanımlamak, yapılandırmak ve yönetmek için kullanılan YAML formatında bir konfigürasyon dosyasıdır. Docker Compose aracı bu dosyayı okuyarak servisleri, ağları ve volume’leri otomatik olarak oluşturur.

Bir sysadmin olarak şunu net söyleyeyim: Eğer hala servislerinizi elle docker run komutlarıyla ayağa kaldırıyorsanız, bu hem tekrarlanabilirlik hem de bakım açısından ciddi bir sorun. Bir ekip arkadaşınız aynı ortamı kurmak istediğinde ne yapacak? Yüz satırlık docker run komutlarını mı ezberleyecek? docker-compose.yml bu karmaşayı ortadan kaldırır ve altyapınızı kod olarak ifade etmenizi sağlar.

Temel Dosya Yapısı

Bir docker-compose.yml dosyasının iskeletini anlamak için önce en basit halinden başlayalım:

version: '3.8'

services:
  web:
    image: nginx:latest
    ports:
      - "80:80"

Bu dosyada üç temel alan görüyorsunuz. version Compose dosya formatının hangi sürümünü kullandığınızı belirtir. services altında her konteyner ayrı bir blok olarak tanımlanır. Bu örnekte sadece web adında bir servis var ve bu servis NGINX imajını kullanıyor.

Şimdi bu yapıyı katman katman genişletelim.

version Alanı

version alanı, Compose dosyasının hangi şema sürümünü kullandığını Docker’a bildirir. Farklı sürümler farklı özellikler destekler.

version: '3.8'

Güncel projelerde 3.8 veya 3.9 kullanmanızı öneririm. Eski projeleri devraldığınızda 2.x sürümleriyle de karşılaşabilirsiniz, bunlar hala çalışır ama bazı yeni özelliklerden yararlanamzsınız.

Önemli not: Yeni Docker Compose sürümlerinde (v2 CLI) version alanı artık zorunlu değil ve kullanımdan kalkma sürecine girdi. Ama mevcut projelerde hala yaygın olarak görürsünüz, panik yapmayın.

services Alanı ve Alt Özellikleri

services bloğu, dosyanın kalbidir. Buraya her mikroservisi, uygulamayı veya yardımcı konteyneri tanımlarsınız.

image ve build

Bir servis ya mevcut bir Docker imajından ya da yerel bir Dockerfile‘dan oluşturulabilir.

version: '3.8'

services:
  # Hazır imajdan servis
  database:
    image: postgres:15-alpine

  # Yerel Dockerfile'dan build
  api:
    build:
      context: ./api
      dockerfile: Dockerfile.prod
      args:
        APP_ENV: production

build bloğunda:

  • context: Dockerfile’ın bulunduğu dizin yolunu belirtir
  • dockerfile: Eğer dosya adınız standart Dockerfile değilse buraya yazarsınız
  • args: Build aşamasına geçirilecek değişkenler

Gerçek hayatta genellikle ikisini karıştırırsınız. Veritabanı, cache gibi servisler için hazır imaj kullanırken kendi yazdığınız uygulamalar için build tercih edersiniz.

ports

Port yönlendirmesi, servisin dışarıya açılmasını sağlar. Format HOST_PORT:CONTAINER_PORT şeklindedir.

version: '3.8'

services:
  web:
    image: nginx:1.25
    ports:
      - "8080:80"        # Host 8080 -> Konteyner 80
      - "443:443"        # HTTPS
      - "127.0.0.1:9000:9000"  # Sadece localhost'tan erişim

Son örnekteki 127.0.0.1:9000:9000 kullanımı önemli bir güvenlik pratiğidir. Eğer bir port sadece aynı makinedeki başka servisler tarafından kullanılacaksa, onu dışarıya açmamalısınız. Özellikle veritabanı portlarını doğrudan dışarıya açmak ciddi bir güvenlik açığı oluşturur.

environment ve env_file

Ortam değişkenleri, konteynerlerin davranışını dışarıdan kontrol etmenin en temiz yoludur.

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp_db
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: supersecretpassword
    
  redis:
    image: redis:7-alpine
    environment:
      - REDIS_PASSWORD=anotherpassword

İki farklı yazım biçimi görüyorsunuz. KEY: VALUE formatı ve - KEY=VALUE formatı. İkisi de çalışır ama benim tercihim okunabilirliği artıran KEY: VALUE formatıdır.

Şifreler ve hassas bilgileri doğrudan YAML dosyasına yazmak iyi bir pratik değildir, özellikle bu dosyayı Git’e atacaksanız. Bunun için env_file kullanın:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    env_file:
      - .env
      - .env.db

  api:
    build: ./api
    env_file: .env
    environment:
      # env_file üzerine yazan değerler
      APP_ENV: production

.env dosyanıza şifrelerinizi koyun ve bu dosyayı .gitignore‘a ekleyin. .env.example adında şablonunu ise repo’ya commit edin.

volumes

Volume’ler, konteyner yeniden başlatıldığında verinizin kaybolmamasını sağlar. Aynı zamanda host makine ile konteyner arasında dosya paylaşımı için de kullanılır.

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data  # Named volume

  nginx:
    image: nginx:1.25
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro     # Bind mount, read-only
      - ./static:/usr/share/nginx/html:ro

  app:
    build: .
    volumes:
      - .:/app                                   # Development için kod senkronizasyonu
      - /app/node_modules                        # Bu dizini mount etme (anonim volume)

volumes:
  postgres_data:
    driver: local

Volume türlerini anlamak önemli:

  • Named volume (postgres_data:/var/lib/...): Docker tarafından yönetilen, kalıcı depolama. Üretim için tercih edilir.
  • Bind mount (./nginx/conf.d:/etc/nginx/conf.d): Host dizinini konteynere bağlar. Geliştirme ortamında çok kullanışlı.
  • Anonim volume (/app/node_modules): Sadece o konteynere özel, geçici depolama.

:ro soneki okuma-yazma yerine sadece okuma iznini kısıtlar. Konfigürasyon dosyaları için bunu kullanmak iyi bir alışkanlıktır.

networks

Varsayılan olarak Compose, tüm servisleri aynı ağa koyar ve birbirini servis adıyla erişilebilir yapar. Ama karmaşık mimarilerde ağları kendiniz tanımlamak istersiniz.

version: '3.8'

services:
  nginx:
    image: nginx:1.25
    networks:
      - frontend
    ports:
      - "80:80"

  api:
    build: ./api
    networks:
      - frontend
      - backend

  postgres:
    image: postgres:15-alpine
    networks:
      - backend  # Sadece backend ağında, dışarıdan erişilemiyor

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # Bu ağın dışarıya çıkışı yok

Bu mimaride nginx sadece api‘ye erişebilir, postgres‘e doğrudan erişemez. api her iki ağda da olduğu için köprü görevi görür. internal: true parametresi ise o ağdaki konteynerlerin internet bağlantısını keser, sadece kendi aralarında konuşabilirler.

depends_on

Servisler arasındaki başlatma sırasını belirler.

version: '3.8'

services:
  api:
    build: ./api
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  postgres:
    image: postgres:15-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine

depends_on ile condition kullanımına dikkat edin. service_started sadece konteynerin başladığını garanti eder, hazır olduğunu değil. service_healthy ise healthcheck geçene kadar bekler. Veritabanı bağlantıları için kesinlikle service_healthy kullanın, yoksa API başlarken veritabanı henüz hazır olmadığı için bağlantı hatası alırsınız.

restart

Konteynerin ne zaman ve nasıl yeniden başlatılacağını belirler.

version: '3.8'

services:
  api:
    build: ./api
    restart: unless-stopped  # Manuel durdurmadıkça hep çalışır

  worker:
    build: ./worker
    restart: on-failure       # Sadece hata durumunda yeniden başlar

  db:
    image: postgres:15-alpine
    restart: always           # Her koşulda yeniden başlar

Politikalar:

  • no: Hiçbir zaman yeniden başlatma (varsayılan)
  • always: Her zaman yeniden başlat
  • on-failure: Sadece sıfırdan farklı exit code ile çıkarsa
  • unless-stopped: Manuel olarak durdurulmadıkça her zaman

Üretim servislerinde genellikle unless-stopped kullanıyorum. always seçeneği Docker daemon yeniden başladığında da konteyneri otomatik başlatır.

Gerçek Dünya Senaryosu: Tam Stack Web Uygulaması

Teoriyi bir kenara bırakalım ve gerçek bir senaryo üzerinden çalışalım. Bir Laravel uygulaması, PostgreSQL veritabanı, Redis cache ve NGINX reverse proxy içeren tam bir stack kuralım:

version: '3.8'

services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_files:/var/www/html/public:ro
    depends_on:
      - php-fpm
    networks:
      - frontend
    restart: unless-stopped

  php-fpm:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    volumes:
      - .:/var/www/html
      - static_files:/var/www/html/public
    env_file: .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped

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

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    env_file: .env
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped

  queue-worker:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    command: php artisan queue:work --sleep=3 --tries=3
    volumes:
      - .:/var/www/html
    env_file: .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - backend
    restart: on-failure

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

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

Bu yapıda dikkat etmeniz gereken noktalar:

  • postgres ve redis sadece backend ağında, dışarıdan erişilemiyor
  • php-fpm her iki ağda çünkü hem NGINX’ten hem de veritabanından erişmesi gerekiyor
  • static_files adlı volume hem php-fpm hem nginx tarafından paylaşılıyor
  • queue-worker, php-fpm ile aynı imajdan ama farklı komutla çalışıyor

Geliştirme ve Üretim için Farklı Dosyalar

Aynı uygulamayı hem geliştirme hem üretim ortamında çalıştırmanız gerektiğinde, tek bir docker-compose.yml yetersiz kalabilir. Compose’un override mekanizmasını kullanın:

# docker-compose.yml (base, her ikisinde de geçerli)
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    env_file: .env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend

volumes:
  postgres_data:
# docker-compose.override.yml (geliştirme - otomatik yüklenir)
version: '3.8'

services:
  api:
    build:
      context: .
      target: development
    volumes:
      - .:/app              # Hot reload için
    ports:
      - "8080:80"
      - "9229:9229"         # Debug portu

  postgres:
    ports:
      - "5432:5432"         # Geliştirmede DB'ye direkt erişim
# docker-compose.prod.yml (üretim)
version: '3.8'

services:
  api:
    image: registry.mycompany.com/myapp:${IMAGE_TAG}
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M

Kullanımı:

# Geliştirme (otomatik override yüklenir)
docker compose up -d

# Üretim
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

deploy Alanı ve Resource Limitleri

Tek bir makinede bile olsa kaynak limitlerini tanımlamak iyi bir pratiktir. Bir servisin kontrolden çıkması diğerlerini etkilemesin diye:

version: '3.8'

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

Önemli: deploy bloğu Docker Compose v3’te Swarm mod için tasarlanmıştır. Standart docker compose up ile bazı deploy özellikleri (replica sayısı gibi) dikkate alınmaz. Kaynak limitlerini garantilemek istiyorsanız alternatif olarak mem_limit ve cpus alanlarını doğrudan servis seviyesinde kullanabilirsiniz.

Sık Yapılan Hatalar

Yıllarca docker-compose.yml yazarken ve başkalarının yazdığını okurken karşılaştığım en yaygın hataları paylaşayım:

  • Şifreleri dosyaya gömmek: .env dosyası kullanın ve .gitignore‘a ekleyin
  • depends_on’un sağlıklı başlatmayı garantilemediğini unutmak: healthcheck olmadan depends_on sadece başlatma sırasını belirler
  • Tüm portları dışarıya açmak: Sadece gerçekten dışarıya açılması gereken portları ports ile tanımlayın, servisler arası iletişim için expose yeterli
  • Volume’leri kullanmamak: Veritabanı konteyneri silindiğinde tüm veriniz gider, named volume şart
  • Tek ağ kullanmak: Tüm servisler aynı ağdaysa birbirine erişebilir, ağ segmentasyonu önemli
  • Geliştirme yapılandırmasını üretime taşımak: Kod bind mount’u, debug portları ve benzeri şeyler üretime çıkmamalı

Sonuç

docker-compose.yml dosyası, ilk bakışta karmaşık görünebilir ama mantığını kavradığınızda altyapınızı yönetmenin en temiz yolu haline gelir. Bu yazıda ele aldığımız temel alanlar, version, services, networks ve volumes bloklarıyla beraber image, build, ports, environment, volumes, depends_on, healthcheck ve restart gibi servis özellikleri, gerçek hayattaki vakaların büyük çoğunluğunu karşılar.

Başlangıç için şu pratikleri benimseyin: Hassas bilgileri her zaman .env dosyasına alın, veritabanı gibi durum koruyan servislere mutlaka named volume ekleyin, servisler arası ağ segmentasyonunu ihmal etmeyin ve healthcheck ile depends_on ikilisini birlikte kullanmayı alışkanlık haline getirin.

Geliştirme ve üretim ortamlarınız için ayrı override dosyaları kullanmak başlangıçta fazladan iş gibi görünse de ilerleyen süreçte bu ayrımın sizin yerinize kaç sorunu önlediğini görünce bu yapıya olan sevginiz artacak. Compose dosyanızı bir kez doğru yazın, tekrar tekrar kazanın.

Yorum yapın