Yerelde python manage.py runserver yazdığınızda her şey çalışıyor: sayfalar açılıyor, admin paneli duruyor, CSS'ler yerli yerinde. Sonra projeyi sunucuya kopyalıyorsunuz, aynı komutu çalıştırıyorsunuz, tarayıcıya sunucunun IP'sini yazıyorsunuz ve karşınıza ya bağlantı hatası ya da tasarımı tamamen kaybolmuş, çıplak HTML'den ibaret bir sayfa çıkıyor. SSH oturumunu kapattığınız anda site de kapanıyor.
Bunun sebebi bir yapılandırma hatası değil, bir yanlış anlamadır: runserver bir geliştirme aracıdır. Tek başına eşzamanlı istek işleyemez, statik dosyaları DEBUG=False iken servis etmez, HTTPS bilmez ve süreç yönetimi yapmaz. Üretimde Django'nun önünde üç ayrı bileşen çalışır ve her biri farklı bir işi üstlenir: Gunicorn Python kodunu çalıştırır, systemd Gunicorn'u ayakta tutar, Nginx dış dünyayla konuşup statik dosyaları kendi verir.
Bu rehberde Ubuntu 24.04 üzerinde temiz bir sunucuya baştan sona bu zinciri kuracağız. Sanal ortamdan başlayıp Gunicorn'u kalıcı bir sistem servisine dönüştüreceğiz, Nginx'i reverse proxy olarak yapılandıracağız, collectstatic ile statik ve medya dosyalarını Nginx'e devredeceğiz, DEBUG=False ile birlikte gelen üretim ayarlarını tek tek geçeceğiz ve sonunda SSL'i bağlayacağız. En sonda da deploy sonrası en sık gördüğüm 400, 500 ve 502 hatalarının kısa teşhis listesi var.
Django'yu Yayına Almak İçin Neden Üç Ayrı Bileşen Gerekiyor?#
İsteğin izlediği yolu bir kez netleştirmek, ilerideki her hata mesajını anlamlı kılar:
Tarayıcı → :443 Nginx → unix socket → Gunicorn → Django (WSGI) → Veritabanı
↓
/static/, /media/ (Nginx doğrudan diskten verir)
| Bileşen | Sorumluluğu | Olmazsa ne olur? |
|---|---|---|
| Gunicorn | WSGI sunucusu; Django kodunu çoklu işçi (worker) süreçlerinde çalıştırır | Aynı anda tek istek işlenir, site kilitlenir |
| systemd | Süreci başlatır, çökerse yeniden ayağa kaldırır, sunucu açılışında otomatik çalıştırır | SSH kapanınca uygulama ölür |
| Nginx | TLS sonlandırma, statik dosya servisi, gzip, rate limit, çoklu site barındırma | 443 dinlenmez, statikler Python'dan geçer, performans çöker |
Nginx'i araya koymanın en somut kazancı statik dosyalardır. Bir sayfa açılışında 40 CSS/JS/görsel isteği gidiyorsa, bunların Python işçilerini meşgul etmesi anlamsızdır; Nginx bu dosyaları diskten okuyup mikrosaniyelerde döndürürken Gunicorn işçileri sadece gerçek uygulama isteklerine bakar. Reverse proxy mantığının genel çalışma prensibini daha önce Nginx reverse proxy yapılandırması yazısında ele almıştık; burada doğrudan Django'ya özel kısmına gireceğiz.
Sunucuyu Hazırlama ve Sanal Ortam Kurulumu#
Temiz bir Ubuntu 24.04 sunucusunda gerekli paketleri kuralım:
sudo apt update
sudo apt install -y python3-venv python3-pip nginx git
Uygulamayı root ile çalıştırmak, uygulamada bulunacak herhangi bir açığın doğrudan sunucunun tamamına dönüşmesi demektir. Bu yüzden projeye özel, kabuk girişi olmayan bir kullanıcı açın:
sudo adduser --system --group --home /var/www/proje django
sudo mkdir -p /var/www/proje
sudo chown -R django:django /var/www/proje
Projeyi bu dizine alıp sanal ortamı kurun:
cd /var/www/proje
sudo -u django git clone https://github.com/kullanici/proje.git .
sudo -u django python3 -m venv venv
sudo -u django venv/bin/pip install --upgrade pip
sudo -u django venv/bin/pip install -r requirements.txt
sudo -u django venv/bin/pip install gunicorn
requirements.txt içinde Gunicorn yoksa yukarıdaki gibi ayrıca kurun ve mutlaka dosyaya da ekleyin — bir sonraki deploy'da unutulan tek paket bu olur. Sanal ortamları ilk kez kuruyorsanız Python venv ve pip yönetimi yazısı temel komutları ayrıntılı anlatıyor.
Veritabanı sürücüsü de bu aşamada kurulur: PostgreSQL için psycopg[binary], MySQL/MariaDB için mysqlclient (öncesinde sudo apt install -y libmysqlclient-dev pkg-config gerekir).
Gizli Ayarları Kod Deposundan Çıkarın#
SECRET_KEY, veritabanı şifresi ve API anahtarları settings.py içinde sabit yazılıysa bunlar Git geçmişine de yazılmış demektir. Ortam değişkenine taşıyın:
# settings.py
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"
Değerleri servis dosyasının okuyacağı bir dosyada tutun:
sudo install -o root -g django -m 640 /dev/null /etc/proje.env
sudo nano /etc/proje.env
DJANGO_SECRET_KEY=buraya-uzun-rastgele-bir-anahtar
DJANGO_DEBUG=0
DATABASE_URL=postgres://kullanici:[email protected]:5432/projedb
640 izni ve django grubu, dosyayı yalnızca root'un yazabilmesini ama servis kullanıcısının okuyabilmesini sağlar.
Gunicorn'u Elle Test Etme#
Servis dosyası yazmadan önce Gunicorn'un gerçekten çalıştığını görün. Bu adımı atlamak, ilerideki hataların Gunicorn'dan mı systemd'den mi geldiğini ayırt etmeyi imkânsızlaştırır:
cd /var/www/proje
sudo -u django venv/bin/gunicorn --bind 127.0.0.1:8000 proje.wsgi:application
proje.wsgi kısmı, settings.py ile aynı klasördeki wsgi.py dosyasının modül yoludur — proje klasörünüzün adı neyse odur. Başka bir terminalden kontrol edin:
curl -I http://127.0.0.1:8000/
HTTP/1.1 200 OK veya 302 Found görüyorsanız uygulama ayakta. ModuleNotFoundError alıyorsanız yanlış dizindesiniz ya da modül yolunu yanlış yazdınız. Testi Ctrl+C ile sonlandırın.
İşçi Sayısını Doğru Seçmek#
Gunicorn varsayılan olarak tek işçiyle çalışır ve bu, üretim için neredeyse her zaman azdır. Yaygın başlangıç formülü (2 × çekirdek) + 1'dir:
nproc # çekirdek sayısını verir
2 çekirdekli bir sunucuda 5 işçi makul bir başlangıçtır. Her işçi Django uygulamasının tam bir kopyasını bellekte tutar; tipik bir projede işçi başına 80-150 MB hesaplayın. 2 GB RAM'li bir makinede 17 işçi açmak, siteyi hızlandırmaz, OOM killer'ın Gunicorn'u öldürmesine yol açar. RAM'iniz darsa işçi sayısını düşürüp --threads 2 eklemek genelde daha verimlidir.
Gunicorn'u systemd Servisi Olarak Kalıcı Çalıştırma#
Asıl kalıcılık burada başlıyor. Unix socket kullanacağız: TCP portuna göre biraz daha hızlıdır ve socket'e dosya izniyle erişim kısıtlanabildiği için dışarıdan doğrudan erişilemez.
sudo nano /etc/systemd/system/proje.service
[Unit]
Description=Gunicorn - proje Django uygulamasi
After=network.target
[Service]
Type=notify
User=django
Group=www-data
WorkingDirectory=/var/www/proje
EnvironmentFile=/etc/proje.env
RuntimeDirectory=proje
RuntimeDirectoryMode=0750
ExecStart=/var/www/proje/venv/bin/gunicorn \
--workers 3 \
--bind unix:/run/proje/gunicorn.sock \
--umask 007 \
--access-logfile - \
--error-logfile - \
proje.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
KillMode=mixed
[Install]
WantedBy=multi-user.target
Bu dosyada dikkat edilmesi gereken satırlar şunlar:
Group=www-datave--umask 007: Socket dosyasıdjango:www-datasahipliğinde ve grup yazma izinli oluşur. Nginxwww-dataolarak çalıştığı için socket'e yazabilir. Bu ikisinden biri eksikse Nginx'tenpermission deniedalırsınız — 502 hatalarının bir numaralı sebebi budur.RuntimeDirectory=proje:/run/projedizinini servis başlarken oluşturur, servis dururken siler. Ellemkdiryapmayın;/runbir tmpfs'tir ve her yeniden başlatmada boşalır, dizin kaybolur ve servis açılışta çalışmaz.--access-logfile -ve--error-logfile -: Logları stdout'a basar, systemd de journald'a yazar. Böylece tüm çıktıyıjournalctlile okursunuz; ayrı log dosyası ve ayrı logrotate kuralı gerekmez.Type=notify: Gunicorn hazır olduğunu systemd'ye bildirir,systemctl startgerçekten hazır olunca döner. Eski Gunicorn sürümlerinde sorun çıkarsaType=simpleyapın.Restart=on-failure: Uygulama çökerse 5 saniye sonra otomatik kalkar.
Servisi devreye alın:
sudo systemctl daemon-reload
sudo systemctl enable --now proje
sudo systemctl status proje
Durum active (running) değilse doğrudan loga bakın:
sudo journalctl -u proje -n 50 --no-pager
sudo journalctl -u proje -f # canlı takip
Servis birimlerinin Restart, After, WantedBy gibi direktiflerini derinlemesine incelemek isterseniz systemd servis yönetimi yazısı bu alanların tamamını açıklıyor. Log tarafında ise journalctl ile log yönetimi rehberi filtreleme kalıplarını içeriyor.
collectstatic ile Statik ve Medya Dosyalarını Nginx'e Devretme#
DEBUG=True iken Django statik dosyaları kendi servis eder. DEBUG=False yaptığınız anda bu davranış kapanır — sitenin CSS'siz görünmesinin sebebi tam olarak budur ve bir hata değil, kasıtlı bir tasarımdır. Üretimde bu işi Nginx yapar.
Önce toplama hedefini tanımlayın:
# settings.py
import os
STATIC_URL = "/static/"
STATIC_ROOT = "/var/www/proje/staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = "/var/www/proje/media"
STATIC_ROOT ile STATICFILES_DIRS sıkça karıştırılır ve aynı klasörü ikisine birden vermek collectstatic sırasında hata verir:
| Ayar | Ne işe yarar | Kim yazar |
|---|---|---|
STATICFILES_DIRS | Sizin yazdığınız kaynak statik klasörleri | Geliştirici |
STATIC_ROOT | Tüm uygulamaların statiklerinin toplandığı çıktı klasörü | collectstatic |
MEDIA_ROOT | Kullanıcıların yüklediği dosyalar | Uygulama, çalışma anında |
Toplama komutunu çalıştırın:
cd /var/www/proje
sudo -u django venv/bin/python manage.py collectstatic --noinput
sudo -u django venv/bin/python manage.py migrate --noinput
--noinput etkileşimli onay sormaz; bu, komutu deploy betiğine koyabilmenin şartıdır. collectstatic her deploy'da tekrar çalıştırılmalıdır — CSS'i güncelleyip bu adımı unutmak, "değişikliğim sunucuda görünmüyor" şikâyetinin en yaygın sebebidir.
Nginx'i Reverse Proxy Olarak Yapılandırma#
Site yapılandırmasını oluşturun:
sudo nano /etc/nginx/sites-available/proje
server {
listen 80;
server_name ornek.com www.ornek.com;
client_max_body_size 25M;
location /static/ {
alias /var/www/proje/staticfiles/;
access_log off;
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
alias /var/www/proje/media/;
access_log off;
expires 7d;
}
location / {
proxy_pass http://unix:/run/proje/gunicorn.sock;
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;
proxy_read_timeout 60s;
}
}
Üç ayrıntı kritik:
aliassonundaki eğik çizgi.location /static/ilealias /var/www/proje/staticfiles/;çiftinde ikisi de/ile bitmelidir. Birini unutursanız Nginx yolları yanlış birleştirir ve tüm statikler 404 döner.X-Forwarded-Protobaşlığı. SSL'i Nginx sonlandırdığı için Gunicorn'a gelen istek düz HTTP'dir; Django bu başlık olmadan isteğin HTTPS olduğunu bilemez verequest.is_secure()yanlış sonuç verir.client_max_body_size. Varsayılan 1 MB'dır. Kullanıcı dosya yükleyen bir projede bu satır yoksa 1 MB üstü her yükleme Django'ya hiç ulaşmadan413ile reddedilir.
Siteyi etkinleştirip yapılandırmayı sınayın:
sudo ln -s /etc/nginx/sites-available/proje /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
nginx -t çıktısı syntax is ok ve test is successful demeden asla reload etmeyin; hatalı yapılandırmayla yapılan reload çalışan siteyi de düşürebilir.
Nginx'i ilk kez kuruyorsanız Nginx kurulumu ve temel yapılandırma yazısı dizin yapısını ve sites-available mantığını ayrıntılandırıyor.
DEBUG=False ile Birlikte Değişmesi Gereken Üretim Ayarları#
DEBUG=False tek başına yeterli değildir; beraberinde bir dizi ayarı da devreye almanız gerekir.
# settings.py
DEBUG = False
ALLOWED_HOSTS = ["ornek.com", "www.ornek.com"]
CSRF_TRUSTED_ORIGINS = ["https://ornek.com", "https://www.ornek.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
ALLOWED_HOSTS boş bir listeyken DEBUG=False ise Django gelen her isteği reddeder — sonucu 400 Bad Request sayfasıdır. Alan adını https:// öneki olmadan, sadece host olarak yazın; CSRF_TRUSTED_ORIGINS ise tam tersine Django 4.0'dan itibaren şema zorunlu ister. Bu ikisinin biçim farkı, formların çalışmamasının en sık nedenidir.
SECURE_SSL_REDIRECT ile SECURE_PROXY_SSL_HEADER birlikte kullanılmalıdır. Proxy başlığını tanımlamadan yönlendirmeyi açarsanız Django her isteği HTTP sanar, HTTPS'e yönlendirir, Nginx tekrar Gunicorn'a düz HTTP olarak iletir ve tarayıcı sonsuz yönlendirme döngüsüne girer (ERR_TOO_MANY_REDIRECTS).
Django'nun kendi denetleyicisi bu ayarların eksiklerini listeler:
sudo -u django venv/bin/python manage.py check --deploy
Çıktıdaki her W0xx uyarısını okuyun; çoğu tek satırlık bir ayarla kapanır.
SSL Sertifikasını Bağlama#
HTTP üzerinden site açılıyorsa sertifika almaya hazırsınız. Alan adının A kaydı sunucunun IP'sine bakıyor olmalı, aksi hâlde doğrulama başarısız olur.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ornek.com -d www.ornek.com
Certbot yapılandırmaya listen 443 ssl bloğunu ve sertifika yollarını kendisi ekler, 80 portunu 443'e yönlendirir. Yenilemeyi test edin:
sudo certbot renew --dry-run
Sertifika kurulduktan sonra SECURE_SSL_REDIRECT = True ayarını da açıp servisi yeniden başlatın. Ücretsiz sertifika sürecinin ayrıntıları için Let's Encrypt ile ücretsiz SSL, Nginx tarafındaki protokol ve şifre paketi ayarları için Nginx SSL yapılandırması yazılarına bakabilirsiniz.
Kod Güncellemesinden Sonra Ne Yapılır?#
Yeni kodu çektikten sonraki sıra sabittir ve sırayı bozmak yarım yayına yol açar:
cd /var/www/proje
sudo -u django git pull
sudo -u django venv/bin/pip install -r requirements.txt
sudo -u django venv/bin/python manage.py migrate --noinput
sudo -u django venv/bin/python manage.py collectstatic --noinput
sudo systemctl reload proje
reload, restart'tan farklı olarak Gunicorn'a HUP sinyali gönderir: eski işçiler mevcut isteklerini bitirir, yenileri yeni kodla açılır. Kesinti neredeyse sıfırdır. Ancak EnvironmentFile içeriğini veya servis dosyasını değiştirdiyseniz reload yetmez, daemon-reload ve restart gerekir.
Deploy Sonrası En Sık Görülen Hatalar ve Teşhisi#
Aşağıdaki tablo, "yayına aldım ama açılmıyor" durumlarının büyük çoğunluğunu kapsıyor. Sıralama, tarayıcının gösterdiği belirtiye göre:
| Belirti | Muhtemel sebep | Doğrulama / çözüm |
|---|---|---|
400 Bad Request | ALLOWED_HOSTS içinde istekteki host yok | journalctl -u proje içinde DisallowedHost arayın; alan adını listeye ekleyin |
502 Bad Gateway | Gunicorn çalışmıyor veya socket izni yok | systemctl status proje; ls -l /run/proje/gunicorn.sock çıktısında grubun www-data olduğunu doğrulayın |
504 Gateway Time-out | Bir istek proxy_read_timeout süresini aşıyor | Yavaş sorguyu bulun; uzun işi arka plan görevine taşıyın |
| Sayfa açılıyor, tasarım yok | collectstatic çalıştırılmadı veya alias yolu yanlış | ls /var/www/proje/staticfiles/; curl -I https://ornek.com/static/admin/css/base.css |
500 Internal Server Error | Uygulama istisnası (çoğunlukla veritabanı veya eksik ortam değişkeni) | journalctl -u proje -n 100 — gerçek traceback burada |
CSRF verification failed | CSRF_TRUSTED_ORIGINS eksik veya şemasız yazılmış | Değeri https://ornek.com biçiminde verin |
ERR_TOO_MANY_REDIRECTS | SECURE_SSL_REDIRECT açık, SECURE_PROXY_SSL_HEADER tanımsız | Proxy başlığı ayarını ekleyin ve Nginx'te X-Forwarded-Proto gönderildiğini doğrulayın |
Yüklemede 413 | client_max_body_size varsayılanı 1 MB | Nginx bloğunda değeri yükseltin, reload edin |
| Site bir süre sonra düşüyor | İşçi sayısı RAM'e göre fazla, OOM killer devrede | journalctl -k çıktısında oom arayın; --workers değerini düşürün |
Teşhiste altın kural şudur: 502 Nginx'in şikâyetidir, 500 Django'nun. 502 alıyorsanız hata Python kodunda değildir; Nginx arka uca hiç ulaşamamıştır, önce servis durumuna ve socket iznine bakın. 500 alıyorsanız istek Django'ya ulaşmıştır ve gerçek sebep journalctl -u proje çıktısındaki traceback'tedir; DEBUG=False olduğu için tarayıcı size hiçbir ipucu vermez, log tek kaynaktır.
Bir de sessiz bir tuzak var: DEBUG=True bırakılmış bir üretim sitesi hata sayfasında ayarları, dosya yollarını ve sorgu içeriklerini olduğu gibi gösterir. Yayına almadan önce curl -s https://ornek.com/olmayan-sayfa | head -20 çıktısında Django'nun ayrıntılı hata ekranını görüyorsanız derhal DEBUG değerini kapatın.
Sıkça Sorulan Sorular#
Gunicorn yerine uWSGI kullanmalı mıyım?#
İkisi de olgun WSGI sunucularıdır ve doğru yapılandırıldığında performansları birbirine yakındır. Gunicorn'un yapılandırması belirgin biçimde daha basittir; Django belgelerindeki örneklerin çoğu da Gunicorn üzerinden ilerler. uWSGI daha fazla ayar düğmesi ve dil desteği sunar, ancak bu esneklik öğrenme maliyetiyle gelir. Yeni bir projede Gunicorn'la başlamak makul bir varsayılandır; ölçek gerçekten sorun çıkardığında geçiş yapmak zaten kolaydır.
Unix socket mi TCP portu mu tercih etmeliyim?#
Nginx ve Gunicorn aynı sunucudaysa Unix socket daha iyidir: TCP yığınını atladığı için biraz daha hızlıdır ve dosya sistemi izinleriyle korunduğu için dışarıdan erişilemez. Gunicorn'u ayrı bir makinede ya da konteynerde çalıştırıyorsanız TCP portu zorunlu hâle gelir. O durumda --bind 127.0.0.1:8000 yerine gerçek arayüze bağlanacaksanız güvenlik duvarını mutlaka kısıtlayın.
collectstatic komutunu her deploy'da çalıştırmak gerekli mi?#
Evet, statik dosyalarda en ufak bir değişiklik olduysa gereklidir. collectstatic tüm uygulamaların statiklerini STATIC_ROOT klasörüne kopyalar; Nginx de yalnızca oradan okur. CSS'i düzenleyip komutu atlarsanız Nginx eski kopyayı servis etmeye devam eder ve değişiklik sitede görünmez. Deploy betiğinizde migrate ile birlikte sabit bir adım olarak tutun.
Kaç Gunicorn işçisi açmalıyım?#
Başlangıç formülü (2 × çekirdek sayısı) + 1'dir, ancak asıl sınır bellektir. Her işçi uygulamanın tam bir kopyasını yükler ve tipik bir Django projesinde 80-150 MB tutar. RAM'iniz elverişsizse işçi sayısını düşürüp --threads ile iş parçacığı eklemek daha iyi sonuç verir. Ayarı değiştirdikten sonra gerçek trafik altında bellek kullanımını izleyip kararı ölçüme dayandırın.
Veritabanı olarak SQLite üretimde kullanılabilir mi?#
Çok düşük trafikli, tek yazıcılı ve tek sunucuda çalışan projelerde çalışır. Ancak SQLite yazma işlemlerinde tüm veritabanını kilitler; birden fazla Gunicorn işçisi aynı anda yazmaya kalktığında database is locked hataları başlar. Kullanıcı üreten herhangi bir projede PostgreSQL veya MySQL tercih edin. Geçiş yapacaksanız veriyi dumpdata ile alıp yeni veritabanına loaddata ile aktarabilirsiniz.
Sunucuyu yeniden başlattığımda site otomatik açılıyor mu?#
systemctl enable proje komutunu çalıştırdıysanız evet. Bu komut servisi multi-user.target hedefine bağlar ve açılışta otomatik başlatır. Doğrulamak için systemctl is-enabled proje çıktısının enabled döndüğünü kontrol edin. Ayrıca /run dizini her yeniden başlatmada sıfırlandığı için socket klasörünün RuntimeDirectory ile tanımlanmış olması şarttır; elle oluşturulmuş bir dizin yeniden başlatmada kaybolur ve servis açılışta hata verir.