Veritabanı ve Web Uygulamasını Birlikte Çalıştırma: Docker Compose Rehberi

Gerçek bir prodüksiyon ortamında tek başına çalışan bir web uygulaması pek rastlanan bir şey değil artık. Uygulamanın yanında bir veritabanı var, belki bir cache katmanı var, belki bir reverse proxy var. Bunları ayrı ayrı ayağa kaldırmak, bağlantılarını elle yönetmek ve her seferinde onlarca docker run komutu yazmak hem yorucu hem de hata yapmaya açık bir süreç. İşte tam bu noktada Docker Compose devreye giriyor ve “bu servisler birlikte çalışacak” diyerek işi tek bir dosyaya indirgiyor.

Bu yazıda sıfırdan başlayarak gerçekçi bir senaryo üzerinden ilerleyeceğiz: PostgreSQL veritabanı ve Node.js tabanlı bir web uygulaması. Ama konuyu orada bırakmayacağız, Redis cache katmanı ekleyeceğiz, environment variable yönetimini doğru yapacağız, volume ve network konularını masaya yatıracağız ve prodüksiyon ortamına taşımadan önce dikkat edilmesi gereken noktalara değineceğiz.

Docker Compose Neden Bu Kadar Popüler?

Compose’un çözdüğü temel problem şu: çok servisli bir uygulamayı tek bir konfigürasyon dosyasıyla tanımlamak ve tek komutla ayağa kaldırmak. Bunun ötesinde servisler arası bağımlılıkları yönetmek, network izolasyonu sağlamak ve geliştirme ile prodüksiyon ortamları arasında tutarlılığı korumak da Compose’un sağladığı avantajlar arasında.

Bir developer makinesinde çalışan şeyin CI/CD pipeline’ında da, staging sunucusunda da aynı şekilde çalışması için Compose dosyası adeta bir sözleşme işlevi görüyor. “Bende çalışıyor” sorununu büyük ölçüde ortadan kaldırıyor.

Proje Yapısını Kurmak

Önce dizin yapımızı oluşturalım. Düzenli bir yapı, ileride bakım yapacak kişinin (çoğunlukla gelecekteki sen) işini kolaylaştırır.

mkdir webapp-compose-demo
cd webapp-compose-demo

mkdir -p app/src
mkdir -p nginx/conf.d
mkdir -p postgres/init

touch docker-compose.yml
touch docker-compose.override.yml
touch .env
touch .env.example

Bu yapıda app/ klasörü web uygulamamızı, nginx/ klasörü reverse proxy konfigürasyonunu, postgres/ klasörü ise başlangıç SQL script’lerini barındıracak.

Temel Compose Dosyasını Oluşturmak

Önce en basit haliyle başlayalım, sadece uygulama ve veritabanı:

# docker-compose.yml
version: "3.9"

services:
  db:
    image: postgres:15-alpine
    container_name: webapp_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME:-appdb}
      POSTGRES_USER: ${DB_USER:-appuser}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./postgres/init:/docker-entrypoint-initdb.d:ro
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-appuser} -d ${DB_NAME:-appdb}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: webapp_app
    restart: unless-stopped
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://${DB_USER:-appuser}:${DB_PASSWORD}@db:5432/${DB_NAME:-appdb}
      PORT: 3000
    depends_on:
      db:
        condition: service_healthy
    networks:
      - backend
      - frontend
    ports:
      - "3000:3000"

volumes:
  postgres_data:
    driver: local

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

Burada dikkat edilmesi gereken birkaç önemli nokta var. depends_on ile condition: service_healthy kombinasyonu çok kritik. Sadece depends_on: db yazsaydık, Compose veritabanı container’ı başlatıldıktan hemen sonra uygulamayı da başlatırdı. Oysa PostgreSQL’in gerçekten hazır hale gelmesi 10-20 saniye sürebilir. service_healthy condition’ı healthcheck geçene kadar uygulamanın başlamasını bekletiyor.

Environment Variable Yönetimi

Şifreler ve hassas bilgileri Compose dosyasına doğrudan yazmak felaket reçetesi. .env dosyasını doğru kullanmak şart:

# .env dosyası (asla git'e commit etme!)
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=guclu_bir_sifre_buraya_yaz
REDIS_PASSWORD=redis_sifresi_buraya
SECRET_KEY=uygulama_secret_key_buraya
# .env.example dosyası (git'e commit edilebilir, şablondur)
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=change_this_password
REDIS_PASSWORD=change_this_password
SECRET_KEY=change_this_secret_key

.gitignore dosyasına mutlaka .env ekle:

echo ".env" >> .gitignore
echo "*.env.local" >> .gitignore

Compose, docker-compose.yml ile aynı dizindeki .env dosyasını otomatik olarak yükler. ${DEGISKEN_ADI:-varsayilan_deger} syntax’ıyla da değişken tanımlı değilse kullanılacak varsayılan değer belirtebilirsin.

Redis Cache Katmanını Eklemek

Gerçek dünya uygulamalarında Redis olmadan iş olmuyor. Session yönetimi, cache, rate limiting… Hepsinde Redis kullanılıyor. Servisimize ekleyelim:

# docker-compose.yml'e eklenecek servis
  redis:
    image: redis:7-alpine
    container_name: webapp_redis
    restart: unless-stopped
    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 256mb
      --maxmemory-policy allkeys-lru
      --save 60 1000
      --loglevel warning
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

Redis için maxmemory ve maxmemory-policy ayarları production’da çok önemli. Bellek limiti koymadan Redis, sistemin tüm RAM’ini yutabilir. allkeys-lru policy’si bellek dolduğunda en az kullanılan anahtarları otomatik temizler.

Volumes bölümüne de redis_data ekle:

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

Nginx Reverse Proxy Eklemek

Web uygulamasını doğrudan 3000 portunda açmak yerine nginx arkasına almak çok daha iyi bir pratik. SSL termination, static file serving, rate limiting hepsini nginx üzerinden yönetebilirsin:

# nginx/conf.d/app.conf
upstream webapp {
    server app:3000;
    keepalive 32;
}

server {
    listen 80;
    server_name _;

    # Static dosyalar için cache
    location /static/ {
        alias /app/static/;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }

    # Uygulama proxy
    location / {
        proxy_pass http://webapp;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # Timeout ayarları
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "healthyn";
        add_header Content-Type text/plain;
    }
}

Nginx servisini Compose dosyasına ekle:

  nginx:
    image: nginx:1.25-alpine
    container_name: webapp_nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      app:
        condition: service_healthy
    networks:
      - frontend

Artık app servisinin ports bölümünü kaldırabilirsin. Uygulama sadece iç network’ten erişilebilir olsun, dışarıya nginx üzerinden çıksın.

Geliştirme Ortamı Override Dosyası

Development’ta her değişiklikte image rebuild etmek zorunda kalmamak için override dosyasını kullan:

# docker-compose.override.yml
version: "3.9"

services:
  app:
    build:
      target: development
    environment:
      NODE_ENV: development
    volumes:
      - ./app:/app
      - /app/node_modules
    command: npm run dev
    ports:
      - "3000:3000"
      - "9229:9229"  # Node.js debugger portu

  db:
    ports:
      - "5432:5432"  # Development'ta DB'ye doğrudan erişim
    environment:
      POSTGRES_PASSWORD: dev_password_123

  redis:
    ports:
      - "6379:6379"  # Development'ta Redis'e doğrudan erişim

docker-compose.override.yml dosyası, docker compose up komutunu çalıştırdığında otomatik olarak docker-compose.yml ile merge edilir. Production’da ise şu şekilde override olmadan çalıştırabilirsin:

# Development (override otomatik dahil olur)
docker compose up -d

# Production (sadece ana dosyayı kullan)
docker compose -f docker-compose.yml up -d

# Ya da spesifik dosyaları belirt
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Veritabanı Başlangıç Script’leri

Uygulama ilk kez ayağa kalktığında veritabanında bazı tablolar veya veriler hazır olsun istiyorsan, postgres/init/ dizinine SQL dosyaları koyabilirsin:

-- postgres/init/01_schema.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS sessions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    token VARCHAR(500) NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);

Bu dosyalar sadece veritabanı ilk oluşturulurken çalışır. Eğer postgres_data volume’u zaten varsa bu script’ler tekrar çalışmaz.

Sık Kullanılan Compose Komutları

Günlük iş akışında en çok kullanacağın komutlar:

# Tüm servisleri arka planda başlat
docker compose up -d

# Sadece belirli servisi başlat
docker compose up -d db redis

# Logları takip et (tüm servisler)
docker compose logs -f

# Sadece app loglarını izle
docker compose logs -f app

# Servislerin durumunu gör
docker compose ps

# Belirli bir container'da komut çalıştır
docker compose exec db psql -U appuser -d appdb

# Redis'e bağlan
docker compose exec redis redis-cli -a ${REDIS_PASSWORD}

# Servisleri yeniden başlat
docker compose restart app

# Sadece değişen servisleri rebuild edip başlat
docker compose up -d --build app

# Her şeyi kapat ve volume'ları SİL (dikkatli ol!)
docker compose down -v

# Her şeyi kapat ama volume'ları koru
docker compose down

Health Check ve Bağımlılık Yönetimi

Uygulamanın kendi healthcheck’ini de tanımlamak önemli. Compose’un service_healthy condition’ı kullanabilmesi için bunu yapılandırman gerekiyor:

  app:
    # ... diğer ayarlar ...
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

start_period değeri önemli. Uygulama ilk başladığında biraz zaman gerekiyor. Bu süre içinde healthcheck başarısız olursa Compose bunu failure saymaz, sadece bekler.

Prodüksiyon İçin Ekstra Ayarlar

Production ortamına geçmeden önce birkaç kritik ayar daha:

# docker-compose.prod.yml
version: "3.9"

services:
  app:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  db:
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "5"

  redis:
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M

Resource limit koymak hem sistemi korur hem de bir servisin kontrolden çıkmasını engeller. Logging driver ayarları da kritik; varsayılan olarak Docker log dosyaları sonsuza kadar büyüyebilir, max-size ve max-file ile bunu sınırlandır.

Veritabanı Backup Stratejisi

Veritabanını Compose ile çalıştırıyorsan backup işini de Compose ekosistemi içinde çözebilirsin:

# Manuel backup
docker compose exec db pg_dump -U appuser -d appdb | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz

# Backup'ı geri yükle
gunzip -c backup_20240115_120000.sql.gz | docker compose exec -T db psql -U appuser -d appdb

# Tüm PostgreSQL cluster'ını yedekle (daha kapsamlı)
docker compose exec db pg_dumpall -U appuser | gzip > full_backup_$(date +%Y%m%d).sql.gz

Backup işini cron’a ekle:

# crontab -e
0 2 * * * cd /opt/webapp && docker compose exec -T db pg_dump -U appuser -d appdb | gzip > /opt/backups/db_$(date +%Y%m%d_%H%M%S).sql.gz

# 7 günden eski backup'ları temizle
0 3 * * * find /opt/backups -name "db_*.sql.gz" -mtime +7 -delete

Yaygın Sorunlar ve Çözümleri

Uygulama veritabanına bağlanamıyor: En sık karşılaşılan sorun bu. Kontrol listesi şu şekilde:

  • depends_on ile condition: service_healthy doğru yazıldı mı?
  • Healthcheck komutu gerçekten çalışıyor mu? docker compose exec db pg_isready -U appuser ile test et.
  • DATABASE_URL içindeki hostname servis adıyla eşleşiyor mu? Container name değil, servis adı kullanılmalı.
  • Her iki servis aynı network’te mi? docker compose exec app ping db ile test edebilirsin.

Volume izin hataları: PostgreSQL container’ı bazen volume üzerinde yazma izni hatası verebilir.

# Volume'u temizle ve yeniden oluştur
docker compose down -v
docker compose up -d

# Ya da volume'un sahibini kontrol et
docker volume inspect webapp-compose-demo_postgres_data

Port çakışması: Localde zaten bir PostgreSQL çalışıyorsa 5432 portu meşgul olur.

# Hangi process portu kullanıyor?
sudo ss -tlnp | grep 5432

# Ya da portu değiştir
ports:
  - "5433:5432"  # Host'ta 5433, container içinde 5432

Network Izolasyonu Hakkında

Compose projelerinde birden fazla network kullanmak iyi bir pratik. Örneğimizde:

  • backend network: db, redis ve app servisleri bu network’te. Dışarıdan erişilemiyor.
  • frontend network: nginx ve app bu network’te. Nginx dışarıya açık, app sadece nginx üzerinden erişilebilir.

Bu yapıyla veritabanı ve Redis, nginx’ten tamamen izole. Nginx sadece app ile konuşabiliyor. App her iki network’te olduğu için hem nginx’ten gelen istekleri karşılıyor hem de db ve redis’e ulaşabiliyor.

# Network'leri listele
docker network ls | grep webapp

# Hangi container hangi network'te?
docker network inspect webapp-compose-demo_backend

Compose Dosyasını Doğrulamak

Commit etmeden veya deploy etmeden önce Compose dosyasını doğrulamak iyi alışkanlık:

# Syntax kontrolü ve konfigürasyonu görüntüle
docker compose config

# Sadece geçerliliği kontrol et, çıktı verme
docker compose config --quiet && echo "Config geçerli" || echo "Config hatalı"

# Hangi image'ların kullanılacağını gör
docker compose config --images

docker compose config komutu environment variable’ları çözümlenmiş halde tüm konfigürasyonu çıktı veriyor. Değişkenlerin doğru değerleri alıp almadığını görmek için çok faydalı.

Sonuç

Docker Compose ile çok servisli uygulama yönetimi başlangıçta karmaşık görünse de doğru pratiğe ulaştıktan sonra vazgeçilmez oluyor. Özetlemek gerekirse:

  • Environment variable’ları asla dosyaya gömmeden .env ile yönet
  • Healthcheck tanımlarını es geçme, depends_on tek başına yeterli değil
  • Network izolasyonunu servis ihtiyaçlarına göre kurgula
  • Resource limit‘leri production’da mutlaka belirle
  • Override dosyası kullanarak dev ve prod konfigürasyonlarını birbirinden ayır
  • Backup stratejini Compose workflow’una dahil et

Burada anlattıklarımız temel bir web uygulaması senaryosuydu. Buna message queue (RabbitMQ, Kafka), ek mikroservisler, monitoring (Prometheus, Grafana) ekledikçe Compose dosyası büyüyor ama mantık aynı kalıyor. Her servis kendi sorumluluğunda, bağımlılıklar net, network’ler izole, veriler volume’larda güvende. Bu temeli sağlam kurduktan sonra üstüne ne inşa etsen yönetilebilir olur.

Yorum yapın