← Kursa Dön
📄 Text · 35 min

docker-compose.yml Anatomisi — services, networks, volumes

Bir önceki derste Docker Compose'un ne olduğunu ve temel kullanımını öğrendik. Bu derste docker-compose.yml dosyasının her bölümünü detaylıca inceleyeceğiz. Services, networks, volumes — her birinin tüm seçenekleri, incelikleri ve production'da nasıl kullanılacağı.

Bunu bir film senaryosu gibi düşün. Senaryo her şeyi tanımlar: hangi oyuncular sahnede, hangi dekor kullanılacak, ışıklar nasıl olacak. Yönetmen "başla!" dediğinde herkes ne yapacağını bilir çünkü senaryo var. docker-compose.yml senin senaryondur. docker compose up dediğinde tüm "set" ayağa kalkar.


Büyük Resim — Üç Ana Bölüm

Her docker-compose.yml dosyası üç ana bölümden oluşur:

# 1. SERVİSLER — Container tanımları
services:
  web:
    image: nginx:alpine
  api:
    build: ./api
  db:
    image: postgres:16

# 2. AĞ'LAR — Network tanımları
networks:
  frontend:
    driver: bridge
  backend:
    internal: true

# 3. VOLUME'LAR — Kalıcı depolama tanımları
volumes:
  pgdata:
  redis-data:

Şimdi her bölümü derinlemesine inceleyelim.


Services — Container Tanımları

services bölümü dosyanın kalbi. Her servis bir container tanımıdır. Servis adı hem DNS alias hem de yönetim referansı olarak kullanılır — yani db adını verdiğin servis, aynı network'teki diğer servislerden db ismiyle erişilebilir.

Image vs Build

Bir servis ya hazır bir image kullanır ya da Dockerfile'dan build edilir:

services:
  # Hazır image kullan
  db:
    image: postgres:16-alpine

  # Dockerfile'dan build et (basit)
  api:
    build: ./api

  # Dockerfile'dan build et (detaylı)
  api:
    build:
      context: ./api                 # Build context dizini
      dockerfile: Dockerfile.prod    # Dockerfile adı
      args:                          # Build arguments
        NODE_ENV: production
        VERSION: "2.1.0"
      target: production             # Multi-stage target stage
    image: myregistry/api:2.1.0      # Build sonrası bu isimle tag'le

target: production çok önemli. Multi-stage Dockerfile'ın varsa (development ve production stage'ler) hangi stage'i build edeceğini belirtir. Development ortamında target: development dersin, hot reload ve debug araçları dahil olur.

Port Mapping

services:
  web:
    ports:
      # En yaygın format: "HOST:CONTAINER"
      - "80:80"
      - "443:443"

      # Farklı host portu
      - "8080:80"

      # Sadece localhost
      - "127.0.0.1:5432:5432"

      # Dinamik host portu (Docker rastgele seçer)
      - "80"

      # UDP
      - "53:53/udp"

      # Uzun format (daha açık)
      - target: 80
        published: 8080
        protocol: tcp

    # expose: Sadece container network'üne açar, host'a değil
    expose:
      - "3000"

💡 İpucu: Port numaralarını her zaman string olarak yaz (tırnak içinde): "8080:80". Tırnaksız 80:80 bazı YAML parser'larda sorun çıkarabilir.

expose ile ports arasındaki fark: ports host'a açar (dışarıdan erişilebilir), expose sadece aynı network'teki diğer container'lara açar. Veritabanı gibi internal servisler için expose kullan, web sunucuları için ports.

Environment Variables

Dört farklı yöntemle environment variable tanımlayabilirsin:

services:
  api:
    # Yöntem 1: Liste format
    environment:
      - NODE_ENV=production
      - PORT=3000

    # Yöntem 2: Map format (aynı şey, farklı yazım)
    environment:
      NODE_ENV: production
      PORT: 3000

    # Yöntem 3: Dosyadan yükle
    env_file:
      - .env
      - .env.production

    # Yöntem 4: Variable substitution (.env'den)
    environment:
      DATABASE_URL: postgres://${DB_USER:-admin}:${DB_PASS}@db:5432/${DB_NAME:-myapp}

Variable substitution'da ${VAR:-default} syntax'ı var — eğer VAR tanımlı değilse default değerini kullanır. Bu çok kullanışlı çünkü .env dosyası olmadan da çalışır.

Hangi değerlerin kullanılacağını önceden görmek için:

docker compose config

Bu komut tüm substitution'ları çözümler ve final YAML'ı gösterir. Deploy öncesi mutlaka çalıştır.

Volumes (Servis Seviyesi)

services:
  api:
    volumes:
      # Named volume
      - pgdata:/var/lib/postgresql/data

      # Bind mount (relative path — . ile başlar)
      - ./src:/app/src

      # Bind mount, read-only
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

      # Anonymous volume
      - /app/node_modules

      # Uzun format (daha açık)
      - type: volume
        source: pgdata
        target: /var/lib/postgresql/data

      - type: bind
        source: ./src
        target: /app/src

      - type: tmpfs
        target: /app/tmp
        tmpfs:
          size: 104857600  # 100MB

Kısa format'ta ./ ile başlayan yol bind mount, isimsiz yol ise named volume olarak yorumlanır. Bu kural basit ama bazen kafa karıştırabilir — emin olmadığında uzun format kullan.

Healthcheck

Her kritik servise healthcheck ekle. Bu, servisin gerçekten çalışıp çalışmadığını Docker'a bildirir:

services:
  api:
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 30s       # Her 30 saniyede kontrol
      timeout: 10s         # 10 saniye içinde cevap gelmezse fail
      retries: 3           # 3 kez fail → unhealthy
      start_period: 40s    # İlk 40 saniye kontrol yapma

  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  redis:
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

start_period çok önemli. Veritabanı gibi başlaması uzun süren servislerde ilk X saniye healthcheck yapılmaz — servisin başlama zamanı tanınır.

Restart Policy

Container crash olduğunda ne yapılacağını belirler:

services:
  api:
    restart: unless-stopped
    # "no"            — Yeniden başlatma (varsayılan)
    # "always"        — Her zaman yeniden başlat
    # "on-failure"    — Sadece hata kodu ile çıkınca
    # "unless-stopped" — Manuel durdurmadıkça başlat

Production için unless-stopped en uygun seçenek. Container crash olursa otomatik başlar, ama sen docker compose stop ile durdurduysan başlatmaz.

Command ve Entrypoint Override

Container'ın varsayılan komutunu veya entrypoint'ini değiştirebilirsin:

services:
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb

  api:
    build: ./api
    entrypoint: ["docker-entrypoint.sh"]
    command: ["node", "server.js"]

Logging

Container loglarını sınırla — yoksa disk dolabilir:

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "20m"   # Her log dosyası max 20MB
        max-file: "5"      # En fazla 5 dosya (toplam max 100MB)

Güvenlik Ayarları

Production'da container'ın yetkilerini sınırla:

services:
  api:
    read_only: true           # Root filesystem read-only
    user: "1000:1000"         # Non-root user
    cap_drop:
      - ALL                   # Tüm Linux capability'leri kaldır
    cap_add:
      - NET_BIND_SERVICE      # Sadece gerekeni ekle
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp                  # Yazılabilir geçici dizin

Bu ayarlar saldırı yüzeyini minimize eder. Container'a sızılsa bile yapabilecekleri çok sınırlı olur.


Networks — Ağ Tanımları

Default Network

Network tanımlamazsan Compose otomatik bir default network oluşturur. Tüm servisler bu network'te birbirine erişir:

services:
  web:
    image: nginx:alpine
  api:
    image: myapp
  db:
    image: postgres:16
# Hepsi aynı default network'te
# web → db:5432 erişebilir — ama istemeyebilirsin!

Custom Network'ler — İzolasyon

Servisleri mantıksal network'lere ayırmak güvenlik sağlar:

services:
  nginx:
    networks:
      - frontend

  api:
    networks:
      - frontend    # nginx ile konuşabilir
      - backend     # db ile konuşabilir

  db:
    networks:
      - backend     # Sadece backend'de
    # nginx → db: BAĞLANAMAZ ✓

networks:
  frontend:
  backend:
    internal: true   # İnternete çıkış YOK

Erişim matrisi:

         nginx    api      db
nginx     —       ✅       ❌
api       ✅       —       ✅
db        ❌       ✅       —

Nginx doğrudan veritabanına erişemez. Bu güvenlik katmanı, network seviyesinde enforced — uygulama koduna bağımlı değil.

Network Konfigürasyonu

networks:
  backend:
    driver: bridge
    internal: true               # Dış erişim yok
    ipam:
      config:
        - subnet: 172.28.0.0/16  # Özel subnet
          gateway: 172.28.0.1
    labels:
      - "com.example.project=myapp"

  # External network (dışarıda oluşturulmuş)
  shared:
    external: true
    name: shared-services

Servis Bazlı Network Ayarları

services:
  api:
    networks:
      frontend:
        aliases:                 # Ek DNS alias'ları
          - api.local
          - backend-api
        ipv4_address: 172.28.0.10  # Sabit IP

Volumes — Kalıcı Depolama Tanımları

volumes:
  # Minimal tanım
  pgdata:

  # Driver belirtme
  redis-data:
    driver: local

  # Label ile
  uploads:
    labels:
      com.example.backup: "daily"

  # NFS volume
  shared-storage:
    driver: local
    driver_opts:
      type: nfs
      o: addr=192.168.1.100,rw,nfsvers=4
      device: ":/exports/docker"

  # External volume (dışarıda oluşturulmuş)
  production-data:
    external: true
    name: prod-pgdata

external: true olan volume'lar Compose tarafından oluşturulmaz — zaten var olması beklenir. Bu, production'da yanlışlıkla yeni boş volume oluşturulmasını önler.

Volume Naming

Compose, volume'lara proje adı prefix'i ekler:

docker compose up -d
docker volume ls
# myapp_pgdata
# myapp_redis-data

External volume'larda prefix eklenmez. Prefix'i değiştirmek için:

docker compose -p custom-name up -d
# custom-name_pgdata

Tam Örnek: Production-Ready Compose

Şimdi öğrendiğimiz her şeyi birleştiren kapsamlı bir örnek yazalım:

services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    depends_on:
      api:
        condition: service_healthy
    networks:
      - frontend
    restart: unless-stopped
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }

  api:
    build:
      context: ./backend
      target: production
    expose:
      - "8080"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
      REDIS_URL: redis://:${REDIS_PASS}@redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped
    read_only: true
    tmpfs: [/tmp]
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    deploy:
      resources:
        limits: { memory: 512M, cpus: "1.0" }

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./database/init:/docker-entrypoint-initdb.d: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
    deploy:
      resources:
        limits: { memory: 1G }

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --requirepass ${REDIS_PASS}
    volumes:
      - redis-data:/data
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASS}", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

volumes:
  pgdata:
    labels: { backup: "daily" }
  redis-data:

networks:
  frontend:
  backend:
    internal: true

Bu dosyada dikkat edilmesi gereken noktalar:

  • API expose kullanıyor, ports değil — sadece Nginx üzerinden erişilebilir

  • Veritabanı ve Redis sadece backend network'te — dışarıdan erişilemez

  • Backend network internal: true — internete çıkış yok

  • Her kritik serviste healthcheck var

  • Resource limitleri var

  • Logging limitleri var

  • API container'ı read_only: true — güvenlik


Config Doğrulama

Deploy öncesi mutlaka config'i doğrula:

# Tüm konfigürasyonu göster
docker compose config

# Sadece servis isimlerini
docker compose config --services

# Sadece volume isimlerini
docker compose config --volumes

Yaygın Hatalar

YAML indentation hatası: YAML'da tab kullanma, sadece space (2 veya 4). Tab'lar YAML'da syntax error.

Port string vs integer: 80:80 bazen sorun çıkarır. Her zaman "80:80" yaz (tırnak içinde).

service_healthy ama healthcheck yok: condition: service_healthy kullandıysan o serviste mutlaka healthcheck tanımla, yoksa hata alırsın.


Bu Derste Ne Öğrendik?

  • docker-compose.yml üç ana bölümden oluşur: services, networks, volumes.

  • Services bölümünde her container'ın image/build, port, volume, env, healthcheck ayarları tanımlanır.

  • Networks ile container'lar arası iletişimi yönet — frontend/backend ayrımı güvenlik sağlar.

  • Volumes ile kalıcı depolama tanımla — external volume'lar güvenli bir seçenek.

  • Healthcheck ve depends_on ile servis bağımlılıklarını doğru sırala.

  • docker compose config ile YAML doğrulaması yap — deploy öncesi hataları yakala.

Sonraki derste Docker Compose ile gerçek full-stack projeler kuracağız — blog platformu, microservice mimarisi ve development ortamı.