Reverse Proxy Yapılandırması: Nginx ve Docker Compose Entegrasyonu

Docker Compose ile birden fazla servis ayağa kaldırdığında, her servisi ayrı bir porta bağlamak kısa vadede işe yarasa da zamanla hem güvenlik açığı hem de yönetim cehennemi haline gelir. Nginx reverse proxy, bu kaosa düzen getiren ve production ortamlarında fiilen standart haline gelen çözümdür. Bu yazıda, gerçek bir senaryo üzerinden Nginx ve Docker Compose entegrasyonunu sıfırdan kuracağız.

Reverse Proxy Neden Gereklidir?

Diyelim ki bir sunucuda şu servisleri çalıştırıyorsun: bir React frontend (port 3000), bir Node.js API (port 5000), bir Grafana dashboard (port 3001) ve bir pgAdmin (port 8080). Bu servislere dışarıdan erişmek için her birinin portunu firewall’dan açman gerekiyor. Her port ayrı bir saldırı yüzeyi demek. Üstelik kullanıcıların siteadi.com:3000, siteadi.com:5000 gibi portları aklında tutması beklenir ki bu da gerçekçi değil.

Nginx reverse proxy bu tabloda şu işleri yapar:

  • Dışarıya sadece 80 ve 443 portlarını açarsın, diğer portlar iç ağda kalır
  • api.siteadi.com -> Node.js servisi, grafana.siteadi.com -> Grafana gibi subdomain yönlendirmesi yapabilirsin
  • SSL termination tek noktada halledilir, her servis ayrı sertifika yönetmez
  • Rate limiting, gzip sıkıştırma, header manipülasyonu gibi cross-cutting concern’ler merkezi olarak uygulanır
  • Upstream servisler hata verdiğinde custom error page gösterilebilir

Proje Yapısı

Önce dosya yapımızı belirleyelim. Gerçek hayatta bu yapı hem okunabilirliği artırır hem de CI/CD pipeline’larına uyumu kolaylaştırır.

mkdir -p ~/projects/compose-nginx-demo
cd ~/projects/compose-nginx-demo

mkdir -p nginx/conf.d
mkdir -p nginx/ssl
mkdir -p app/frontend
mkdir -p app/backend

# Temel dosyaları oluştur
touch docker-compose.yml
touch nginx/nginx.conf
touch nginx/conf.d/default.conf
touch app/frontend/Dockerfile
touch app/backend/Dockerfile

Proje dizin ağacı şöyle görünmeli:

tree ~/projects/compose-nginx-demo
# compose-nginx-demo/
# ├── docker-compose.yml
# ├── nginx/
# │   ├── nginx.conf
# │   ├── conf.d/
# │   │   └── default.conf
# │   └── ssl/
# ├── app/
# │   ├── frontend/
# │   └── backend/

Docker Compose Dosyasını Oluşturma

Senaryo olarak şunu kullanalım: Bir şirketin iç dashboard uygulaması. Frontend olarak basit bir Nginx static site, backend olarak Python Flask API, ve bunların yanında pgAdmin ve Grafana çalışıyor.

cat > docker-compose.yml << 'EOF'
version: '3.8'

networks:
  frontend_net:
    driver: bridge
  backend_net:
    driver: bridge

volumes:
  postgres_data:
  grafana_data:

services:
  nginx-proxy:
    image: nginx:1.25-alpine
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    networks:
      - frontend_net
    depends_on:
      - frontend
      - backend
      - grafana
      - pgadmin
    restart: unless-stopped

  frontend:
    image: nginx:1.25-alpine
    container_name: frontend-app
    volumes:
      - ./app/frontend/dist:/usr/share/nginx/html:ro
    networks:
      - frontend_net
    expose:
      - "80"
    restart: unless-stopped

  backend:
    build:
      context: ./app/backend
      dockerfile: Dockerfile
    container_name: backend-api
    environment:
      - DATABASE_URL=postgresql://appuser:secretpassword@postgres:5432/appdb
      - FLASK_ENV=production
    networks:
      - frontend_net
      - backend_net
    expose:
      - "5000"
    depends_on:
      - postgres
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    container_name: postgres-db
    environment:
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=secretpassword
      - POSTGRES_DB=appdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend_net
    restart: unless-stopped

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    environment:
      - [email protected]
      - PGADMIN_DEFAULT_PASSWORD=adminpassword
      - PGADMIN_CONFIG_SERVER_MODE=True
    networks:
      - frontend_net
      - backend_net
    expose:
      - "80"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=grafanapassword
      - GF_SERVER_ROOT_URL=https://grafana.company.local
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - frontend_net
      - backend_net
    expose:
      - "3000"
    restart: unless-stopped
EOF

Buradaki kritik detaya dikkat et: Nginx proxy hariç hiçbir servis ports directive’i kullanmıyor, sadece expose kullanıyor. expose direktifi sadece Docker network içinde portu açar, host’a bind etmez. Bu sayede postgres, grafana, pgadmin gibi servisler internetten doğrudan erişilemez hale gelir.

Ana Nginx Konfigürasyonu

nginx.conf global ayarları içerir. Bunu production için optimize edilmiş haliyle yazalım:

cat > nginx/nginx.conf << 'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
    multi_accept on;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Logging formatı
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    # Performans ayarları
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;

    # Gzip sıkıştırma
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml
               application/json application/javascript
               application/xml+rss application/atom+xml
               image/svg+xml;

    # Proxy buffer ayarları
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;

    # Upstream timeout'ları
    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;

    # Virtual host konfigürasyonları
    include /etc/nginx/conf.d/*.conf;
}
EOF

Virtual Host Konfigürasyonları

Her servis için ayrı virtual host tanımlayacağız. Gerçek hayatta bunları ayrı dosyalara bölmek, conf.d dizinini düzenli tutar ve tek bir servisi devre dışı bırakmayı kolaylaştırır.

cat > nginx/conf.d/default.conf << 'EOF'
# Ana uygulama - Frontend
server {
    listen 80;
    server_name app.company.local;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://frontend:80;
        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;

        # Cache static assets
        proxy_cache_valid 200 1d;
    }
}

# Backend API
server {
    listen 80;
    server_name api.company.local;

    # Rate limiting - API koruması
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;

    location / {
        limit_req zone=api_limit burst=10 nodelay;

        proxy_pass http://backend:5000;
        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;

        # API için CORS header (gerekirse)
        add_header Access-Control-Allow-Origin "https://app.company.local" always;
    }

    # Health check endpoint'i rate limiting'den muaf tut
    location /health {
        proxy_pass http://backend:5000/health;
        access_log off;
    }
}

# pgAdmin
server {
    listen 80;
    server_name pgadmin.company.local;

    # Sadece iç ağdan erişime izin ver
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    allow 172.16.0.0/12;
    deny all;

    location / {
        proxy_pass http://pgadmin:80;
        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_set_header X-Script-Name /pgadmin;
    }
}

# Grafana
server {
    listen 80;
    server_name grafana.company.local;

    # Grafana websocket desteği
    location / {
        proxy_pass http://grafana:3000;
        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;

        # WebSocket upgrade
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
EOF

SSL/TLS Entegrasyonu

Production’da HTTP yeterli değil. Self-signed sertifika ile başlayalım, ardından Let’s Encrypt’i göstereceğim. Test ortamları ve iç ağ uygulamaları için self-signed gayet işlevsel.

# Self-signed sertifika oluştur
openssl req -x509 -nodes -days 365 -newkey rsa:2048 
    -keyout nginx/ssl/company.local.key 
    -out nginx/ssl/company.local.crt 
    -subj "/C=TR/ST=Istanbul/L=Istanbul/O=Company/CN=*.company.local" 
    -addext "subjectAltName=DNS:*.company.local,DNS:company.local"

# Dosya izinlerini ayarla
chmod 600 nginx/ssl/company.local.key
chmod 644 nginx/ssl/company.local.crt

SSL için Nginx konfigürasyonunu güncelleyelim. Mevcut default.conf dosyasını HTTP’den HTTPS’e yönlendirecek şekilde düzenleriz:

cat > nginx/conf.d/ssl.conf << 'EOF'
# HTTP -> HTTPS yönlendirme
server {
    listen 80;
    server_name *.company.local company.local;
    return 301 https://$host$request_uri;
}

# SSL konfigürasyonu - Frontend
server {
    listen 443 ssl http2;
    server_name app.company.local;

    ssl_certificate /etc/nginx/ssl/company.local.crt;
    ssl_certificate_key /etc/nginx/ssl/company.local.key;

    # Modern SSL ayarları
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        proxy_pass http://frontend:80;
        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 https;
    }
}
EOF

Ortamı Ayağa Kaldırma ve Test Etme

Her şey hazır olduğuna göre servisleri başlatalım ve doğrulayalım:

# Compose'u başlat
cd ~/projects/compose-nginx-demo
docker compose up -d

# Servislerin durumunu kontrol et
docker compose ps

# Nginx konfigürasyonunu test et
docker compose exec nginx-proxy nginx -t

# Nginx loglarını canlı izle
docker compose logs -f nginx-proxy

# Backend API'ye proxy üzerinden erişimi test et
curl -H "Host: api.company.local" http://localhost/health

# /etc/hosts'a test için kayıt ekle (local geliştirme)
echo "127.0.0.1 app.company.local api.company.local grafana.company.local pgadmin.company.local" | sudo tee -a /etc/hosts

# Artık domain adıyla test edebilirsin
curl http://api.company.local/health

Nginx konfigürasyonunda değişiklik yaptığında container’ı yeniden başlatmaya gerek yok, reload yeterli:

# Nginx'i reload et (sıfır downtime)
docker compose exec nginx-proxy nginx -s reload

# Veya sadece nginx servisini yeniden başlat
docker compose restart nginx-proxy

# Tüm upstream servis loglarını bir arada izle
docker compose logs -f --tail=50

Healthcheck ve Otomatik Kurtarma

Production’da servisler birbirine bağımlı olduğu için healthcheck mekanizması kritik önem taşır. Docker Compose bu konuda güçlü bir destek sunuyor:

# docker-compose.yml'e healthcheck ekle (backend servisi örneği)
# Mevcut backend servis tanımını şu şekilde güncelle:

cat >> docker-compose.yml << 'EOF'
# NOT: Bu satırları docker-compose.yml içindeki
# backend servisi altına eklemen gerekiyor

#    healthcheck:
#      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
#      interval: 30s
#      timeout: 10s
#      retries: 3
#      start_period: 40s
EOF

# Container health durumunu kontrol et
docker inspect --format='{{.State.Health.Status}}' backend-api

# Tüm servislerin health durumunu özetle
docker compose ps --format "table {{.Name}}t{{.Status}}t{{.Ports}}"

Dinamik Upstream ile Ölçeklendirme

Compose ile backend servisini scale ettiğinde Nginx’in bunu otomatik algılaması için upstream bloğunu kullanmak gerekir:

cat > nginx/conf.d/upstream.conf << 'EOF'
# Upstream tanımı - Docker DNS ile load balancing
upstream backend_pool {
    # Docker Compose scale ile birden fazla instance çalışıyorsa
    # Nginx, DNS round-robin ile dağıtım yapar
    server backend:5000;

    # Keepalive bağlantı havuzu
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.company.local;

    ssl_certificate /etc/nginx/ssl/company.local.crt;
    ssl_certificate_key /etc/nginx/ssl/company.local.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://backend_pool;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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 https;

        # Upstream hata durumunda retry
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
        proxy_next_upstream_tries 3;
    }
}
EOF

# Backend'i 3 instance ile ölçeklendir
docker compose up -d --scale backend=3

# Nginx'i reload et
docker compose exec nginx-proxy nginx -s reload

# Load balancing'i test et - her istek farklı container'a gitmeli
for i in {1..6}; do
    curl -sk https://api.company.local/health | python3 -m json.tool
done

Yaygın Sorunlar ve Çözümleri

Gerçek hayatta en sık karşılaştığım sorunları ve çözümlerini paylaşayım:

502 Bad Gateway: Upstream servis henüz hazır değilken Nginx istek almaya başlar. depends_on direktifinin sadece container başlangıcını beklediğini unutma, servisin hazır olduğunu değil. Çözüm ya healthcheck eklemek ya da backend’e retry mekanizması koymak.

WebSocket bağlantıları kopuyor: Grafana, Socket.io kullanan uygulamalar için proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade ve proxy_set_header Connection "upgrade" satırlarının eklendiğinden emin ol.

Büyük dosya yüklemelerinde 413 hatası: Nginx’in varsayılan client_max_body_size değeri 1MB. Dosya yükleme yapan servisler için artır:

# İlgili location bloğuna ekle
client_max_body_size 100M;
proxy_request_buffering off;

pgAdmin’de boş sayfa sorunu: pgAdmin, X-Script-Name header’ına ihtiyaç duyar. Ayrıca PGADMIN_CONFIG_X_FRAME_OPTIONS environment variable’ını SAMEORIGIN olarak ayarlamayı unutma.

Container hostname çözümlenemiyor: Docker Compose içindeki servisler birbirine servis adıyla ulaşır. Nginx proxy_pass http://backend:5000 yazarken backend servis adının docker-compose.yml‘deki service name ile birebir eşleştiğinden emin ol. Ayrıca her iki servisin aynı network’te olması şart.

Konfigürasyon Güncelleme Workflow’u

Production ortamında konfigürasyon değişikliklerini nasıl yönetmeli?

# 1. Değişikliği yap
vim nginx/conf.d/default.conf

# 2. Syntax kontrolü yap (container yeniden başlatmadan)
docker compose exec nginx-proxy nginx -t

# Hata varsa şu çıktıyı alırsın:
# nginx: [emerg] unknown directive "servre_name" ...
# nginx: configuration file /etc/nginx/nginx.conf test failed

# 3. Test başarılıysa reload et
docker compose exec nginx-proxy nginx -s reload

# 4. Logları izle, hata var mı kontrol et
docker compose logs nginx-proxy --tail=20

# Acil durumda son çalışan konfigürasyona dön
git checkout nginx/conf.d/default.conf
docker compose exec nginx-proxy nginx -s reload

Sonuç

Nginx ve Docker Compose entegrasyonu, multi-servis uygulamaların production’a taşınmasında olmazsa olmaz bir katman. Bu yazıda sıfırdan kurulum, SSL termination, rate limiting, WebSocket desteği ve ölçeklendirme konularını gerçek senaryo üzerinden işledik.

Özetleyecek olursak en önemli noktalar şunlar:

  • Sadece Nginx proxy’nin portlarını host’a bağla, diğer servisler sadece expose kullansın
  • Her servis için ayrı conf.d dosyası oluştur, yönetimi kolaylaştırır
  • nginx -t ile syntax kontrolü yap, sonra nginx -s reload ile uygula, asla direkt restart etme
  • depends_on yetmez, healthcheck ekle
  • Upstream servis adları Docker Compose service name ile birebir eşleşmeli
  • WebSocket kullanan servisler için HTTP/1.1 ve Upgrade header’larını unutma

Bu yapıyı yerine oturursa, yeni bir servis eklemek sadece docker-compose.yml‘e servis tanımı ve conf.d/ altına bir virtual host dosyası eklemekten ibaret hale gelir. İşte reverse proxy mimarisinin gerçek değeri de bu tekrar edilebilirlikte yatıyor.

Yorum yapın