Geliştirme ortamında her şey güzel çalışıyor, production’a attınız ve sistem çakıldı. Tanıdık geldi mi? Bu klasik senaryonun temel nedenlerinden biri, geliştirme ve üretim ortamları arasındaki konfigürasyon farklarının düzgün yönetilememesidir. Docker Compose’un override dosyası mekanizması, bu problemi zarif bir şekilde çözen ama çoğu ekip tarafından tam anlamıyla kullanılmayan bir özelliktir. Bugün bu mekanizmayı derinlemesine inceleyeceğiz.
Override Dosyaları Nedir ve Nasıl Çalışır?
Docker Compose, birden fazla Compose dosyasını bir araya getirip birleştirebilir. Temel mantık şudur: bir ana docker-compose.yml dosyanız olur, üzerine ortama özel dosyalar bindirirsiniz. Bu “bindirme” işlemi aslında derin bir birleştirme (deep merge) operasyonudur.
Compose varsayılan olarak şu sırayı takip eder:
- İlk önce
docker-compose.ymlokunur - Eğer
docker-compose.override.ymlvarsa, otomatik olarak üzerine uygulanır -fparametresiyle ek dosyalar belirtilirse, sırayla uygulanır
Bu otomatik birleştirme davranışı sayesinde geliştiriciler ekstra parametre belirtmeden docker compose up diyebilir ve development ortamı ayarları devreye girer. Production’da ise override dosyasını açıkça belirtirsiniz.
Birleştirme kuralları basit ama bilmek gerekir:
- Tekil değerler (image, command, restart): Son dosyadaki değer geçer
- Liste değerleri (ports, volumes, depends_on): Listelere ekleme yapılır, replace edilmez
- Dictionary değerleri (environment, labels): Key bazında birleştirilir
Proje Yapısını Oturtmak
Önce sağlam bir klasör yapısı kuralım. Gerçek dünya projelerinde genellikle şu yapı işe yarar:
myapp/
├── docker-compose.yml # Base konfigürasyon
├── docker-compose.override.yml # Development (otomatik yüklenir)
├── docker-compose.prod.yml # Production
├── docker-compose.staging.yml # Staging
├── docker-compose.test.yml # Test/CI
├── .env # Development ortam değişkenleri
├── .env.prod # Production ortam değişkenleri
└── .env.example # Örnek env dosyası (repoya eklenir)
.env ve .env.prod dosyaları asla git’e eklenmez. .env.example ise şablon görevi görür.
Base Compose Dosyası
Ana docker-compose.yml dosyası ortam bağımsız, ortak tanımları içermelidir. Burada sihirli değerler olmaz, sadece iskelet vardır:
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
image: myapp:${APP_VERSION:-latest}
environment:
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
- APP_ENV=${APP_ENV:-development}
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-myapp}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
networks:
default:
name: myapp_network
Bu dosyada port mapping yok, volume tanımı minimal, restart policy yok. Bunları override dosyalarına bırakıyoruz.
Development Override Dosyası
Development ortamında ihtiyacımız olan şeyler tamamen farklıdır. Hot reload, debug port’ları, kaynak kod mount’ı, gereksiz güvenlik kısıtlamalarını kaldırma bunların başında gelir:
# docker-compose.override.yml (development - otomatik yüklenir)
version: '3.8'
services:
app:
build:
target: development # Multi-stage build hedefi
args:
- INSTALL_DEV_DEPS=true
volumes:
- .:/app # Kaynak kod canlı mount
- /app/node_modules # node_modules override edilmesin
- app_dev_cache:/app/.cache
ports:
- "3000:3000" # Uygulama portu
- "9229:9229" # Node.js debug portu
environment:
- DEBUG=app:*
- NODE_ENV=development
- CHOKIDAR_USEPOLLING=true # Docker içinde dosya izleme
command: npm run dev # Development server komutu
db:
ports:
- "5432:5432" # DB'ye host'tan direkt erişim
volumes:
- postgres_dev_data:/var/lib/postgresql/data
redis:
ports:
- "6379:6379" # Redis'e host'tan erişim
# Sadece dev'de çalışan servisler
mailhog:
image: mailhog/mailhog:latest
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
networks:
- default
adminer:
image: adminer:latest
ports:
- "8080:8080"
environment:
ADMINER_DEFAULT_SERVER: db
networks:
- default
volumes:
postgres_dev_data:
app_dev_cache:
Dikkat edin: mailhog ve adminer servisleri sadece override dosyasında tanımlı. Base dosyada bunların hiç varlığı yok. Override dosyaları yeni servis ekleyebilir, bu güzel bir özellik.
Production Compose Dosyası
Production dosyası güvenlik, performans ve kararlılık odaklı olmalıdır:
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
target: production # Optimize edilmiş production image
args:
- INSTALL_DEV_DEPS=false
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000" # Sadece localhost'a bind (nginx arkasında)
environment:
- NODE_ENV=production
- LOG_LEVEL=warn
deploy:
resources:
limits:
cpus: '2'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/tmp
db:
restart: unless-stopped
volumes:
- postgres_prod_data:/var/lib/postgresql/data
- ./backups:/backups
environment:
POSTGRES_INITDB_ARGS: "--data-checksums"
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "5"
redis:
restart: unless-stopped
command: >
redis-server
--appendonly yes
--requirepass ${REDIS_PASSWORD}
--maxmemory 256mb
--maxmemory-policy allkeys-lru
volumes:
- redis_prod_data:/data
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
- static_files:/var/www/static:ro
depends_on:
- app
networks:
- default
volumes:
postgres_prod_data:
driver: local
driver_opts:
type: none
o: bind
device: /data/postgres
redis_prod_data:
static_files:
Ortam Dosyalarını Yönetmek
Her ortam için ayrı .env dosyası kullanmak hayatı kolaylaştırır. Ancak bu dosyaların yönetimi önemli:
# .env.example (repoya eklenir, şablon görevi görür)
APP_VERSION=1.0.0
APP_ENV=development
# Database
DATABASE_URL=postgresql://postgres:password@db:5432/myapp
POSTGRES_DB=myapp
POSTGRES_USER=postgres
POSTGRES_PASSWORD=CHANGE_THIS_IN_PRODUCTION
# Redis
REDIS_PASSWORD=CHANGE_THIS_IN_PRODUCTION
# App secrets
SECRET_KEY=CHANGE_THIS_IN_PRODUCTION
JWT_SECRET=CHANGE_THIS_IN_PRODUCTION
# External services
SMTP_HOST=mailhog
SMTP_PORT=1025
Production’da .env.prod dosyasını kullanırken şifreli bir vault çözümü (HashiCorp Vault, AWS Secrets Manager) veya en azından şifrelenmiş dosya (git-crypt, sops) kullanmanızı şiddetle tavsiye ederim. Düz metin production credential’ları ne kadar tehlikeli olduğunu anlatmama gerek yok sanırım.
Compose Komutlarını Pratikte Kullanmak
Şimdi bu yapıyı nasıl kullanacağımıza bakalım:
# Development - override.yml otomatik yüklenir
docker compose up -d
docker compose up -d --build # Image'ları rebuild et
# Hangi konfigürasyonun yüklendiğini görmek (çok işe yarar!)
docker compose config
# Production - açıkça belirtiyoruz, override YÜKLENMİYOR
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Production build + deploy
docker compose -f docker-compose.yml -f docker-compose.prod.yml
--env-file .env.prod
up -d --build --remove-orphans
# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml
--env-file .env.staging
up -d
# Tek servis rebuild
docker compose -f docker-compose.yml -f docker-compose.prod.yml
up -d --build --no-deps app
# Logları takip et
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f app
Dikkat: Production komutunda docker-compose.yml‘i açıkça belirtiyoruz. Eğer sadece -f docker-compose.prod.yml yazsaydık, base dosya yüklenmezdi. Hem base hem override belirtilmeli.
Makefile ile Komutları Basitleştirme
Uzun docker compose komutlarını her seferinde yazmak hem hataya açık hem de sinir bozucu. Bir Makefile ile bunu çözebilirsiniz:
# Makefile
.PHONY: dev prod staging test clean logs ps
COMPOSE_DEV = docker compose
COMPOSE_PROD = docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.prod
COMPOSE_STAGING = docker compose -f docker-compose.yml -f docker-compose.staging.yml --env-file .env.staging
# Development
dev:
$(COMPOSE_DEV) up -d
dev-build:
$(COMPOSE_DEV) up -d --build
dev-down:
$(COMPOSE_DEV) down
dev-logs:
$(COMPOSE_DEV) logs -f
# Production
prod:
$(COMPOSE_PROD) up -d --remove-orphans
prod-build:
$(COMPOSE_PROD) up -d --build --remove-orphans
prod-down:
$(COMPOSE_PROD) down
prod-logs:
$(COMPOSE_PROD) logs -f
# Database işlemleri
prod-db-backup:
$(COMPOSE_PROD) exec db pg_dumpall -U postgres > backups/backup_$(shell date +%Y%m%d_%H%M%S).sql
prod-db-restore:
$(COMPOSE_PROD) exec -T db psql -U postgres < $(BACKUP_FILE)
# Genel
ps:
docker compose ps
clean:
docker compose down -v --remove-orphans
docker system prune -f
config-check-prod:
$(COMPOSE_PROD) config
Artık make dev veya make prod-build gibi temiz komutlar kullanabilirsiniz.
Gerçek Dünya Senaryosu: Node.js + PostgreSQL Uygulaması
Teorik değil, gerçek bir senaryoya bakalım. Diyelim ki bir e-ticaret backend’i yönetiyorsunuz. Development’ta:
- Kodda değişiklik yapınca sunucu otomatik restart ediyor
- Veritabanına host makineden bağlanıp sorgu çalıştırabiliyorsunuz
- E-posta gönderme MailHog’a gidiyor, gerçek mail gitmiyor
- Debug modu açık, verbose log var
Production’da:
- Uygulama read-only filesystem üzerinde çalışıyor
- Tüm portlar iç ağda, dışarıya sadece nginx üzerinden erişim var
- Gerçek SMTP servisi kullanılıyor
- Resource limit’ler tanımlı, bir servis çıldırırsa sunucuyu yemiyor
- Log rotation var, disk dolmuyor
Bu iki ortamın konfigürasyonu override dosyaları olmadan tek dosyada yönetmek hem okunaksız hem de hata riskini artırır. Override yaklaşımıyla base dosya her iki ortamda da geçerli ortak zemini tutar.
CI/CD Pipeline’ında Override Kullanımı
GitHub Actions veya GitLab CI’da bu yapıyı nasıl kullanacağınıza dair bir örnek:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create production env file
run: |
echo "APP_VERSION=${{ github.sha }}" > .env.prod
echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env.prod
echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env.prod
echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env.prod
echo "REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}" >> .env.prod
- name: Copy files to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "docker-compose.yml,docker-compose.prod.yml,.env.prod,nginx/"
target: "/opt/myapp"
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
docker compose -f docker-compose.yml -f docker-compose.prod.yml
--env-file .env.prod
pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml
--env-file .env.prod
up -d --remove-orphans
# Kullanılmayan image'ları temizle
docker image prune -f
Burada dikkat edin: CI/CD pipeline’ına docker-compose.override.yml asla gitmiyor. Sadece gerekli dosyalar sunucuya kopyalanıyor.
Sık Yapılan Hatalar
Birkaç yıldır bu pattern’i kullanan biri olarak en sık karşılaştığım hataları paylaşayım:
- Override.yml’i production’a göndermek: Otomatik yükleme özelliği bir tuzaktır. CI/CD’de
docker-compose.ymltek başına çalıştırılırsa override yüklenmez ama birisi “debug için” yanlışlıkla dosyayı sunucuya atmışsa kaos çıkar.
- Base dosyada production-only değerler bırakmak: Restart policy, resource limit gibi şeyler development’ta da devreye girer ve bazen beklenmedik davranışlara yol açar.
- Compose config komutunu kullanmamak: Birleştirme sonucunu görmeden nasıl ne çalışacağını anlarsınız?
docker compose configaltın değerinde bir komuttur, her değişiklikten sonra çalıştırma alışkanlığı edinin.
- Volume isimlerini ortamlar arası karıştırmak: Development volume’u ve production volume’u aynı isme sahip olmamalı. Özellikle aynı sunucuda her ikisi çalışıyorsa bu felaket olur.
- .env.example’ı güncel tutmamak: Yeni bir değişken eklediğinizde hem
.envhem.env.prodhem de.env.example‘ı güncellemeyi unutanlar çoktur. Yeni geliştirici geldiğinde neden çalışmıyor diye saatlerce uğraşır.
Test Ortamı Ekleme
Bütünlük açısından bir de test/CI override dosyasına bakalım:
# docker-compose.test.yml
version: '3.8'
services:
app:
build:
target: test
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://postgres:testpass@db:5432/myapp_test
command: npm test
depends_on:
db:
condition: service_healthy
db:
environment:
POSTGRES_DB: myapp_test
POSTGRES_PASSWORD: testpass
tmpfs:
- /var/lib/postgresql/data # Test için disk yerine RAM kullan, çok hızlı
redis:
command: redis-server # Test'te persistence gerekmez
Test ortamında veritabanı için tmpfs kullanmak test süresini dramatik şekilde kısaltır. Disk I/O ortadan kalktığı için özellikle entegrasyon testlerinde fark çok belirgin olur.
Sonuç
Docker Compose override mekanizması, tek bir docker-compose.yml içinde if dev else prod koşulları yazmak yerine temiz, okunabilir ve bakımı kolay bir yapı sunar. Base dosya ortak zemini tutar, override dosyaları ortam özelliklerini ekler veya değiştirir.
Uygulamaya koyarken şu adımları takip edin: Önce base dosyayı oluşturun, tüm ortamlarda geçerli olan minimal konfigürasyonu buraya koyun. Sonra docker-compose.override.yml ile development konforunu sağlayın. docker-compose.prod.yml ile production güvenlik ve kararlılığını kurun. Makefile veya benzeri bir yardımcı araçla komutları basitleştirin. Son olarak CI/CD pipeline’ınızın doğru dosya kombinasyonlarını kullandığından emin olun.
Bu yaklaşımın en büyük faydası, “development’ta çalışıyor production’da çalışmıyor” sorunlarının büyük bir kısmını ortadan kaldırmasıdır. Çünkü environment farkları artık açık, versiyonlanmış ve gözlemlenebilir dosyalarda tutuluyor. Bir ekip arkadaşınız neden production’un farklı davrandığını merak ettiğinde, diff alması yeterli.