Docker'da Django Static ve Media: nginx Neden 404 Verir?
Django ve nginx'i Docker Compose ile bir araya getirdiyseniz — Django bir container'da Gunicorn arkasında, nginx önde reverse proxy ve static dosya sunucusu — collectstatic sorunsuz çalışır, migration'lar uygulanır, site açılır. Sonra iki şeyden biri olur: bir kullanıcının geçen hafta yüklediği görsel bir deploy sonrası kaybolur, ya da static dosya sunumu kendi container'ına ayrılır ayrılmaz nginx her CSS dosyasında 404 döner.
İkisi de Django hatası değil. İkisinin de kaynağı gözden kaçması kolay bir ayrıntı: static ve media dosyaları dosya sistemindeki gerçek dosyalardır, Compose'da ise siz açıkça söylemedikçe "dosya sistemi" container'lar arasında paylaşılmaz.
Static ve media: iki ayar, iki farklı yaşam döngüsü
Django bu ikisini bilinçli olarak ayırır:
STATIC_ROOT—collectstatic'in uygulamanızın CSS, JS ve paketlenmiş varlıklarını topladığı yer. İçeriğin her baytını siz kontrol edersiniz; içerik deploy anında sabitlenir.MEDIA_ROOT— Django'nun kendi dokümantasyonu bunu kullanıcı yüklediği dosyaları tutan, mutlak dosya sistemi yolu olarak tanımlıyor. İçerik çalışma zamanında, kullanıcılardan gelir; Django ayrıcaMEDIA_ROOTileSTATIC_ROOT'un aynı yol olamayacağını doğrular — ikisini karıştırmak gerçek güvenlik sorunlarına yol açabiliyor.
collectstatic MEDIA_ROOT'a hiç dokunmaz, yalnızca static dosyaları bilir. "Collectstatic dosyaları hallediyor" varsayımı, kullanıcının yüklediği hiçbir şey için (bunun içine Django admin inline'larından yüklenen görseller de dahil) doğru değildir.
Geliştirme sunucusu sorunu neden gizler
Geliştirme ortamında runserver static dosyaları otomatik sunar — ama sadece DEBUG=True iken. Django'nun kendi dokümantasyonu bu yöntemi "son derece verimsiz ve büyük olasılıkla güvensiz" olarak tanımlıyor ve production için uygun olmadığını açıkça söylüyor. DEBUG=False olduğunda bu otomatik sunum durur, siz bir şey kurmadıysanız yerine hiçbir şey geçmez.
Media dosyalarının zaten böyle bir kolaylığı hiç olmadı. Bazı eğitimlerin yüklenen dosyaları yerelde sunmak için urls.py'a eklediği static() yardımcı fonksiyonu da yalnızca debug modunda çalışır — bu bir geliştirme kısayolu, production planı değil.
Yani production'a çıkmak "debug modunu kapat ve gönder" değildir. "Django'nun sizden şimdiye kadar gizlediği iki dosya sunma sorununu artık siz üstleniyorsunuz" demektir.
Asıl kırılma noktası: container ayrımı
Yaygın kurulumu düşünün: Django'yu çalıştıran bir backend container'ı, önünde de /static/ ve /media/ yollarını doğrudan sunacak şekilde ayarlanmış bir nginx container'ı — her varlık isteğini Gunicorn üzerinden yönlendirmek yerine:
location /static/ { alias /app/staticfiles/; expires 30d; }
location /media/ { alias /app/media/; expires 7d; }Bu doğru bir tercih — nginx'in dosyaları doğrudan sunması Django üzerinden yönlendirmekten çok daha hızlı. Ama alias /app/staticfiles/ şu anlama gelir: "bu yola nginx container'ının içinde bak." collectstatic o dosyaları backend container'ının içine yazdı. İki farklı container, iki farklı dosya sistemi. Aralarında açık bir bağlantı olmadıkça nginx'in /app/staticfiles/ yolu boş bir dizindir ve her varlık isteği 404 döner.
Çözüm, her iki servise de bağlanan adlandırılmış (named) bir volume:
volumes:
staticfiles:
media:
services:
backend:
volumes:
- staticfiles:/app/staticfiles
- media:/app/media
nginx:
volumes:
- staticfiles:/app/staticfiles:ro
- media:/app/media:roArtık iki container da aynı dizini görüyor. backend yazıyor, nginx sadece okuyor (:ro).
Redeploy'lar yüklenen dosyaları neden sessizce siler
404 durumu gürültülüdür — hemen patlar, fark edersiniz, düzeltirsiniz. Dosyaların kaybolması durumu daha sessiz ve daha kötüdür: haftalarca sorunsuz çalışır, sonra rutin bir deploy kullanıcı verisini siler.
Docker'ın kendi dokümantasyonu bu konuda net: varsayılan olarak bir container içinde oluşturulan dosyalar, salt okunur imaj katmanlarının üzerindeki yazılabilir bir katmanda tutulur ve container yok edildiğinde bu veri kalıcı olmaz. docker compose up --build — veya backend container'ını yeniden oluşturan herhangi bir iş akışı — eski container'ı kaldırıp imajdan yeni bir tane başlatır. MEDIA_ROOT hiçbir zaman bir volume'e bağlanmadıysa, son iki deploy arasında kullanıcının yüklediği her dosya gitmiştir. Arşivlenmemiş, taşınmamış — gitmiştir, çünkü artık var olmayan bir container'ın içinde var olmuştur sadece.
Bu yüzden media volume'ü staticfiles volume'ünden daha kritiktir. Üretilmiş CSS'i kaybetmenin maliyeti bir yeniden build'dir. Bir kullanıcının yüklediği fotoğrafı kaybetmenin maliyeti mutsuz bir kullanıcıdır — ve o fotoğraf tek kopyaysa, kalıcı bir kayıptır.
Cache header'ları birbirinin yerine geçmez
Yukarıdaki nginx bloğundaki iki expires değeri bir stil tercihi değil — iki dosya türünün nasıl değiştiği konusundaki gerçek bir farkı yansıtıyor.
Django'nun ManifestStaticFilesStorage sınıfı, her dosyanın içeriğinin MD5 özetini dosya adına ekler — styles.css, styles.55e7cbb9ba48.css gibi bir şey olur. Dosya değişince adı da değişir. Bu tam olarak agresif, uzun ömürlü cache header'larını güvenle kullanabilmeniz için tasarlanmıştır: tarayıcılar ve CDN'ler dosyayı "sonsuza kadar" cache'leyebilir, çünkü içerik değişikliği farklı bir URL üretir, aynı URL'de farklı içerik hiçbir zaman olmaz.
Media dosya adları varsayılan olarak hash'lenmez. Bir kullanıcı profil fotoğrafını değiştirse ve URL /media/avatars/42.jpg olarak kalsa, 30 günlük bir expires header'ı bazı ziyaretçilerin bir aya kadar eski fotoğrafı görmeye devam etmesi demektir. /media/ için daha kısa bir süre — veya media dosya adlarını da kendiniz hash'lemediğiniz sürece hiç uzun cache — bunu önler.
Üçüncü kırılma noktası: izinler
Daha nadir ama dosyalar diskte varken nginx hâlâ okuyamıyorsa bakılması gereken yer: Django'nun FILE_UPLOAD_PERMISSIONS ayarı varsayılan olarak 0o644'tür — herkes tarafından okunabilir — tam olarak Django'dan farklı bir kullanıcı olarak çalışan bir web sunucusunun dosyayı yine de sunabilmesi için. Dockerfile'ınız Django'yu ayrı bir root olmayan kullanıcı olarak çalıştırıyorsa (kendi başına iyi bir pratik) ve kurulumunuzdaki bir şey bu varsayımı sıkılaştırıyorsa — özel bir storage sınıfı, kısıtlayıcı bir umask, kilitli bir base imaj — nginx'in kullanıcısı, Django'nun kendi kullanıcısının sorunsuz gördüğü bir dosyaya okuma izni kaybedebilir. Dosyalar diskte varken nginx 404 değil 403 dönüyorsa bakılacak yer burasıdır; dizinler için karşılık gelen FILE_UPLOAD_DIRECTORY_PERMISSIONS ayarı da dahil.
Media dosyalarının da yedeği gerekir, sadece veritabanının değil
Volume, "redeploy'da kayboluyor" sorununu çözer ama "disk bozulduğu için kayboldu" sorununu çözmez. Adlandırılmış bir volume yine de tek bir sunucuda yaşar. Yedekleme rutininiz sadece pg_dump ile veritabanını kapsıyorsa — yaygın ve doğru bir ilk adım — kullanıcı yüklediği dosyalar hâlâ hiçbir yerde kopyası olmayan tek bir hata noktasıdır.
Çözüm veritabanı yedeğiyle aynı şekle sahip, sadece bir tabloya değil bir dizine yönelik: media volume'ünü bir programa göre object storage'a (veya başka bir sunucuya) senkronize edin, veritabanı dump'ını cron'ladığınız gibi. Veritabanının aksine media dosyaları yüklendikten sonra nadiren değişir, bu yüzden artımlı (incremental) bir senkronizasyon — sadece yeni veya değişen dosyaları kopyalayan — genelde fark edilir bir maliyet olmadan her gün çalıştırılabilecek kadar hafiftir.
Sonuç olarak ne yapmalı
- Gerçek kullanıcılar dosya yüklemeye başlamadan önce
MEDIA_ROOTiçin adlandırılmış bir volume tanımlayın; backend'de okuma-yazma, nginx'te salt okunur olarak bağlayın. Sonradan eklemek, mevcut dosyaları volume'e elle taşımak demektir. - nginx ve backend farklı container'lar olduğunda
STATIC_ROOTiçin de aynı sebeple ayrı bir volume tanımlayın. - Static dosya cache'ini agresif tutun — içerik hash'lemesi sayesinde güvenlidir — media dosya cache'ini ise media dosya adlarını da hash'lemediğiniz sürece daha kısa tutun.
- Dosyalar var ama sunulmuyorsa, eksik volume varsaymadan önce sahiplik ve izinleri kontrol edin.
- Media volume'ünü de veritabanıyla aynı sıklıkta yedekleyin — volume sizi kötü bir redeploy'dan korur, kaybolan bir diskten korumaz.
Bunların hiçbiri egzotik değil. Production'a hazır bir Compose dosyasındaki veritabanı volume'üyle aynı ders: kaybetmeyi kaldıramayacağınız her şeyin bir volume'e ihtiyacı vardır, bir gün yeniden oluşturulacak container'ın içindeki sıradan bir çalışma dizinine değil.