Free Ebook cover Django Fundamentals: From First App to a Complete Backend

Django Fundamentals: From First App to a Complete Backend

New course

12 pages

Static and Media Files in Django: Handling Assets and User Uploads

Capítulo 10

Estimated reading time: 10 minutes

+ Exercise

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 where collectstatic gathers all static assets for production serving.
  • STATICFILES_DIRS (optional): extra directories where Django looks for static files during development (often a project-level static/ 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: runserver can serve static files automatically when DEBUG=True (with django.contrib.staticfiles enabled). You typically reference assets via the template tag {% static %}.
  • Production: Django should not serve static files directly. You run python manage.py collectstatic to copy all static assets into STATIC_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 App

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=True by adding a URL pattern that maps MEDIA_URL to MEDIA_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 Pillow

Example 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.title

Example 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 f

Validate 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 f

For 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_URL path. 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-Type and Content-Disposition when 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

PatternExampleWhen to use
Date-based foldersuploads/2026/01/...Large volume uploads; easy archival
User-based foldersusers/<user_id>/...Per-user management and quotas
UUID filenames<uuid>_original.extCollision 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.

Now answer the exercise about the content:

In a production Django setup, what is the correct approach for handling static files compared to user-uploaded media files?

You are right! Congratulations, now go to the next page

You missed! Try again.

Static files are shipped with the code and should be collected into STATIC_ROOT for serving by a web server or CDN. Media files are user uploads created at runtime, must persist across deployments, and often need validation and access control.

Next chapter

Performance Basics with the Django ORM and Common Bottlenecks

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.