Django geliştirmeye başlarken python manage.py runserver komutu her şeyi halleder gibi görünür; ancak yayına çıkma vakti geldiğinde tablo değişir. Geliştirme sunucusu üretim için uygun değildir, statik dosyalar artık Django tarafından sunulmaz, veritabanı SQLite'tan çıkar, arka plan görevleri için ayrı bir işçi süreci gerekir. Docker ile Django uygulaması çalıştırmanın değeri, bu parçaların hepsini tek bir tanım dosyasında toplayıp geliştirme ile üretim arasındaki farkı öngörülebilir hale getirmesidir.
Bu rehberde Django için dört servisli bir yapı kuracağız: Gunicorn ile çalışan uygulama, PostgreSQL, Redis ve Celery işçisi. Bağımlılıkları doğru katmanlarda kuran bir Dockerfile yazacak, statik ve medya dosyalarının hacimlerle nasıl yönetileceğini, göçlerin başlangıçta nasıl güvenle çalıştırılacağını, ALLOWED_HOSTS ile ters proxy ilişkisini ve DEBUG kapalıyken ortaya çıkan tuzakları ele alacağız. Son bölümde de "sunucuda 500 dönüyor ama logda hiçbir şey yok" gibi klasik durumların nedenlerini açıklayacağım.
Django'yu Konteynerlemenin Kendine Özgü Tarafları#
Django'nun konteynerde davranışını üç şey belirler ve bunlar çoğu framework'ten farklıdır. Birincisi statik dosyalardır: geliştirmede Django bunları kendisi sunar, üretimde ise collectstatic ile tek klasöre toplanır ve web sunucusu tarafından sunulur. Bu adım atlanırsa yönetim paneli biçimsiz görünür ve CSS dosyaları 404 döner.
İkincisi göç (migration) yönetimidir. Göçler otomatik çalışmaz; başlangıç komutunda ya da elle tetiklenmeleri gerekir. Birden fazla uygulama konteyneri çalıştırıyorsanız hepsinin aynı anda göç çalıştırması istenmeyen bir durumdur; bu yüzden göçü ya tek seferlik ayrı bir adım olarak yürütmek ya da giriş betiğinde kilit mekanizmasıyla korumak gerekir.
Üçüncüsü ise ALLOWED_HOSTS ayarıdır. DEBUG = False yapıldığı anda Django, Host başlığı bu listede olmayan istekleri reddeder. Konteyner arkasında ters proxy varsa gelen başlık beklediğinizden farklı olabilir ve uygulama sessizce 400 Bad Request döner.
| Konu | Geliştirme | Üretim |
|---|---|---|
| Sunucu | runserver | Gunicorn / ASGI sunucusu |
| Statik dosyalar | Django sunar | collectstatic + web sunucusu |
| Veritabanı | SQLite olabilir | PostgreSQL |
| DEBUG | True | False |
| ALLOWED_HOSTS | boş bırakılabilir | açıkça tanımlı |
| Arka plan görevleri | senkron | Celery işçisi |
Servislerin birbirini nasıl bulduğu, ağ ve bağımlılık sırası konularında temel bilgiye ihtiyacınız varsa Docker Compose kullanımı yazısı iyi bir başlangıçtır.
Dockerfile: Bağımlılıklar ve Gunicorn#
Python imajlarında iki ayar hemen her projede aynıdır ve ikisini de açıkça yazmakta fayda vardır: PYTHONDONTWRITEBYTECODE gereksiz .pyc dosyalarını engeller, PYTHONUNBUFFERED ise logların anında standart çıktıya düşmesini sağlar. İkincisi olmadan docker logs çıktısında hiçbir şey görmez, hata ayıklarken kör kalırsınız.
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
# psycopg ve Pillow için gereken sistem kütüphaneleri
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev libjpeg-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Önce yalnızca bağımlılık dosyası: katman önbelleği için
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Sonra kaynak kod
COPY . .
# Ayrıcalıksız kullanıcı
RUN useradd --create-home --uid 1000 django && chown -R django:django /app
USER django
EXPOSE 8000
CMD ["gunicorn", "proje.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "3", \
"--timeout", "60", \
"--access-logfile", "-", \
"--error-logfile", "-"]
Gunicorn işçi sayısı için yaygın başlangıç formülü 2 × çekirdek + 1'dir, ancak bu formül CPU yoğun işler içindir. Django uygulamalarının çoğu veritabanı beklerken boşta durur; bu durumda işçi sayısını artırmak yerine eşzamanlı (async) bir işçi sınıfı kullanmak daha verimli olabilir. Her işçi ayrı bir Python süreci olduğu ve her biri uygulamanın bellek kopyasını taşıdığı için, işçi sayısını konteynerin bellek limitiyle birlikte düşünmelisiniz.
--access-logfile - ve --error-logfile - bayrakları logları standart çıktıya yönlendirir; Docker felsefesinde loglar dosyaya değil çıktıya yazılır ve toplama işini konteyner çalışma zamanı yapar.
docker-compose: Dört Servisli Yapı#
Şimdi servisleri birleştirelim. Celery işçisi uygulama ile aynı imajı kullanır, yalnızca komutu farklıdır:
services:
web:
build: .
restart: unless-stopped
env_file: .env
volumes:
- static_files:/app/staticfiles
- media_files:/app/media
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: django
POSTGRES_USER: django
POSTGRES_PASSWORD: guclu-bir-parola
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U django"]
interval: 5s
retries: 10
redis:
image: redis:alpine
restart: unless-stopped
celery:
build: .
restart: unless-stopped
env_file: .env
command: celery -A proje worker -l info --concurrency=2
depends_on:
- redis
- db
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static_files:/var/www/static:ro
- media_files:/var/www/media:ro
depends_on:
- web
volumes:
pgdata:
static_files:
media_files:
condition: service_healthy kullanımı önemlidir: depends_on tek başına yalnızca başlatma sırasını belirler, servisin hazır olmasını beklemez. PostgreSQL ilk açılışta veritabanını hazırlarken birkaç saniye harcar ve bu sürede Django "connection refused" ile çöker. Sağlık kontrolü bu yarışı ortadan kaldırır.
.env dosyanız yapılandırmayı taşır ve imaja girmemelidir; .dockerignore dosyasına eklemeyi unutmayın:
DEBUG=False
SECRET_KEY=uzun-rastgele-bir-anahtar
ALLOWED_HOSTS=firmaniz.com,www.firmaniz.com
DATABASE_URL=postgres://django:guclu-bir-parola@db:5432/django
CELERY_BROKER_URL=redis://redis:6379/0
CSRF_TRUSTED_ORIGINS=https://firmaniz.com,https://www.firmaniz.com
Statik ve Medya Dosyaları#
Django'da iki farklı dosya kategorisi vardır ve karıştırılmaları sık görülür. Statik dosyalar (CSS, JS, admin varlıkları) kodunuzun parçasıdır ve collectstatic ile toplanır. Medya dosyaları ise kullanıcıların yüklediği içeriktir ve kalıcı olmak zorundadır.
# settings.py karşılıkları
STATIC_URL = /static/
STATIC_ROOT = /app/staticfiles
MEDIA_URL = /media/
MEDIA_ROOT = /app/media
collectstatic komutunu her dağıtımda çalıştırmanız gerekir, çünkü kodunuzdaki yeni varlıklar ancak o zaman toplanır:
docker compose exec web python manage.py collectstatic --noinput
# 178 static files copied to '/app/staticfiles'.
Nginx tarafında bu iki klasörü doğrudan sunarsınız; Django'ya kadar gitmeyen her istek hem daha hızlıdır hem de Gunicorn işçilerini meşgul etmez:
server {
listen 80;
server_name firmaniz.com www.firmaniz.com;
client_max_body_size 32m;
location /static/ {
alias /var/www/static/;
expires 30d;
access_log off;
}
location /media/ {
alias /var/www/media/;
expires 7d;
}
location / {
proxy_pass http://web:8000;
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;
}
}
Medya klasörünün adlandırılmış bir hacimde durması kritik önemdedir; konteyner katmanında kalırsa ilk docker compose down ile kullanıcıların yüklediği her şey silinir. Hangi verinin kalıcı, hangisinin geçici olduğunu ayırt etmek için Docker volume ve veri yönetimi yazısına göz atın.
Göçler ve Giriş Betiği#
Göçleri elle çalıştırmak geliştirmede sorun değildir ama otomatik dağıtımda unutulur. Çözüm, konteyner başlarken çalışan bir giriş betiği yazmaktır:
#!/bin/sh
set -e
echo "Veritabanı bekleniyor..."
python manage.py migrate --noinput
echo "Statik dosyalar toplanıyor..."
python manage.py collectstatic --noinput
echo "Gunicorn başlatılıyor..."
exec "$@"
Bu betiği entrypoint.sh olarak kaydedip Dockerfile'a ekleyin:
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "proje.wsgi:application", "--bind", "0.0.0.0:8000"]
Son satırdaki exec "$@" ifadesi önemlidir: kabuk sürecini Gunicorn ile değiştirir, böylece Gunicorn PID 1 olur ve SIGTERM sinyallerini doğrudan alır. exec olmadan kabuk süreci arada kalır, sinyaller iletilmez ve docker compose stop on saniye bekleyip süreci zorla öldürür.
Süper kullanıcı oluşturmak gibi tek seferlik işleri ise run ile yaparsınız; bu, mevcut servisi etkilemeden geçici bir konteyner açar:
docker compose run --rm web python manage.py createsuperuser
docker compose run --rm web python manage.py shell
docker compose run --rm web python manage.py makemigrations blog
Geliştirme ve Üretim Ortamını Ayırmak#
Aynı compose dosyasını hem geliştirmede hem üretimde kullanmak cazip görünür ama iki ortamın ihtiyaçları taban tabana zıttır. Geliştirmede kodu hacimle bağlayıp anlık değişiklik görmek, üretimde ise kodu imajın içine kopyalayıp dağıtımı değişmez kılmak istersiniz. Compose'un dosya birleştirme (override) özelliği bu ayrımı temiz biçimde kurar.
Ana docker-compose.yml dosyanız üretim davranışını tanımlar. Yanına bir de docker-compose.override.yml koyarsınız; Compose bu dosyayı yerelde otomatik olarak okur ve üstüne bindirir:
# docker-compose.override.yml - yalnızca geliştirme makinesinde
services:
web:
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/app
ports:
- "8000:8000"
environment:
DEBUG: "True"
db:
ports:
- "127.0.0.1:5433:5432"
Sunucuda bu dosya bulunmadığı için üretim davranışı korunur; hiçbir bayrak vermeniz gerekmez. Yine de açıkça kontrol etmek isterseniz dosyaları elle seçebilirsiniz:
# Yerelde: override otomatik uygulanır
docker compose up -d
# Sunucuda: yalnızca ana dosya
docker compose -f docker-compose.yml up -d --build
# Etkin yapılandırmanın son hâlini görmek
docker compose config | head -40
Ayarların kendisi için de aynı ayrımı yapın. Tek bir dev settings.py yerine ortak ayarları bir temel modülde toplayıp geliştirme ve üretim modüllerini ondan türetmek yaygın ve sağlam bir düzendir; hangi modülün kullanılacağını DJANGO_SETTINGS_MODULE ortam değişkeniyle seçersiniz. Böylece DEBUG, log seviyesi, e-posta arka ucu ve önbellek sürücüsü gibi ortama bağlı değerler kod içinde koşullara boğulmadan ayrışır.
Son bir uyarı: geliştirme override dosyasını yanlışlıkla sunucuya kopyalamak, DEBUG=True ile çalışan bir üretim sitesi demektir ve bu, ayar dosyanızın tamamının hata sayfasında görünmesine yol açar. Dağıtım betiğinizde bu dosyanın sunucuda bulunmadığını kontrol eden tek satırlık bir doğrulama, ileride büyük bir baş ağrısını önler.
Sık Yapılan Hatalar#
DisallowedHost hatası — DEBUG=False yaptınız ama ALLOWED_HOSTS içinde istekteki alan adı yok. Ters proxy arkasındaysanız proxy'nin gönderdiği Host başlığının ne olduğunu kontrol edin; Nginx'te proxy_set_header Host $host; satırı orijinal başlığı korur.
Admin paneli biçimsiz görünüyor — collectstatic çalıştırılmamış ya da Nginx alias yolu hacimle uyuşmuyordur. Konteyner içinden dosyaların gerçekten orada olduğunu doğrulayın: docker compose exec nginx ls /var/www/static/admin/css | head.
CSRF doğrulaması başarısız — HTTPS sonlandıran bir proxy arkasında Django isteğin şemasını yanlış algılıyordur. .env içinde CSRF_TRUSTED_ORIGINS tanımlayın ve proxy'nin X-Forwarded-Proto başlığını ilettiğinden emin olun.
Logda hiçbir şey yok ama 500 dönüyor — PYTHONUNBUFFERED ayarlanmamıştır, çıktı tamponda bekliyordur. Dockerfile'daki ENV satırını ekleyin. Ayrıca DEBUG=False iken Django hata ayrıntılarını ekrana basmaz; Gunicorn'un hata logunu standart çıktıya yönlendirdiğinizden emin olun.
Celery görevleri çalışmıyor — İşçi konteyneri ayakta mı, broker adresi doğru mu (redis konteyner adı, localhost değil) ve işçi doğru uygulama modülünü mü hedefliyor diye bakın. docker compose logs -f celery çıktısında işçinin kayıtlı görev listesini görmelisiniz.
Yerelde çalışıyor sunucuda çalışmıyor — Genellikle requirements.txt sürümleri sabitlenmemiştir ve sunucuda farklı sürümler kurulmuştur. Bağımlılıkları kesin sürümlerle sabitleyin; imaj derlemesini tekrarlanabilir kılmanın en basit ve en etkili yolu budur.
Sıkça Sorulan Sorular#
Django'yu Docker'da runserver ile çalıştırabilir miyim#
Geliştirme sırasında evet, üretimde kesinlikle hayır. runserver tek iş parçacıklı bir geliştirme sunucusudur, eşzamanlı istekleri verimli işleyemez, güvenlik açısından sertleştirilmemiştir ve statik dosya sunumu üretim için tasarlanmamıştır. Geliştirmede kod değişikliğini anında görmek için kullanabilir, üretimde Gunicorn ya da bir ASGI sunucusuna geçebilirsiniz.
Göçleri konteyner başlarken çalıştırmak güvenli mi#
Tek uygulama konteyneri çalıştırıyorsanız güvenlidir ve pratiktir. Birden fazla kopya aynı anda başlıyorsa hepsi aynı göçü çalıştırmaya kalkar; PostgreSQL bunu genellikle kilitle yönetir ama yine de risklidir. Ölçekli kurulumlarda göçü ayrı ve tek seferlik bir adım olarak yürütmek, ardından uygulama konteynerlerini başlatmak daha doğru bir sıralamadır.
PostgreSQL yerine SQLite kullanabilir miyim#
Geliştirmede ve çok küçük projelerde çalışır, ancak konteynerde SQLite dosyasının kalıcı bir hacimde durması şarttır; aksi halde konteyner yeniden oluşturulduğunda veritabanı silinir. Eşzamanlı yazma gerektiren üretim yüklerinde SQLite kilitlenme sorunları çıkarır. Üretim düşünüyorsanız baştan PostgreSQL ile başlamak, sonradan taşımaktan çok daha az iş çıkarır.
Celery şart mı#
Hayır. Yalnızca e-posta gönderme, rapor üretme, görsel işleme gibi isteği bekletecek işler varsa gerekir. Bu tür işiniz yoksa Celery ve Redis servislerini hiç açmayın; her ek servis bellek ve bakım maliyeti demektir. İhtiyaç doğduğunda compose dosyasına iki servis eklemek birkaç dakikalık iştir.
Medya dosyalarını nasıl yedeklerim#
Medya adlandırılmış bir hacimde durduğu için hacmin içeriğini bir arşive almanız gerekir. Geçici bir konteyner ile hacmi bağlayıp arşivleyebilir, sonra bu arşivi sunucu dışına kopyalayabilirsiniz. Veritabanı için ise pg_dump ile mantıksal döküm alın; ikisini birlikte almadan yapılan yedek eksik kalır çünkü veritabanındaki kayıtlar dosyalara referans verir. Bu akışın ayrıntıları için Docker uygulama yedekleme ve geri yükleme yazısına bakın.
Ortam değişkenlerini nasıl güvenli tutarım#
.env dosyasını sürüm kontrolüne asla eklemeyin, .dockerignore ile imajdan da dışlayın ve dosya izinlerini 600 yapın. SECRET_KEY değerini üretim için ayrı ve rastgele üretin; geliştirmedeki anahtarı canlıya taşımak, oturum ve token güvenliğini tamamen boşa çıkarır. Rastgele anahtar üretmek için parola üretici aracımızı kullanabilirsiniz.
Kapanış#
Django'yu konteynerde sağlıklı çalıştırmanın özeti şudur: PYTHONUNBUFFERED ile logları görünür kılın, bağımlılık kurulumunu kaynak koddan önceki katmanda tutup derleme süresini kısaltın, statik ve medya dosyalarını ayrı hacimlerde tutup Nginx üzerinden sunun, göçleri bir giriş betiğine alıp exec ile sinyalleri Gunicorn'a devredin ve DEBUG=False ile birlikte ALLOWED_HOSTS ile CSRF_TRUSTED_ORIGINS değerlerini mutlaka doldurun. Bu beş madde, Django dağıtımlarında karşılaşılan sorunların büyük bölümünü baştan kapatır.
Bu yapıyı çalıştıracağınız ortam Docker'ı tam yetkiyle desteklemelidir; VDS ve bulut sunucu paketlerimiz Python tabanlı dağıtımlar için doğrudan uygundur. Görsel işleme veya makine öğrenmesi gibi ağır işler yapıyorsanız GPU VDS seçeneğine bakabilir, sunucu bakımını devretmek isterseniz sunucu yönetimi hizmetimizden yararlanabilirsiniz.