Kendi VPS’imde 13’ten fazla Docker container yönetiyorum. Bunların arasında benim hesapciyiz.com, islistesi.com gibi projelerim, bu blog sitesi ve birkaç arka plan servisi var. Her birini internete açmak için ayrı ayrı portlar kullanmak, hem güvenlik hem de yönetim açısından çok pratik değil.
Bu kadar servisi tek bir IP adresinde, 80 ve 443 gibi standart portlardan nasıl yayınlayacağım sorusu, ilk başta beni epey düşündürdü. Çözüm, yıllardır kullandığım Nginx reverse proxy mimarisini Docker ortamına taşımakta saklıydı. Bu yazıda, bu yapıyı nasıl kurduğumu ve bu süreçte edindiğim tecrübeleri adım adım anlatacağım.
Tek VPS’te Çoklu Servis Yönetimi Neden Bir Problem?
Tek bir sunucuda birden fazla web uygulaması çalıştırmak istediğinizde, temel bir problemle karşılaşırsınız: Port çakışması. HTTP için 80, HTTPS için 443 numaralı portlar standarttır ve her bir servisin bu portları doğrudan dinlemesi mümkün değildir. Sunucu üzerinde sadece tek bir işlem bu portları kullanabilir.
Bu durumu ilk yaşadığımda, her servisi farklı bir porta atayıp (örneğin, site1.com:3000, site2.com:4000) denemiştim. Ancak bu, hem kullanıcılar için kötü bir deneyimdi hem de SSL sertifikası yönetimi gibi konularda ek karmaşa yaratıyordu. Projeler büyüdükçe bu yaklaşımın sürdürülemez olduğunu anladım ve daha merkezi bir çözüm arayışına girdim.
Nginx Reverse Proxy’nin Temelleri
Nginx reverse proxy, bu karmaşık durumu çözmek için kapı bekçisi gibi davranan bir katmandır. Gelen tüm HTTP/HTTPS isteklerini önce Nginx karşılar, sonra isteğin geldiği domain adına (Host header) bakarak ilgili Docker container’ına yönlendirir. Böylece, dış dünyaya sadece Nginx’in 80 ve 443 portları açık olurken, içerideki Docker servisleri kendi özel portlarında çalışmaya devam eder.
Bu yapı sayesinde, SSL sertifikalarını tek bir noktadan (Nginx üzerinde) yönetebilirim. Aynı zamanda, Cloudflare gibi CDN servisleriyle birlikte Nginx’in önbellekleme yeteneklerini kullanarak performans artışı da sağlayabiliyorum. Yani, Nginx benim için sadece bir yönlendirici değil, aynı zamanda bir performans ve güvenlik katmanı haline geliyor.
Benim Nginx + Docker Ortamı Kurulumum
Kendi VPS’imde, Nginx’i bir Docker container’ı içinde çalıştırıyorum. Bu bana hem izolasyon sağlıyor hem de Nginx konfigürasyonunu ve bağımlılıklarını kolayca yönetmeme olanak tanıyor. Aşağıda bu kurulumun ana adımlarını ve önemli detaylarını bulacaksınız.
Docker Network Yapısı
Nginx container’ının ve diğer uygulama container’larının birbirleriyle güvenli ve isimlendirme tabanlı bir şekilde iletişim kurabilmesi için özel bir Docker network’ü oluşturmak kritik. Bu, container’ların IP adresleri yerine isimleriyle birbirlerine ulaşmasını sağlar.
docker network create nginx-proxy-net
Bu komut ile nginx-proxy-net adında bir bridge network oluşturuyorum. Tüm web uygulaması container’larımı ve Nginx container’ımı bu network’e bağlıyorum. Böylece Nginx, proxy_pass direktifinde http://my_app_container_name:3000 gibi basit bir ifadeyle uygulamama ulaşabiliyor.
Nginx Container’ını Çalıştırma
Nginx container’ını çalıştırmak için genellikle bir docker-compose.yml dosyası kullanıyorum. Bu, hem Nginx’in konfigürasyon dosyalarını host sistemimden mount etmemi hem de port eşleştirmelerini ve network bağlantısını tek bir yerden yönetmemi sağlıyor.
İşte basit bir docker-compose.yml örneği:
version: '3.8'
services:
nginx:
image: nginx:latest
container_name: nginx-proxy
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
networks:
- nginx-proxy-net
command: "/bin/sh -c 'while :; do sleep 6h & wait $!; nginx -s reload; done & nginx -g \"daemon off;\"'"
networks:
nginx-proxy-net:
external: true
Bu yapılandırmada, Nginx’in ana konfigürasyon dosyası (nginx.conf) ve her bir domain için ayrı ayrı tuttuğum sanal host konfigürasyonları (conf.d) host sistemimden okunuyor. Ayrıca, Certbot ile otomatik olarak alınan SSL sertifikaları için de letsencrypt dizinini mount ediyorum. command satırı ise Nginx’in düzenli aralıklarla konfigürasyonu yeniden yüklemesini sağlıyor, bu da yeni sertifikaların otomatik devreye alınması için önemli.
SSL Sertifikası Yönetimi (Certbot ile)
SSL sertifikaları günümüzde olmazsa olmaz. Ben genellikle Let’s Encrypt ve Certbot kullanıyorum. Certbot’u da ayrı bir Docker container olarak çalıştırıyorum ve Nginx container’ı ile aynı letsencrypt ve certbot/www dizinlerini paylaştırıyorum.
İlk sertifikayı almak için genellikle certbot certonly --webroot -w /var/www/certbot -d domain.com komutunu kullanıyorum. Sonra Nginx konfigürasyonumu bu sertifikayı kullanacak şekilde güncelliyorum.
Servislerin Nginx ile Konfigürasyonu
Nginx’in conf.d dizini içine her bir Docker servisi için ayrı bir .conf dosyası oluşturuyorum. Bu, konfigürasyonun düzenli kalmasını sağlıyor ve yeni bir servis eklemek istediğimde sadece o servise ait dosyayı eklemem yeterli oluyor.
Örnek Bir Uygulama (Astro Blog) Konfigürasyonu
Bu blog sitesi de benim VPS’imde bir Docker container’ı içinde çalışıyor. İşte blog.mustafaerbay.com.conf dosyasının basit bir hali:
server {
listen 80;
server_name blog.mustafaerbay.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name blog.mustafaerbay.com;
ssl_certificate /etc/letsencrypt/live/blog.mustafaerbay.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.mustafaerbay.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/blog.mustafaerbay.com/chain.pem;
include /etc/nginx/snippets/ssl-params.conf; # Ortak SSL ayarlarım
location / {
proxy_pass http://blog_app_container_name:3000; # Uygulamanın çalıştığı port
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_redirect off;
}
# Astro'nun statik asset'leri için Cache-Control override
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff2|woff|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
proxy_pass http://blog_app_container_name: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;
proxy_redirect off;
}
}
Bu konfigürasyonda, 80 portundan gelen istekleri otomatik olarak 443 portuna yönlendiriyorum. SSL sertifikalarımı tanımladıktan sonra, / path’ine gelen tüm istekleri blog_app_container_name isimli Docker container’ımın 3000 portuna yönlendiriyorum. proxy_set_header direktifleri, uygulamanın doğru Host, IP ve protokol bilgilerini almasını sağlıyor.
Birden Fazla Uygulama Ekleme
Yeni bir uygulama eklemek istediğimde, temelde aynı adımları izliyorum:
- Uygulamayı Docker container’ı içinde
nginx-proxy-netnetwork’üne bağlıyorum. - Uygulama için yeni bir
domain.com.confdosyası oluşturupconf.ddizinine ekliyorum. - Nginx container’ına bir
docker exec nginx-proxy nginx -s reloadkomutu göndererek konfigürasyonu yeniden yükletiyorum.
Bu yöntemle, hesapciyiz.com, islistesi.com ve spamkalkani.com gibi farklı projelerimi aynı fiziksel sunucuda, aynı Nginx reverse proxy üzerinden sorunsuzca yayınlayabiliyorum. Her birinin kendi domain adı var ve Nginx, Host header’a bakarak doğru container’a yönlendirmeyi yapıyor.
Sık Karşılaştığım Sorunlar ve Çözümleri
Bu yapıyı kurarken ve yönetirken elbette ki bazı sorunlarla karşılaştım. Saha tecrübesi dediğimiz şey biraz da bu hatalardan ders çıkarmakla ilgili.
Nginx Konfigürasyon Hataları
Nginx konfigürasyon dosyalarında bir syntax hatası yapmak çok kolay. Unutulmuş bir noktalı virgül (;) veya yanlış bir direktif, Nginx’in başlamamasına veya hatalı çalışmasına neden olabilir.
Eğer Nginx başlatılamazsa veya beklediğim gibi çalışmazsa, ilk baktığım yer docker logs nginx-proxy çıktısı oluyor. Buradaki hata mesajları genellikle sorunun kaynağını doğrudan gösterir.
Docker Network İletişim Sorunları
Bazen Nginx, proxy_pass ile hedeflediği Docker container’ına ulaşamaz. Bu genellikle aşağıdaki nedenlerden kaynaklanır:
- Yanlış Container Adı:
proxy_passdirektifinde yanlış container adı kullanmak. - Farklı Network’ler: Nginx ve uygulama container’ının aynı Docker network’ünde olmaması. Kendi VPS’imde bir keresinde uygulama container’ımı farklı bir network’e bağlamışım, Nginx tabii ki “upstream timed out” hatası veriyordu.
- Uygulama Portu: Uygulamanın Docker içinde dinlediği portun yanlış belirtilmesi (örneğin, 3000 yerine 8080 yazmak).
Bu tür durumlarda, docker inspect <container_name> komutu ile container’ın hangi network’lere bağlı olduğunu ve IP adresini kontrol ederim. Ayrıca, Nginx container’ının içinden curl http://<app_container_name>:<port> komutunu kullanarak uygulama container’ına erişip erişemediğimi test ederim.
Cache ve Header Yönetimi (Cloudflare ile)
Benim Astro sitelerimde bazen Cloudflare’ın cache’i ile ilgili ince ayarlar yapmam gerekiyor. Astro varsayılan olarak Cache-Control: public, max-age=0, must-revalidate gibi başlıklar dönebiliyor, bu da Cloudflare’ın her zaman origin’e gitmesine neden oluyor.
Bu durumu aşmak için Nginx konfigürasyonunda statik asset’ler için özel Cache-Control başlıkları ekliyorum. Yukarıdaki Astro blog örneğindeki gibi, .js, .css, .png gibi dosya uzantılarına sahip URL’ler için Nginx’in expires 1y; ve add_header Cache-Control "public, max-age=31536000, immutable"; başlıklarını eklemesini sağlıyorum. Bu sayede, Cloudflare bu asset’leri uzun süre önbelleğinde tutabiliyor ve sunucuya daha az istek geliyor.
Performans ve Güvenlik İpuçları
Nginx’i sadece bir reverse proxy olarak kullanmakla kalmıyorum, aynı zamanda sunucumun performansını ve güvenliğini artırmak için de bazı ayarlar yapıyorum.
Nginx Worker Ayarları
Nginx’in ne kadar worker_processes kullanacağı, sunucunuzun CPU çekirdek sayısına göre ayarlanmalı. Ben genellikle worker_processes auto; kullanıyorum, bu Nginx’in CPU çekirdek sayısını otomatik olarak algılamasını sağlıyor. worker_connections ise her bir worker process’in kaç eşzamanlı bağlantıyı işleyebileceğini belirler. Kendi VPS’imde, bu değeri genellikle 1024 veya 2048 olarak tutuyorum.
Güvenlik Başlıkları
Web uygulamalarımın daha güvenli olması için Nginx üzerinden bazı HTTP güvenlik başlıklarını ekliyorum. Bu başlıklar, tarayıcıların bazı güvenlik açıklarını istismar etmesini engellemeye yardımcı olur.
İşte snippets/security-headers.conf gibi bir dosyada tuttuğum bazı örnekler:
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 "no-referrer-when-downgrade" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
Bu snippets dosyasını her bir server bloğumda include ederek tüm sitelerime bu başlıkları otomatik olarak ekliyorum.
Rate Limiting
Spam botları ve kötü niyetli istekler benim için her zaman bir sorun olmuştur, özellikle spamkalkani.com gibi API tabanlı servislerimde. Nginx’in rate limiting özellikleri, belirli IP adreslerinden gelen istekleri sınırlamak için harika bir araç.
# nginx.conf içinde http bloğuna ekle
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/s;
Yukarıdaki konfigürasyon, her IP adresi için saniyede 5 isteğe izin veren bir limit tanımlar. Daha sonra bunu bir location bloğuna uygulayabilirim:
location /api/ {
limit_req zone=mylimit burst=10 nodelay;
proxy_pass http://api_app_container_name:3001;
# ... diğer proxy ayarları
}
Bu, spamkalkani.com’un API’ine gelen bot isteklerini kontrol altında tutmama yardımcı oluyor. Geçen ay bir bot saldırısı yüzünden CPU’m %90’lara fırlamıştı, bu ayarı devreye aldıktan sonra rahat bir nefes aldım.
Sonuç
Tek bir VPS üzerinde Nginx reverse proxy ve Docker kullanarak birden fazla web servisi yönetmek, benim için hem maliyet etkin hem de oldukça düzenli bir çözüm oldu. Bu yapı sayesinde, sunucu kaynaklarımı verimli kullanabiliyor, servislerimi kolayca ölçeklendirebiliyor ve merkezi bir noktadan güvenlik ile performansı yönetebiliyorum.
Bu düzeni kurarken çok başım ağrıdı, özellikle ilk başta container’lar arası network sorunları yüzünden. Ama şimdi taş gibi çalışıyor ve yeni bir proje eklemek sadece birkaç dakika mı alıyor. Sen de böyle tek bir VPS’te birden fazla servisi yönetirken kullandığın farklı bir yaklaşım var mı? Ya da bu rehberdeki adımları uygularken takıldığın bir yer oldu mu? Yorumlarda konuşabiliriz. Belki bir sonraki yazımda bu yapının üzerine nasıl bir monitoring eklediğimi anlatırım.