Kalıcı Veri Saklama: Docker Compose Ortamında Volume Kullanımı

Docker ile uygulama geliştirirken ya da production ortamı yönetirken, en sık yapılan hatalardan biri veri kalıcılığını göz ardı etmektir. Konteyner silindi, veri gitti. Konteyner yeniden oluşturuldu, veritabanı sıfırlandı. Bu senaryoyla bir kez karşılaşmak bile volume kullanımını hayatınızın vazgeçilmezi haline getirir. Compose ortamında kalıcı veri yönetimi, sadece bir konfigürasyon detayı değil, production-ready bir altyapının temel taşıdır.

Volume Nedir ve Neden Kritiktir?

Konteynerler doğası gereği geçicidir. Bir imajdan ayağa kalkar, işini yapar ve silindiğinde tüm dosya sistemi değişiklikleriyle birlikte ortadan kalkar. Bu, stateless uygulamalar için mükemmeldir. Ama bir PostgreSQL veritabanı, bir Redis instance’ı ya da kullanıcıların yüklediği dosyaları saklayan bir uygulama için bu durum felaket anlamına gelir.

Docker volume’ları bu sorunu çözmek için tasarlanmıştır. Volume, Docker tarafından yönetilen ve konteyner yaşam döngüsünden bağımsız olan bir depolama alanıdır. Konteyner silinse bile volume varlığını sürdürür. Yeni bir konteyner aynı volume’u bağladığında, tüm veriler yerli yerinde olur.

Compose ortamında volume yönetimi üç temel yöntemle yapılır:

  • Named Volume: Docker tarafından yönetilen, isimlendirilmiş volume’lar
  • Bind Mount: Host makinedeki bir dizini doğrudan konteynere bağlamak
  • Anonymous Volume: İsimsiz, geçici volume’lar (genellikle önerilmez)

Named Volume ile Çalışmak

Named volume’lar, production ortamları için en güvenli ve taşınabilir seçenektir. Docker, bu volume’ları kendi yönetim alanında tutar ve host’un dosya sistemi yapısından bağımsız kılar.

Basit bir örnek üzerinden başlayalım:

# docker-compose.yml
version: '3.8'

services:
  postgres:
    image: postgres:15
    container_name: uygulama_db
    environment:
      POSTGRES_DB: uygulamam
      POSTGRES_USER: dbadmin
      POSTGRES_PASSWORD: guclu_sifre_buraya
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

volumes:
  postgres_data:
    driver: local

Burada postgres_data isimli bir volume tanımladık ve bunu PostgreSQL’in veri dizinine bağladık. volumes: bloğu iki yerde geçiyor: servis tanımının içinde (bağlama noktasını belirtmek için) ve dosyanın en altında (volume’un kendisini tanımlamak için).

Volume’u incelemek için:

# Volume listesi
docker volume ls

# Volume detayları
docker volume inspect postgres_data_compose_uygulama

# Hangi konteynerlerin kullandığını görmek
docker ps --filter volume=postgres_data

Gerçek Dünya Senaryosu: Web Uygulaması + Veritabanı + Cache

Şimdi daha gerçekçi bir senaryoya bakalım. Bir e-ticaret uygulaması düşünelim: Django backend, PostgreSQL veritabanı ve Redis cache. Her birinin farklı kalıcılık ihtiyaçları var.

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    container_name: eticaret_web
    command: gunicorn eticaret.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_files:/app/staticfiles
      - media_files:/app/media
    environment:
      - DATABASE_URL=postgresql://dbadmin:guclu_sifre@db:5432/eticaret
      - REDIS_URL=redis://cache:6379/0
    depends_on:
      - db
      - cache
    restart: unless-stopped

  db:
    image: postgres:15
    container_name: eticaret_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backup:/backup
    environment:
      POSTGRES_DB: eticaret
      POSTGRES_USER: dbadmin
      POSTGRES_PASSWORD: guclu_sifre
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    container_name: eticaret_cache
    command: redis-server --appendonly yes --requirepass redis_sifresi
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: eticaret_nginx
    volumes:
      - static_files:/var/www/static:ro
      - media_files:/var/www/media:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  static_files:
  media_files:

Bu örnekte dikkat edilmesi gereken birkaç nokta var. static_files ve media_files volume’ları hem web hem de nginx servisi tarafından paylaşılıyor. Django’nun ürettiği statik dosyalar ve kullanıcıların yüklediği medya dosyaları, Nginx tarafından doğrudan sunulabiliyor. nginx servisindeki :ro eki, volume’un o konteyner tarafından sadece okunabilir olarak bağlandığını belirtir, bu iyi bir güvenlik pratiğidir.

Bind Mount Kullanımı ve Ne Zaman Tercih Edilmeli

Bind mount, host makinedeki gerçek bir dizini konteynere bağlar. Geliştirme ortamı için idealdir çünkü kod değişikliklerini anında konteynere yansıtır. Ama production’da dikkatli olmak gerekir.

# Geliştirme ortamı için docker-compose.dev.yml
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: uygulama_web_dev
    volumes:
      # Bind mount: kod değişiklikleri anında yansır
      - .:/app
      # Named volume: node_modules host'tan izole
      - node_modules:/app/node_modules
    ports:
      - "3000:3000"
    command: npm run dev
    environment:
      - NODE_ENV=development

  db:
    image: postgres:15
    volumes:
      # Production'da named volume kullanılır
      - postgres_dev_data:/var/lib/postgresql/data
      # Geliştirme için seed dosyaları
      - ./sql/seed:/docker-entrypoint-initdb.d:ro

volumes:
  node_modules:
  postgres_dev_data:

node_modules için named volume kullanmak önemli bir trick’tir. Eğer tüm proje dizinini bind mount yaparsanız ve node_modules host’ta mevcutsa, konteyner içindeki node_modules‘u gölgeler. Bu özellikle farklı işletim sistemlerinde (macOS host, Linux container) sorun çıkarır.

Volume Driver’ları ve İleri Seviye Kullanım

Varsayılan local driver dışında, farklı senaryolar için özelleştirilmiş driver’lar kullanabilirsiniz.

# NFS volume kullanımı (shared storage senaryosu)
version: '3.8'

services:
  app:
    image: myapp:latest
    volumes:
      - shared_uploads:/app/uploads

volumes:
  shared_uploads:
    driver: local
    driver_opts:
      type: nfs
      o: addr=192.168.1.100,rw,noatime,rsize=8192,wsize=8192
      device: ":/mnt/nfs/uploads"

Birden fazla uygulama sunucusunun aynı dosyalara erişmesi gerektiğinde NFS volume’ları kurtarıcı olur. Özellikle yatay ölçekleme yapılan ortamlarda kullanıcı yüklemelerini ya da shared config dosyalarını bu şekilde yönetebilirsiniz.

External volume tanımı da önemli bir kavramdır. Docker Compose dışında oluşturulmuş bir volume’u compose projesine dahil edebilirsiniz:

# Önce volume'u elle oluştur
docker volume create --driver local 
  --opt type=none 
  --opt device=/srv/uygulama/veriler 
  --opt o=bind 
  onceden_olusturulmus_volume

# docker-compose.yml içinde external olarak referans ver
version: '3.8'

services:
  app:
    image: myapp:latest
    volumes:
      - onceden_olusturulmus_volume:/app/data

volumes:
  onceden_olusturulmus_volume:
    external: true

Bu yaklaşım, volume’un yaşam döngüsünü compose projesinden bağımsız tutmak istediğinizde faydalıdır. docker-compose down -v komutu bile bu volume’u silmez.

Volume Yedekleme ve Geri Yükleme

Production ortamında en önemli operasyonlardan biri yedeklemedir. Volume’ları yedeklemenin en pratik yöntemi, geçici bir konteyner ayağa kaldırıp tar ile sıkıştırmaktır.

# PostgreSQL volume yedeği alma
docker run --rm 
  -v eticaret_postgres_data:/kaynak:ro 
  -v $(pwd)/yedekler:/hedef 
  alpine 
  tar czf /hedef/postgres_yedek_$(date +%Y%m%d_%H%M%S).tar.gz -C /kaynak .

# Yedekten geri yükleme
docker run --rm 
  -v eticaret_postgres_data:/hedef 
  -v $(pwd)/yedekler:/kaynak:ro 
  alpine 
  tar xzf /kaynak/postgres_yedek_20240115_120000.tar.gz -C /hedef

PostgreSQL için daha doğru yöntem ise pg_dump kullanmaktır:

# pg_dump ile yedek alma (konteyner çalışıyorken)
docker exec eticaret_db pg_dump 
  -U dbadmin 
  -d eticaret 
  --format=custom 
  --compress=9 
  > yedekler/eticaret_$(date +%Y%m%d).dump

# pg_restore ile geri yükleme
docker exec -i eticaret_db pg_restore 
  -U dbadmin 
  -d eticaret 
  --clean 
  --if-exists 
  < yedekler/eticaret_20240115.dump

Yedekleme işlemini compose servisi olarak tanımlamak da mümkündür. Cron benzeri bir yedekleme servisi ekleyebilirsiniz:

version: '3.8'

services:
  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: uygulamam
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: sifre123

  db_yedek:
    image: postgres:15
    volumes:
      - ./yedekler:/yedekler
    environment:
      PGPASSWORD: sifre123
    command: >
      sh -c "while true; do
        pg_dump -h db -U admin uygulamam > /yedekler/yedek_$$(date +%Y%m%d_%H%M%S).sql
        echo 'Yedek alindi: '$$(date)
        find /yedekler -name '*.sql' -mtime +7 -delete
        sleep 86400
      done"
    depends_on:
      - db
    restart: unless-stopped

volumes:
  postgres_data:

Volume İzleme ve Temizlik

Zamanla kullanılmayan volume’lar disk alanını doldurabilir. Özellikle sık sık docker-compose up --build çalıştıran geliştirme ortamlarında bu sorun baş gösterir.

# Kullanılmayan volume'ları listele
docker volume ls -f dangling=true

# Kullanılmayan tüm volume'ları sil (dikkatli kullanın!)
docker volume prune

# Compose projesine ait tüm volume'ları sil
docker-compose down --volumes

# Belirli bir volume'u sil
docker volume rm eticaret_postgres_data

# Volume disk kullanımını görüntüle
docker system df -v | grep -A 20 "VOLUME NAME"

# Tüm Docker kaynaklarının özet disk kullanımı
docker system df

Uyarı: docker-compose down --volumes komutu tüm named volume’larınızı siler. Production ortamında bu komutu çalıştırmadan önce mutlaka yedek alın.

Sık Yapılan Hatalar ve Çözümleri

Permission sorunları en yaygın sorunların başında gelir. Özellikle Linux’ta host kullanıcısı ile konteyner kullanıcısının UID/GID’leri uyuşmazsa dosyalara erişilemeyebilir.

# Bind mount ile permission sorunu çözümü
version: '3.8'

services:
  app:
    image: node:18-alpine
    user: "1000:1000"  # Host kullanıcısının UID:GID
    volumes:
      - ./uygulama:/app
    working_dir: /app
    command: node server.js

# Alternatif: Dockerfile'da kullanıcı oluşturma
# RUN addgroup -g 1000 appgroup && 
#     adduser -u 1000 -G appgroup -D appuser
# USER appuser

Compose projeleri arası volume çakışması da sık karşılaşılan bir durumdur. Varsayılan olarak Docker Compose, volume isimlerinin önüne proje adını ekler. Bu davranışı kontrol etmek için:

# Proje adını açıkça belirt
docker-compose -p uretim up -d

# Ya da compose dosyasında
version: '3.8'

name: uretim_eticaret

services:
  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
  # Bu volume'un tam adı: uretim_eticaret_postgres_data olacak

Çoklu Ortam Yönetimi: Dev, Staging, Production

Aynı uygulamayı farklı ortamlarda çalıştırırken volume stratejinizi de ayarlamanız gerekir.

# docker-compose.yml (base)
version: '3.8'

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:
# docker-compose.override.yml (development - otomatik merge edilir)
version: '3.8'

services:
  db:
    ports:
      - "5432:5432"  # Dev'de port expose et
    volumes:
      - ./sql/test_data:/docker-entrypoint-initdb.d:ro  # Test verisi
  
  web:
    volumes:
      - .:/app  # Hot reload için bind mount
      - /app/node_modules  # node_modules'u koru
    environment:
      - DEBUG=true
# docker-compose.prod.yml
version: '3.8'

services:
  db:
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - /etc/ssl/certs:/etc/ssl/certs:ro  # SSL sertifikaları

volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /mnt/ssd/postgresql  # SSD üzerinde özel konum

Production compose dosyasını kullanmak için:

# Production ortamında çalıştırma
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Staging için
docker-compose -f docker-compose.yml -f docker-compose.staging.yml up -d

Volume Güvenliği

Volume güvenliği sıklıkla göz ardı edilir ama production ortamında kritik öneme sahiptir.

  • Hassas verileri secret olarak yönetin: Veritabanı şifrelerini environment variable yerine Docker secret ile kullanın
  • Read-only mount: Sadece okuma gerektiren durumlarda :ro ekini kullanın
  • Volume şifreleme: Hassas veriler için LUKS şifrelenmiş disk üzerinde volume oluşturun
  • Network isolation: Volume paylaşan servisler aynı ağda olmalı, gereksiz port expose etmeyin
# Docker secret kullanımı (Swarm mode gerektirir ama best practice)
version: '3.8'

services:
  db:
    image: postgres:15
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    volumes:
      - postgres_data:/var/lib/postgresql/data

secrets:
  db_password:
    file: ./secrets/db_password.txt

volumes:
  postgres_data:

Sonuç

Docker Compose ortamında kalıcı veri yönetimi, doğru yapılandırıldığında güçlü ve güvenilir bir altyapı sunar. Named volume’lar production için tercih edilmesi gereken yöntemdir; taşınabilirlik, izolasyon ve Docker’ın yerleşik yönetim araçlarından yararlanma açısından bind mount’lara göre belirgin avantajlar sağlar. Bind mount’lar ise geliştirme ortamında hot reload gibi ihtiyaçlar için vazgeçilmezdir.

Pratikte en çok işe yarayan kural şudur: production’da her stateful servisin (veritabanı, cache, dosya depolama) kendi named volume’u olmalı, bu volume’ların düzenli yedeği alınmalı ve yedeklerin geri yüklenebildiği periyodik olarak test edilmelidir. Çünkü alınmayan ya da geri yüklenemeyen yedek, hiç alınmamış yedekten farksızdır.

Compose projelerinizi name: direktifi ile açıkça isimlendirmek, birden fazla ortam yönetirken volume çakışmalarını önler. Çoklu compose dosyası yaklaşımıyla base konfigürasyonu ortamlara göre özelleştirmek ise hem kod tekrarını azaltır hem de yönetimi kolaylaştırır. Bu pratikleri bir kez oturttuğunuzda, konteyner tabanlı altyapınızda veri kaybı korkusuyla değil, güvenle çalışırsınız.

Yorum yapın