Compose ile Gerçek Projeler — Full-Stack Uygulama
Önceki iki derste Docker Compose'un ne olduğunu ve docker-compose.yml dosyasının anatomisini öğrendik. Şimdi teoriden pratiğe geçme zamanı. Bu derste gerçek dünyada karşılaşacağın tam teşekküllü projeler kuracağız — frontend, backend, veritabanı, cache, reverse proxy, hepsi bir arada.
Bir lokanta açıyorsun diyelim. Menü var, mutfak var, garsonlar var — ama bunları bir araya getiremeyen adam lokanta açamaz. Frontend garson gibi (müşteriye servis eder), backend aşçı gibi (iş mantığını çalıştırır), veritabanı malzeme deposu (verileri saklar), reverse proxy kapıdaki karşılama görevlisi (trafiği yönlendirir). Docker Compose ile bu "lokantayı" tek dosyada tanımlayıp tek komutla açıyoruz.
Proje 1: Blog Platformu
Gerçek bir blog platformu oluşturacağız: Nginx reverse proxy, Node.js backend API, PostgreSQL veritabanı ve Redis cache.
Proje Yapısı
blog-platform/
├── docker-compose.yml
├── docker-compose.dev.yml
├── .env
├── .env.example
├── nginx/
│ └── conf.d/
│ └── default.conf
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
│ └── server.js
└── database/
└── init/
└── 01-schema.sqldocker-compose.yml (Production)
services:
nginx:
image: nginx:1.25-alpine
ports:
- "${HTTP_PORT:-80}:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
api:
condition: service_healthy
networks:
- frontend
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
api:
build:
context: ./backend
target: production
expose:
- "8080"
environment:
NODE_ENV: production
PORT: 8080
DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
REDIS_URL: redis://:${REDIS_PASS}@redis:6379
JWT_SECRET: ${JWT_SECRET}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- frontend
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
deploy:
resources:
limits:
memory: 512M
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
- ./database/init:/docker-entrypoint-initdb.d:ro
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 64mb --maxmemory-policy allkeys-lru --requirepass ${REDIS_PASS}
volumes:
- redis-data:/data
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASS}", "ping"]
interval: 10s
timeout: 3s
retries: 5
volumes:
pgdata:
redis-data:
networks:
frontend:
backend:
internal: trueBu konfigürasyonun önemli noktalarını açıklayalım.
API servisi expose: ["8080"] kullanıyor, ports değil. Bu çok bilinçli bir tercih — API doğrudan dışarıya açılmıyor, sadece aynı network'teki Nginx üzerinden erişilebilir. Tüm dış trafik Nginx'ten geçmek zorunda.
Backend network internal: true — yani veritabanı ve Redis internete çıkamaz. Güvenlik açısından mükemmel: bir saldırgan Redis container'ına sızsa bile dış sunucuya veri gönderemez.
Redis komutunda --maxmemory 64mb --maxmemory-policy allkeys-lru var. Bu, Redis'in 64MB'dan fazla bellek kullanmamasını ve limit dolduğunda en az kullanılan key'leri silmesini sağlar. Production'da Redis'i bellek limitsiz bırakmak tehlikelidir.
Nginx Konfigürasyonu
# nginx/conf.d/default.conf
upstream api {
server api:8080;
}
server {
listen 80;
server_name _;
location /health {
access_log off;
return 200 "OK\n";
}
location /api/ {
proxy_pass http://api/;
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_connect_timeout 30s;
proxy_read_timeout 30s;
}
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}Nginx burada iki rol oynuyor: /api/ ile başlayan istekleri backend API'ye yönlendiriyor, diğer her şeyi statik dosya olarak servis ediyor. Bu, Single Page Application (React, Vue, Angular) ile REST API'nin birlikte çalıştığı standart bir pattern.
proxy_set_header X-Real-IP $remote_addr satırı çok önemli — Nginx arkasında çalışan API, gerçek istemci IP'sini görebilir. Bu olmadan her istek Nginx'in IP'sinden geliyormuş gibi görünür.
Backend Multi-Stage Dockerfile
# backend/Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS development
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 8080 9229
CMD ["npx", "nodemon", "--inspect=0.0.0.0:9229", "src/server.js"]
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
FROM node:20-alpine AS production
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER app
EXPOSE 8080
CMD ["node", "dist/server.js"]Bu Dockerfile dört stage'e sahip. Development ortamında target: development ile build edersin — nodemon ve debug port dahil. Production'da target: production — sadece compile edilmiş kod ve production dependencies.
.env Dosyaları
# .env.example (git'e commit et — template)
DB_USER=bloguser
DB_PASS=CHANGE_ME
DB_NAME=blog_production
REDIS_PASS=CHANGE_ME
JWT_SECRET=CHANGE_ME
HTTP_PORT=80# .env (git'e commit ETME — gerçek değerler)
DB_USER=bloguser
DB_PASS=Kj8mN2pQr5vX9
DB_NAME=blog_production
REDIS_PASS=Rm3hT7nYw2bF4
JWT_SECRET=a1b2c3d4e5f6g7h8i9j0
HTTP_PORT=80Çalıştırma
cp .env.example .env
# .env dosyasını düzenle — gerçek şifreleri gir
docker compose up -d --build
docker compose ps
docker compose logs -f apiProje 2: Development Ortamı
Production konfigürasyonunun üzerine development override dosyası ekliyoruz:
# docker-compose.dev.yml
services:
nginx:
ports:
- "8080:80"
api:
build:
target: development
volumes:
- ./backend/src:/app/src
- /app/node_modules
ports:
- "3000:8080"
- "9229:9229"
environment:
NODE_ENV: development
DEBUG: "app:*"
db:
ports:
- "5432:5432"
redis:
ports:
- "6379:6379"
adminer:
image: adminer:latest
ports:
- "8082:8080"
networks:
- frontend
- backend
depends_on:
- db
redis-commander:
image: rediscommander/redis-commander:latest
environment:
REDIS_HOST: redis
REDIS_PASSWORD: ${REDIS_PASS}
ports:
- "8083:8081"
networks:
- backendDevelopment ortamını başlatmak için:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --buildŞimdi şu adreslere erişebilirsin:
http://localhost:8080→ Nginx (reverse proxy)http://localhost:3000→ API (doğrudan)http://localhost:8082→ Adminer (DB yönetim aracı)http://localhost:8083→ Redis Commanderlocalhost:5432→ PostgreSQL (doğrudan)localhost:6379→ Redis (doğrudan)
Development'ta veritabanı ve Redis portları dışarıya açık — host'tan DBeaver, Redis Insight gibi araçlarla doğrudan bağlanabilirsin. Production'da bu portlar kapalı.
API'nin source code'u bind mount ile bağlı (./backend/src:/app/src). Kodu değiştirdiğinde nodemon otomatik restart eder — hot reload çalışıyor.
Proje 3: Microservice Mimarisi
Daha büyük projelerde her servisin kendi veritabanı, kendi network'ü olur:
services:
gateway:
build: ./gateway
ports:
- "80:3000"
environment:
AUTH_URL: http://auth:4001
USERS_URL: http://users:4002
PRODUCTS_URL: http://products:4003
depends_on:
auth: { condition: service_healthy }
users: { condition: service_healthy }
products: { condition: service_healthy }
networks:
- public
- services
auth:
build: ./services/auth
expose: ["4001"]
environment:
DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@auth-db:5432/auth
depends_on:
auth-db: { condition: service_healthy }
networks: [services, auth-net]
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4001/health"]
interval: 15s
timeout: 5s
retries: 3
auth-db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: auth
volumes: [auth-pgdata:/var/lib/postgresql/data]
networks: [auth-net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
users:
build: ./services/users
expose: ["4002"]
environment:
DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@users-db:5432/users
depends_on:
users-db: { condition: service_healthy }
networks: [services, users-net]
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4002/health"]
interval: 15s
timeout: 5s
retries: 3
users-db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: users
volumes: [users-pgdata:/var/lib/postgresql/data]
networks: [users-net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
products:
build: ./services/products
expose: ["4003"]
environment:
MONGO_URL: mongodb://mongo:27017/products
depends_on:
mongo: { condition: service_healthy }
networks: [services, product-net]
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4003/health"]
interval: 15s
timeout: 5s
retries: 3
mongo:
image: mongo:7
volumes: [mongo-data:/data/db]
networks: [product-net]
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASS}
volumes: [redis-data:/data]
networks: [services]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASS}", "ping"]
interval: 10s
timeout: 3s
retries: 5
volumes:
auth-pgdata:
users-pgdata:
mongo-data:
redis-data:
networks:
public:
services: { internal: true }
auth-net: { internal: true }
users-net: { internal: true }
product-net: { internal: true }Bu mimaride her servisin veritabanı kendi izole network'ünde. Auth servisi sadece auth-db'ye erişebilir, users servisi sadece users-db'ye. Bir servis hack'lense bile diğer servislerin veritabanlarına erişemez — bu "least privilege" prensibidir.
Tek dış erişim noktası API Gateway. Dışarıdan sadece bu port açık (80). Gerisi tamamen internal.
Compose Workflow — Günlük Kullanım
Projeyi yönetirken en sık kullanacağın komut akışları:
# Sabah — projeyi başlat
docker compose up -d
# Kod değişti — rebuild et
docker compose up -d --build api
# Logları izle
docker compose logs -f api worker
# Veritabanına bağlan
docker compose exec db psql -U bloguser -d blog_production
# Test çalıştır
docker compose run --rm api npm test
# Migration çalıştır
docker compose run --rm api node scripts/migrate.js
# Akşam — projeyi durdur
docker compose down
# Temizlik (DİKKAT: volume'lar da silinir!)
docker compose down -v --rmi localProduction'da güvenli durdurma sırası:
docker compose stop # Durdur ama silme
docker compose down # Sil ama volume'ları koru
docker compose down -v # HER ŞEYİ sil (sadece bilinçli olarak!)Bu Derste Ne Öğrendik?
Docker Compose ile full-stack uygulamalar tek dosyada tanımlanır ve tek komutla çalıştırılır.
Network segmentation ile servislerin veritabanlarını izole et — güvenlik katmanı.
Override dosyaları ile development ve production ortamlarını ayır.
Healthcheck + depends_on ile servis bağımlılıklarını doğru sırala.
Microservice mimarisinde her servisin kendi veritabanı, kendi network'ü olmalı.
Development ortamında debug araçları (Adminer, Redis Commander) override ile ekle.
Sonraki derste Compose profilleri, override mekanizması ve ortam yönetimini detaylıca inceleyeceğiz.
AI Asistan
Sorularını yanıtlamaya hazır