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
exposekullansın - Her servis için ayrı
conf.ddosyası oluştur, yönetimi kolaylaştırır nginx -tile syntax kontrolü yap, sonranginx -s reloadile uygula, asla direkt restart etmedepends_onyetmez, 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.