Mengelola aset aplikasi: struktur static files, pengumpulan dengan collectstatic dan penyajian dengan WhiteNoise, upload media pengguna dengan FileField, serta memindahkan storage ke S3-compatible untuk production.

Aplikasi blog devblog sudah punya halaman, API, auth, dan background task. Tapi belum ada satu aspek yang tidak bisa dihindari: aset — CSS, JavaScript, gambar, dan file upload pengguna. Di episode ini kita mengatur Static Files, Media, dan Storage: bagaimana Django mengumpulkan dan menyajikannya di development, lalu memindahkannya ke object storage untuk produksi.
Mengapa topik ini penting? Karena cara aset disajikan menentukan kecepatan loading halaman — dan file yang di-upload user menentukan ketersediaan data. Kesalahan konfigurasi storage berarti gambar rusak, CSS tidak ke-load, atau — lebih buruk — file hilang saat server diganti.
Django membedakan dua jenis aset:
| Static files | Media files | |
|---|---|---|
| Contoh | CSS, JS, gambar template | Avatar user, gambar post |
| Sumber | Dikontrol developer (repository) | Di-upload pengguna |
| Penyimpanan | STATIC_ROOT setelah collectstatic | MEDIA_ROOT |
| Backup | Ikut codebase | Perlu backup terpisah |
Di settings:
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"STATICFILES_DIRS adalah folder static project (di luar app), sementara tiap app punya folder blog/static/blog/. STATIC_ROOT adalah folder output saat collectstatic — ini yang disajikan web server di produksi.
Untuk development, serve media lewat urls:
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)Warning
Blok static(settings.MEDIA_URL, ...) di atas hanya boleh aktif saat DEBUG = True. Di produksi, Nginx/CDN (episode 23) yang menyajikan media — Django sendiri tidak dirancang untuk menyajikan file besar secara efisien. Lupa mematikan ini di produksi = bypass yang menghantam performa.
Di produksi, semua static harus dikumpulkan ke satu folder dan disajikan tanpa meminta web server eksternal mengelola (atau bersama-sama dengan itu). collectstatic menyalin semua static dari semua app + STATICFILES_DIRS ke STATIC_ROOT:
python manage.py collectstatic --noinputUntuk menyajikan static langsung dari aplikasi Django (tanpa Nginx khusus static), pakai WhiteNoise:
pip install whitenoiseMIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
...
]
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}WhiteNoiseMiddleware harus berada tepat setelah SecurityMiddleware. CompressedManifestStaticFilesStorage menggabungkan ManifestStaticFilesStorage (nama file di-hash untuk cache busting) dengan kompresi gzip/brotli otomatis. Hasilnya: static cepat dengan cache yang benar — tanpa server static terpisah.
Tambahkan upload gambar ke model:
class Post(models.Model):
...
cover_image = models.ImageField(upload_to="post_covers/", blank=True, null=True)pip install PillowImageField butuh Pillow untuk validasi gambar. File di-upload ke MEDIA_ROOT/post_covers/ dengan nama acak (Django menambahkan suffix untuk mencegah tabrakan nama). Di template:
{% if post.cover_image %}
<img src="{{ post.cover_image.url }}" alt="{{ post.title }}">
{% endif %}{{ post.cover_image.url }} otomatis menghasilkan URL di MEDIA_URL.
Tip
Jangan pernah menggunakan file yang di-upload user langsung tanpa validasi ukuran dan jenis. Batasi MaxLengthValidator pada file, periksa tipe konten, dan pertimbangkan resize gambar dengan Pillow saat proses (task Celery episode 13 cocok untuk ini). File anonim yang di-upload adalah salah satu vektor penyebaran malware.
Di produksi dengan banyak instance (episode 24), media tidak boleh disimpan di disk lokal setiap server — server mana yang menyimpan avatar user itu? Solusinya object storage: S3 AWS atau yang kompatibel seperti Cloudflare R2, MinIO, dan Backblaze B2.
pip install django-storages boto3import os
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
AWS_ACCESS_KEY_ID = env.AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY = env.AWS_SECRET_ACCESS_KEY
AWS_STORAGE_BUCKET_NAME = env.AWS_STORAGE_BUCKET_NAME
AWS_S3_REGION_NAME = env.AWS_S3_REGION_NAME
AWS_S3_ENDPOINT_URL = env.AWS_S3_ENDPOINT_URL # untuk R2/MinIO
AWS_DEFAULT_ACL = "private"Kredensial semua lewat env dari episode 16. AWS_S3_ENDPOINT_URL membuat konfigurasi yang sama bekerja untuk AWS S3 maupun layanan S3-compatible (R2, MinIO) — cukup ganti env. AWS_DEFAULT_ACL = "private" penting: file upload bersifat privat dan hanya disajikan saat ada authorisasi (untuk akses publik, buat presigned URL).
from django.core.files.storage import default_storage
def get_media_url(file_field):
return default_storage.url(file_field.name, expire=3600)default_storage kini mengarah ke S3 — semua kode lama (model.file.url) tetap bekerja tanpa perubahan. Inilah keunggulan abstraksi storage Django.
Untuk static dan media yang sering diakses, pasang CDN di depan (Cloudflare, CloudFront). Dua pengaturan yang saling mendukung:
from django.views.decorators.cache import cache_control
@cache_control(max_age=60 * 60 * 24 * 30, immutable=True)
def serve_cached_asset(request, path):
...ManifestStaticFilesStorage sudah memberi nama file ber-hash — jadi header immutable=True aman dipakai (file baru punya nama baru). Media user sebaiknya private + presigned URL agar tidak sembarang orang mengunduh.
Inti yang harus dibawa pulang:
collectstatic mengumpulkan ke STATIC_ROOT; WhiteNoise menyajikan cepat tanpa server terpisah.ImageField butuh Pillow; validasi ukuran & jenis file upload.django-storages membuatnya transparan.env (episode 16); file privat + presigned URL.ManifestStaticFilesStorage memberi hash filename → cache immutable aman.Di episode 18 selanjutnya kita masuk fase keamanan: Security Best Practices (OWASP) — XSS, CSRF, SQL injection, clickjacking, security headers, manage.py check --deploy, dan hardening produksi dengan SECURE_* settings. Sampai jumpa di episode 18!