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'letarget: 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 configBu 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 # 100MBKı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: 5start_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şlatProduction 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 dizinBu 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ış YOKEriş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-servicesServis Bazlı Network Ayarları
services:
api:
networks:
frontend:
aliases: # Ek DNS alias'ları
- api.local
- backend-api
ipv4_address: 172.28.0.10 # Sabit IPVolumes — 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-pgdataexternal: 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-dataExternal volume'larda prefix eklenmez. Prefix'i değiştirmek için:
docker compose -p custom-name up -d
# custom-name_pgdataTam Ö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: trueBu dosyada dikkat edilmesi gereken noktalar:
API
exposekullanıyor,portsdeğil — sadece Nginx üzerinden erişilebilirVeritabanı ve Redis sadece
backendnetwork'te — dışarıdan erişilemezBackend network
internal: true— internete çıkış yokHer 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 --volumesYaygı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 configile 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ı.
AI Asistan
Sorularını yanıtlamaya hazır