Mert Özen Her Satırda Daha İleri
DevOps Web Server'lar ve Hosting — #04

Nginx Temelleri: Kurulum, nginx.conf Yapısı, server ve location Block'ları

Mert Özen 8 Kas 2026 11 dk 5 görüntülenme
Nginx Temelleri: Kurulum, nginx.conf Yapısı, server ve location Block'ları

Nginx'i nginx.org deposundan kurmak güncel stable dalı (1.30) verir; Ubuntu 24.04'ün paketi 1.24'te kalıyor. Master ve worker process'leri, nginx.conf context'lerini, default_server ile bilinmeyen host'u reddetmeyi, location eşleşme algoritmasını (=, en uzun prefix, ^~, regex), root ile alias farkını, proxy_pass'teki sondaki slash'i ve bozuk config ile reload ve restart'ın farklı sonuçlarını gerçek koşularla görüyoruz.

Sürüm notu: Bu yazıdaki örnekler Nginx 1.30.6 (nginx.org paketi ve resmi nginx:1.30 image'ı), Ubuntu 24.04 ve .NET 10 (ASP.NET Core runtime 10.0.14) ile Docker 29 üzerinde denenmiştir.

Önce Tahmin Et

Bookstore'un Nginx config'inde altı location var:

location = /health { return 200 "exact: = /health\n"; }
location ^~ /static/ { return 200 "prefix with ^~: /static/\n"; }
location ~* \.(png|jpg|css|js)$ { return 200 "regex: ~* extension\n"; }
location /api/ { return 200 "prefix: /api/\n"; }
location /api/books/ { return 200 "longer prefix: /api/books/\n"; }
location / { return 200 "prefix: / (fallback)\n"; }

Tahmin et: /api/books/1, /api/books/cover.jpg, /static/app.js ve /health/ (sonda slash ile) hangi block'a düşer? Config dosyada yazıldığı sıraya göre mi okunuyor, en uzun eşleşmeye göre mi, yoksa ikisi birden mi?

Kurulum: nginx.org Deposu mu, Dağıtımın Paketi mi?

Ubuntu 24.04'te apt install nginx yazdığında dağıtımın kendi paketi geliyor:

$ apt-cache policy nginx
nginx:
  Installed: (none)
  Candidate: 1.24.0-2ubuntu7.19

1.24, Nisan 2023'te çıkan stable dalı. Ubuntu bu sürüme güvenlik yamalarını kendisi taşıyor (paket adındaki ubuntu7.19 bunu anlatıyor), ama 1.25'ten bu yana gelen özellikler (bu seride kullanacağımız upstream keepalive varsayılanları, sticky, native ACME (Automatic Certificate Management Environment) modülü) bu pakette yok. Nginx'in kendi deposu her zaman güncel stable ve mainline dallarını sunuyor. nginx.org'daki "Linux packages" sayfasındaki adımlar:

sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
https://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
    | sudo tee /etc/apt/sources.list.d/nginx.list
# dağıtımın paketine karşı nginx.org paketlerini tercih et
echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
    | sudo tee /etc/apt/preferences.d/99nginx
sudo apt update
sudo apt install nginx

Key'i indirdikten sonraki gpg komutu, dosyadaki key'lerin parmak izlerini gösteriyor; nginx.org sayfasındaki listeyle karşılaştırmadan devam etme. Pinning ve kurulumdan sonra:

$ apt-cache policy nginx
nginx:
  Installed: 1.30.6-1~noble
  Candidate: 1.30.6-1~noble
  Version table:
 *** 1.30.6-1~noble 900
        900 https://nginx.org/packages/ubuntu noble/nginx arm64 Packages
        100 /var/lib/dpkg/status
     1.30.5-1~noble 900
$ nginx -v
nginx version: nginx/1.30.6

Stable mi mainline mı? Nginx'in sürüm modelinde tek sayılı dallar (1.29) mainline, çift sayılılar (1.30) stable. Stable dalına yalnız kritik düzeltmeler ve güvenlik yamaları giriyor; 1.30.0 Nisan 2026'da çıktı ve 1.30.6'ya kadar gelen değişikliklerin neredeyse hepsi CVE (Common Vulnerabilities and Exposures) düzeltmesi. Production için stable dalını seç ve güncel tut; hangi dağıtım paketini kullanırsan kullan "nginx kurulu" ile "nginx güncel" aynı şey değil.

İki paketin bir farkı da klasör yapısı. nginx.org paketi /etc/nginx/conf.d/*.conf'u include ediyor; Debian ve Ubuntu'nun paketi ise sites-available ve sites-enabled klasörlerini getiriyor:

$ dpkg -c nginx-common_1.24.0-2ubuntu7.19_all.deb | grep -E "sites-(available|enabled)"
./etc/nginx/sites-available/
./etc/nginx/sites-available/default
./etc/nginx/sites-enabled/

sites-available'daki dosya, sites-enabled'a symlink verildiğinde etkinleşiyor; site'ı kapatmak için dosyayı silmek yerine linki kaldırıyorsun. conf.d'de aynı işi dosyanın uzantısını .conf dışında bir şeye çevirerek yaparsın. İkisi de çalışır; önemli olan ekipte tek bir yöntem seçmek. Bu seride resmi image'ın ve nginx.org paketinin kullandığı conf.d düzenini kullanıyoruz.

Master ve Worker Process'ler

Nginx'i başlatınca tek bir process değil, bir aile görüyorsun:

$ ps -o user,pid,ppid,cmd -C nginx
USER         PID    PPID CMD
root        3200       1 nginx: master process nginx
nginx       3201    3200 nginx: worker process
nginx       3202    3200 nginx: worker process
nginx       3203    3200 nginx: worker process
nginx       3204    3200 nginx: worker process
nginx       3205    3200 nginx: worker process
nginx       3206    3200 nginx: worker process
nginx       3207    3200 nginx: worker process
nginx       3209    3200 nginx: worker process
nginx       3210    3200 nginx: worker process
nginx       3211    3200 nginx: worker process

Master process root olarak çalışıyor: config'i okuyor, 80 ve 443 gibi ayrıcalıklı portları açıyor, worker'ları başlatıp izliyor. Asıl iş worker'larda: her biri event loop ile binlerce bağlantıyı aynı anda yönetiyor ve nginx kullanıcısıyla, yetkisi düşürülmüş olarak çalışıyor. Worker sayısını worker_processes auto; belirliyor; bu makinede 10 CPU (Central Processing Unit) çekirdeği olduğu için 10 worker var. Önceki yazıda Unix socket'e "Permission denied" alan da bu worker'lardı: socket'e bağlanan root olan master değil, nginx kullanıcısı.

Bu ayrımın pratik sonucu reload'da görünüyor. nginx -s reload master'a SIGHUP (hangup sinyali; Nginx'te "config'i yeniden oku" demek) gönderiyor; master yeni config'i okuyor, yeni worker'ları başlatıyor ve eskilerine "elindeki bağlantıları bitir ve çık" diyor:

2026/11/08 04:00:21 [notice] 1#1: signal 1 (SIGHUP) received from 30, reconfiguring
2026/11/08 04:00:21 [notice] 1#1: reconfiguring
2026/11/08 04:00:21 [notice] 1#1: using the "epoll" event method
2026/11/08 04:00:21 [notice] 1#1: start worker processes
2026/11/08 04:00:21 [notice] 1#1: start worker process 35
2026/11/08 04:00:21 [notice] 1#1: start worker process 36
2026/11/08 04:00:21 [notice] 20#20: gracefully shutting down
2026/11/08 04:00:21 [notice] 21#21: gracefully shutting down

Açık bağlantılar kesilmiyor, port bir an bile kapanmıyor. Restart ise process'i öldürüp yeniden başlatıyor; o arada gelen istekler reddediliyor. Config değişikliğinde her zaman reload, Nginx'in binary'sini güncellediğinde restart.

nginx.conf'un Yapısı: Context'ler ve include

nginx.org paketinin getirdiği ana dosya kısa:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /run/nginx.pid;


events {
    worker_connections  1024;
}


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

    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;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

Config iç içe context'lerden oluşuyor. En dıştaki main context process'leri ilgilendiriyor (user, worker_processes, error_log). events bağlantı işleme ayarları için; worker_connections 1024, worker başına en fazla 1024 eşzamanlı bağlantı demek ve proxy'lenen her istek iki bağlantı (client ve upstream) harcıyor. http bütün HTTP (Hypertext Transfer Protocol) ayarlarının çatısı; içinde bir ya da daha çok server (virtual host), onların içinde de location block'ları var. Bir directive üst context'te tanımlanırsa alttakilere miras kalıyor, alt context'te tekrar tanımlanırsa eziliyor. Bu kuralın sinsi bir istisnası add_header ve proxy_set_header gibi dizi directive'leri: alt context'te bir tane bile tanımlarsan üsttekilerin hepsi düşüyor. Yedinci yazıda security header'larını kurarken buna geri döneceğiz.

include bir dosyayı ya da glob'u olduğu yere yapıştırıyor. Uygulama config'leri conf.d'de, ortak parçalar (proxy header'ları, TLS (Transport Layer Security) ayarları) ayrı dosyalarda durup gerektiği yerde include ediliyor. nginx -T, bütün include'ları açılmış hâliyle tek çıktı veriyor; "bu ayar hangi dosyadan geliyor" sorusunun cevabı orada.

server Block'ları ve default_server

Bir istek geldiğinde Nginx önce hangi server'ın cevap vereceğini seçiyor: önce listen ile IP (Internet Protocol) adresi ve port, sonra Host header'ı ile server_name eşleşmesi. Hiçbir server_name eşleşmezse o port için default_server işaretli block, o da yoksa dosyada ilk tanımlanan block cevap veriyor. Microsoft'un Nginx dokümanı bu yüzden bilinmeyen host'lara 444 dönen açık bir default server öneriyor:

server {
    listen 80 default_server;
    return 444;
}

444, Nginx'e özel bir kod: yanıt yazmadan bağlantıyı kapat. Bilinmeyen bir host ile gelen istek:

$ curl -s -o /dev/null -w "%{http_code}\n" -H "Host: evil.example" http://172.30.85.10/; echo "exit=$?"
000
exit=52

curl'ün 52 çıkış kodu "Empty reply from server". IP adresiyle tarama yapan bot'lar ya da DNS (Domain Name System) kaydını yanlışlıkla senin sunucuna yönlendiren bir domain, uygulamana hiç ulaşmıyor. Önceki yazıda forwarded headers için söylediğimiz "proxy bilinmeyen host'ları reddetmeli" kuralı da bu.

location Eşleşme Sırası

Baştaki tahminin cevabı:

/health                -> exact: = /health
/health/               -> prefix: / (fallback)
/static/app.js         -> prefix with ^~: /static/
/img/logo.png          -> regex: ~* extension
/api/books/1           -> longer prefix: /api/books/
/api/orders            -> prefix: /api/
/api/books/cover.jpg   -> regex: ~* extension
/about                 -> prefix: / (fallback)

Dosyadaki sıra tek başına belirleyici değil. Nginx'in ngx_http_core_module dokümanındaki algoritma şöyle:

  • = ile tanımlanmış exact location URI (Uniform Resource Identifier) ile birebir aynıysa arama orada biter. /health eşleşir, /health/ eşleşmez.
  • Değilse bütün prefix location'lar arasında en uzun eşleşen bulunur ve hatırlanır. Sıra önemsiz; /api/books/, /api/'den sonra yazılmış olsa da kazanır.
  • En uzun prefix ^~ ile işaretliyse regex'lere hiç bakılmaz, o kullanılır. /static/app.js regex'e de uyuyordu ama ^~ onu korudu.
  • Değilse regex location'lar (~ büyük/küçük harfe duyarlı, ~* duyarsız) dosyadaki sırayla denenir; ilk eşleşen kazanır.
  • Hiçbir regex eşleşmezse hatırlanan en uzun prefix kullanılır.

En çok şaşırtan satır /api/books/cover.jpg: daha uzun bir prefix location eşleştiği hâlde regex kazandı, çünkü prefix eşleşmesi yalnız "hatırlanıyor", regex'ler yine de deneniyor. API'nin (Application Programming Interface) altına bir gün .jpg ile biten bir endpoint eklersen istek Kestrel'e hiç gitmez, statik dosya kuralına düşer. Proxy'lenen prefix'leri ^~ ile işaretlemek bu sürprizi önlüyor.

04-location-selection-tr.png

root ile alias

Statik dosya sunarken iki directive var ve farkları path'in nasıl kurulduğunda:

server {
    listen 80;
    server_name files.bookstore.local;

    # root: location path'i root'un sonuna eklenir
    location /covers/ {
        root /srv/www;
    }

    # alias: location path'i alias ile değiştirilir
    location /img/ {
        alias /srv/www/static/img/;
    }
}
/covers/dune.jpg   -> dune cover
/img/logo.png      -> logo from /srv/static

/covers/dune.jpg için Nginx root'un sonuna bütün URI'yi ekleyip /srv/www/covers/dune.jpg'yi açıyor. /img/logo.png içinse eşleşen /img/ kısmını atıp yerine alias'ı koyuyor: /srv/www/static/img/logo.png. Kural basit: URL (Uniform Resource Locator) ile disk aynı yapıdaysa root, farklıysa alias. alias'ta sondaki slash'ler location ile tutarlı olmalı; location /img/ ile alias /srv/www/static/img (slash'siz) birleşince path'ler kayıyor.

proxy_pass'te Sondaki Slash

Aynı kural proxy_pass'te de var ve önceki yazıdaki /bookstore/ alt yolunun çalışmasını sağlayan şey buydu:

server {
    listen 80;
    server_name slash.bookstore.local;

    # URI kısmı yok: istek path'i olduğu gibi gider
    location /shop/ {
        proxy_pass http://172.30.85.21:8080;
    }

    # URI kısmı "/": eşleşen /store/ öneki / ile değiştirilir
    location /store/ {
        proxy_pass http://172.30.85.21:8080/;
    }
}

Kestrel'in HTTP log'u neyin ulaştığını gösteriyor:

$ for u in /shop/api/whoami /store/api/whoami; do curl -s -o /dev/null -w "$u -> %{http_code}\n" -H "Host: slash.bookstore.local" http://172.30.85.10$u; done
/shop/api/whoami -> 404
/store/api/whoami -> 200

      Request starting HTTP/1.1 GET http://172.30.85.21:8080/shop/api/whoami - - -
      Request finished HTTP/1.1 GET http://172.30.85.21:8080/shop/api/whoami - 404 0 - 67.4155ms
      Request starting HTTP/1.1 GET http://172.30.85.21:8080/api/whoami - - -
      Request finished HTTP/1.1 GET http://172.30.85.21:8080/api/whoami - 200 - application/json;+charset=utf-8 4.2388ms

proxy_pass'teki adreste host'tan sonra bir URI kısmı varsa (tek bir / bile), Nginx isteğin location'a eşleşen kısmını o URI ile değiştiriyor. URI kısmı yoksa path dokunulmadan gidiyor. Fark tek karakter, sonuç 404 ile 200 arasında. Log'daki host'un 172.30.85.21:8080 olması da ayrı bir ders: proxy_set_header Host vermediğin için Nginx upstream'in adresini Host olarak gönderdi.

Üretimde Dikkat

Her değişiklikten önce nginx -t. Bozuk bir satırla ne olduğunu üç adımda denedim. Önce test:

$ nginx -t
2026/11/08 03:58:58 [emerg] 36#36: unexpected "}" in /etc/nginx/conf.d/bookstore.conf:14
nginx: [emerg] unexpected "}" in /etc/nginx/conf.d/bookstore.conf:14
nginx: configuration file /etc/nginx/nginx.conf test failed

Sonra master'a doğrudan SIGHUP. nginx.org paketinin systemd unit'inde systemctl reload nginx tam olarak bunu yapıyor: ExecReload=/bin/sh -c "/bin/kill -s HUP $(/bin/cat /run/nginx.pid)".

$ kill -HUP $(cat /run/nginx.pid)
2026/11/08 03:59:09 [notice] 1#1: reconfiguring
2026/11/08 03:59:09 [emerg] 1#1: unexpected "}" in /etc/nginx/conf.d/bookstore.conf:14
$ curl -s -H "Host: bookstore.local" http://172.30.85.10/api/orders
prefix: /api/

Master yeni config'i okuyamayınca eskisiyle devam etti; site ayakta. Ama aynı bozuk config ile restart:

$ docker restart webhwork-nginx
$ docker ps -a --filter name=webhwork-nginx --format '{{.Status}}'
Exited (1) 1 second ago
$ curl -sS -H "Host: bookstore.local" http://172.30.85.10/api/orders
curl: (7) Failed to connect to 172.30.85.10:80 after 3109 ms: Could not connect to server

Reload bozuk config'e karşı seni korur ama sessizce: hata yalnız error log'da, eski config çalışmaya devam ediyor ve ekip değişikliğin uygulandığını sanıyor. Bir sonraki restart'ta (sunucu yeniden başladığında, paket güncellendiğinde) site düşüyor. Deploy script'inde sıra her zaman nginx -t && nginx -s reload.

Log'ların yeri ve biçimi de baştan düşünülmeli. Varsayılan main formatı client IP'sini, isteği, status'ü ve X-Forwarded-For'u yazıyor ama süre bilgisi yok. Yavaş istekleri bulmak için formata $request_time (Nginx'in toplam süresi) ve $upstream_response_time (Kestrel'in süresi) eklemek, beşinci yazıdaki 502 ve 504 ayrımında da işine yarayacak. Resmi Docker image'ında access ve error log'ları stdout ve stderr'e yönlendirilmiş symlink'ler; tail -f /var/log/nginx/access.log container içinde sonsuza kadar bekler, docker logs kullan.

Sık Düşülen Tuzaklar

Config'i test etmeden reload etmek. Yukarıda gördüğün gibi reload hatayı yutuyor, restart'ta site gidiyor.

alias'ı regex location ile karıştırmak. Nginx dokümanına göre regex location içinde alias kullanıyorsan regex capture içermeli ve alias o capture'a başvurmalı; aksi hâlde path'ler beklenmedik biçimde kuruluyor. 1.30.6 sürüm notunda alias ve rewrite birleşimiyle tetiklenen bir buffer overflow düzeltmesi (CVE-2026-15176) da var: karmaşık alias kuralları hem hata hem güvenlik riski.

Prefix location'ın regex'e karşı kazanacağını sanmak. /api/books/cover.jpg örneği: daha uzun prefix yetmiyor, ^~ gerekiyor.

proxy_pass'e alışkanlıkla sondaki slash'i eklemek ya da silmek. Birinin "temizlik" için yaptığı tek karakterlik değişiklik bütün API'yi 404'e çeviriyor. Değişikliği bir curl testi olmadan deploy etme.

Dağıtım paketinin güncel olduğunu varsaymak. Ubuntu 24.04'ün 1.24'ü güvenlik yamalarını alıyor ama bu serinin sonraki yazılarındaki varsayılanlara ve directive'lere sahip değil; Microsoft'un dokümanı da nginx.org paketlerine yönlendiriyor.

Küçük Bir Deneme

★☆☆ Eşleşme deneyindeki config'te location /api/books/'u location ^~ /api/books/ yap ve /api/books/cover.jpg'yi tekrar iste. Sonra regex location'ı ~ (büyük/küçük harfe duyarlı) yap ve /img/LOGO.PNG'yi iste. Her iki sonucu algoritmayla açıkla.

★★☆ location /img/ block'unda alias'ın sonundaki slash'i sil ve /img/logo.png'yi iste. Error log'da Nginx'in hangi dosyayı açmaya çalıştığını bul. Aynı hatayı location /img (slash'siz) ile /imgfoo isteği nasıl daha tehlikeli hâle getirir?

★★★ Log formatına $request_time, $upstream_response_time ve $upstream_addr ekleyen bir log_format bookstore tanımla ve JSON (JavaScript Object Notation) olarak yaz (escape=json). Bookstore.Api'nin /api/reports/slow?seconds=2 endpoint'ini çağırıp iki süre arasındaki farkın ne anlattığını yorumla.

Nginx'in temelleri yerinde: process modeli, context'ler, server ve location seçimi, statik dosya ve proxy_pass. Bir sonraki yazıda Microsoft'un Nginx config'ini satır satır açıyoruz: proxy_set_header seti, WebSocket ve SignalR için upgrade map'i, Server-Sent Events için buffering, 1.29.7 ile gelen upstream keepalive varsayılanları, timeout'lar ve 502 ile 504'ü ayırt etmek.