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
Dockerfiledeğ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:
postgresveredissadecebackendağında, dışarıdan erişilemiyorphp-fpmher iki ağda çünkü hem NGINX’ten hem de veritabanından erişmesi gerekiyorstatic_filesadlı volume hemphp-fpmhemnginxtarafından paylaşılıyorqueue-worker,php-fpmile 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:
.envdosyası kullanın ve.gitignore‘a ekleyin - depends_on’un sağlıklı başlatmayı garantilemediğini unutmak:
healthcheckolmadandepends_onsadece başlatma sırasını belirler - Tüm portları dışarıya açmak: Sadece gerçekten dışarıya açılması gereken portları
portsile tanımlayın, servisler arası iletişim içinexposeyeterli - 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.