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_onilecondition: service_healthydoğru yazıldı mı?- Healthcheck komutu gerçekten çalışıyor mu?
docker compose exec db pg_isready -U appuserile test et. DATABASE_URLiç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 dbile 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
.envile yönet - Healthcheck tanımlarını es geçme,
depends_ontek 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.