Static files vs. media files
Django treats static files and media files as two different categories with different lifecycles:
- Static files: assets shipped with your code (CSS, JavaScript, icons, images used by the UI). They are typically versioned and deployed alongside releases.
- Media files: user-generated uploads (profile pictures, PDFs, attachments). They are created at runtime and must persist across deployments.
This separation matters because static files are usually collected and served from a dedicated location (or CDN), while media files require upload handling, validation, access control, and storage that survives redeploys.
Configuring static files
Core settings
Static configuration revolves around three common settings:
STATIC_URL: the URL prefix used when generating links to static assets.STATIC_ROOT: the directory wherecollectstaticgathers all static assets for production serving.STATICFILES_DIRS(optional): extra directories where Django looks for static files during development (often a project-levelstatic/folder).
# settings.py (relevant excerpt) STATIC_URL = "static/" # In production, set an absolute URL if using a CDN, e.g. https://cdn.example.com/static/ STATIC_ROOT = BASE_DIR / "staticfiles" # Optional: project-level static directory (in addition to app/static/) STATICFILES_DIRS = [BASE_DIR / "static"]How static behaves in development vs. production
- Development:
runservercan serve static files automatically whenDEBUG=True(withdjango.contrib.staticfilesenabled). You typically reference assets via the template tag{% static %}. - Production: Django should not serve static files directly. You run
python manage.py collectstaticto copy all static assets intoSTATIC_ROOT, and then a web server (Nginx/Apache) or a CDN serves them.
Practical implication: if you forget to run collectstatic in production, your CSS/JS will often 404 even though it worked locally.
Referencing static assets in templates
{% load static %} <link rel="stylesheet" href="{% static 'css/site.css' %}"> <script src="{% static 'js/app.js' %}" defer></script>Place app-specific assets under your_app/static/your_app/... to avoid naming collisions between apps.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Configuring media files (user uploads)
Core settings
Media configuration is simpler but has higher security and operational impact:
MEDIA_URL: the URL prefix for uploaded files.MEDIA_ROOT: the filesystem directory where uploaded files are stored (for the default local storage backend).
# settings.py (relevant excerpt) MEDIA_URL = "media/" MEDIA_ROOT = BASE_DIR / "media"How media behaves in development vs. production
- Development: you can configure Django to serve media files when
DEBUG=Trueby adding a URL pattern that mapsMEDIA_URLtoMEDIA_ROOT. - Production: do not serve media through Django. Use a web server (Nginx) or an object storage service (S3-compatible) and ensure access rules match your privacy requirements.
Development-only URL configuration:
# urls.py (project-level) from django.conf import settings from django.conf.urls.static import static from django.urls import path, include urlpatterns = [ path("", include("your_app.urls")), ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)This is intentionally guarded by DEBUG because it is not suitable for production performance or security.
Creating a model with FileField/ImageField
Install Pillow for ImageField
ImageField requires Pillow:
pip install PillowExample model: Document upload
This example stores an uploaded file and some metadata. The upload_to path can be a folder name or a callable for dynamic paths.
# models.py import uuid from django.db import models def upload_to_documents(instance, filename): # Keep user uploads organized and reduce name collisions # Example: documents/2026/01/<uuid>_originalname.pdf return f"documents/{instance.created_at:%Y/%m}/{uuid.uuid4()}_{filename}" class Document(models.Model): title = models.CharField(max_length=200) file = models.FileField(upload_to=upload_to_documents) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.titleExample model: Image upload
# models.py import uuid from django.db import models def upload_to_images(instance, filename): return f"images/{uuid.uuid4()}_{filename}" class Photo(models.Model): caption = models.CharField(max_length=200, blank=True) image = models.ImageField(upload_to=upload_to_images) created_at = models.DateTimeField(auto_now_add=True)Note on naming: user-provided filenames can contain spaces, unicode, or tricky characters. Prefixing with a UUID reduces collisions and makes guessing harder. If you need stricter naming, sanitize the filename (e.g., keep only safe characters) before storing.
Building an upload form (ModelForm)
Form definition
# forms.py from django import forms from .models import Document class DocumentUploadForm(forms.ModelForm): class Meta: model = Document fields = ["title", "file"]View: handle multipart uploads
File uploads require request.FILES and a multipart/form-data form encoding.
# views.py from django.shortcuts import render, redirect from .forms import DocumentUploadForm def upload_document(request): if request.method == "POST": form = DocumentUploadForm(request.POST, request.FILES) if form.is_valid(): form.save() return redirect("document_list") else: form = DocumentUploadForm() return render(request, "documents/upload.html", {"form": form})Template: correct form encoding
<!-- templates/documents/upload.html --> <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">Upload</button> </form>List and display uploaded files
When a FileField/ImageField is populated, Django provides:
instance.file.url: URL to access the file (requires correct media serving).instance.file.name: storage path relative to the storage root.instance.file.path: absolute filesystem path (only for local filesystem storage; avoid relying on it if you may switch to S3).
# views.py from django.shortcuts import render from .models import Document def document_list(request): docs = Document.objects.order_by("-created_at") return render(request, "documents/list.html", {"docs": docs})<!-- templates/documents/list.html --> <ul> {% for d in docs %} <li> {{ d.title }} - <a href="{{ d.file.url }}">Download</a> </li> {% endfor %} </ul>For images, you can render an <img> tag:
<img src="{{ photo.image.url }}" alt="{{ photo.caption|default:'Photo' }}">Validation and safety for uploads
Validate file size
Django does not enforce size limits automatically. Add a validator at the form level (or model field validators) to reject overly large files.
# forms.py from django import forms from .models import Document MAX_UPLOAD_SIZE = 5 * 1024 * 1024 # 5 MB class DocumentUploadForm(forms.ModelForm): class Meta: model = Document fields = ["title", "file"] def clean_file(self): f = self.cleaned_data["file"] if f.size > MAX_UPLOAD_SIZE: raise forms.ValidationError("File too large (max 5 MB).") return fValidate file type (don’t trust extensions)
Checking only the filename extension is weak. Prefer validating:
- Content type (provided by the client) as a quick filter, but it can be spoofed.
- File signature (magic bytes) using a library if you need stronger guarantees.
Basic content-type allowlist example:
# forms.py ALLOWED_CONTENT_TYPES = {"application/pdf", "image/png", "image/jpeg"} def clean_file(self): f = self.cleaned_data["file"] if getattr(f, "content_type", None) not in ALLOWED_CONTENT_TYPES: raise forms.ValidationError("Unsupported file type.") return fFor images, ImageField plus Pillow will reject many invalid images, but you should still consider size limits and, for public sites, additional scanning if risk is high.
Protect against dangerous file serving
- Never serve private uploads from a public
MEDIA_URLpath. If a file must be private, store it in a non-public location and stream it through a view that checks permissions (or use signed URLs with object storage). - Avoid letting users upload executable content that your server might execute. With correct configuration, uploads should be served as static bytes, not executed.
- Consider setting security headers and correct
Content-TypeandContent-Dispositionwhen serving downloads through Django.
File naming and upload paths
Why upload_to matters
upload_to helps you:
- Organize uploads by type/date/user.
- Reduce collisions (two users uploading
resume.pdf). - Make cleanup and lifecycle policies easier (e.g., delete old temporary uploads).
Common patterns
| Pattern | Example | When to use |
|---|---|---|
| Date-based folders | uploads/2026/01/... | Large volume uploads; easy archival |
| User-based folders | users/<user_id>/... | Per-user management and quotas |
| UUID filenames | <uuid>_original.ext | Collision resistance; less guessable |
If you need the uploaded file name to be fully controlled, generate a new name and discard the original. If you keep the original, treat it as untrusted input and sanitize it.
Storage backends: local filesystem vs. object storage
Default storage (local filesystem)
By default, Django stores uploads under MEDIA_ROOT on the server’s disk. This is simple but has production drawbacks:
- Multiple web servers need shared storage (NFS) or uploads will “disappear” when requests hit different machines.
- Deployments that rebuild containers can wipe local files unless you mount persistent volumes.
Object storage (recommended for many production setups)
A common approach is storing media in S3-compatible storage and serving via CDN or signed URLs. In Django, this is typically done by configuring a custom storage backend (often via django-storages) so that FileField/ImageField automatically read/write to that backend.
Key practical considerations when choosing a backend:
- Public vs. private: public bucket for public images; private bucket + signed URLs for sensitive files.
- Lifecycle rules: auto-delete temporary uploads after N days.
- Cost and bandwidth: large downloads can be expensive without a CDN.
- Latency: consider direct-to-cloud uploads for large files (frontend uploads directly to S3 with a pre-signed POST).
Access rules and serving private files
Public media (e.g., blog images) can be served directly from a public media URL. Private media (e.g., invoices, internal documents) should be protected.
Pattern: permission-checked download view
Instead of linking to {{ obj.file.url }}, create a view that checks permissions and returns the file as an attachment. For local storage, you can use FileResponse.
# views.py from django.http import FileResponse, Http404 from django.contrib.auth.decorators import login_required from .models import Document @login_required def download_document(request, pk): doc = Document.objects.get(pk=pk) # Example rule: only staff can download if not request.user.is_staff: raise Http404() # For local filesystem storage: return FileResponse(doc.file.open("rb"), as_attachment=True, filename=doc.file.name.rsplit("/", 1)[-1])For object storage, you often generate a signed URL and redirect the user to it after permission checks, rather than streaming through Django.
Operational considerations
Cleaning up files when records are deleted
Deleting a model instance does not always delete the underlying file automatically in all workflows. Consider explicitly removing files on delete (and on file replacement) using signals or a dedicated cleanup strategy. Be careful with shared files and with storage backends where delete operations are eventual.
Limits and timeouts
- Large uploads may require tuning reverse proxy limits (e.g., Nginx
client_max_body_size) and application server timeouts. - Consider chunked uploads or direct-to-object-storage uploads for large files.
Security checklist for uploads
- Enforce maximum size.
- Allowlist types; consider signature validation for high-risk environments.
- Store private files privately; do not expose them under a public
MEDIA_URL. - Use unpredictable names (UUIDs) if public URLs should be hard to guess.
- Consider malware scanning for public upload features.