Geliştirme ve Üretim Ortamı Ayrımı: Docker Compose Override Dosyaları

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.yml okunur
  • Eğer docker-compose.override.yml varsa, otomatik olarak üzerine uygulanır
  • -f parametresiyle 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.yml tek 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 config altı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 .env hem .env.prod hem 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.

Yorum yapın