Docker & DevOps

    Docker ile Django Uygulaması Çalıştırma

    Django projesini Gunicorn, PostgreSQL ve Celery ile birlikte konteynerde çalıştırmanın pratik rehberi.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    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.

    KonuGeliştirmeÜretim
    SunucurunserverGunicorn / ASGI sunucusu
    Statik dosyalarDjango sunarcollectstatic + web sunucusu
    VeritabanıSQLite olabilirPostgreSQL
    DEBUGTrueFalse
    ALLOWED_HOSTSboş bırakılabiliraçıkça tanımlı
    Arka plan görevlerisenkronCelery 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üyorcollectstatic ç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üyorPYTHONUNBUFFERED 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.

    DjangoDockerPython

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.