Static files and media handling in Django confuses more developers than almost any other topic, and the confusion usually surfaces at the worst possible time: during the first production deployment. This guide walks through the entire pipeline, from how Django discovers static files in development to how you serve them efficiently in production, including collectstatic, WhiteNoise, CDN integration, user-uploaded media, storage backends, and the security concerns that come with accepting file uploads. For more deployment context, see our Deployment hub.
The distinction between static files and media files is fundamental. Static files are assets you ship with your code: CSS, JavaScript, images, fonts. Media files are user-uploaded content: profile photos, documents, attachments. Django handles them through separate settings and pipelines because they have different lifecycle requirements and security profiles.
Static files in development
During development with DEBUG = True, Django’s runserver command automatically serves static files from each app’s static/ directory and from directories listed in STATICFILES_DIRS.
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
Each app can contain its own static/ directory:
catalog/
static/
catalog/
css/
catalog.css
js/
catalog.js
The app name prefix inside static/ avoids collisions when two apps have a file with the same name. Always namespace your static files.
The collectstatic pipeline
In production, Django does not serve static files itself. Instead, you run collectstatic to gather every static file into a single directory:
python manage.py collectstatic --noinput
This copies files from all static/ directories and STATICFILES_DIRS into STATIC_ROOT:
STATIC_ROOT = BASE_DIR / 'staticfiles'
The collected directory is what your web server, CDN, or WhiteNoise serves to users. Never point STATIC_ROOT to the same path as STATICFILES_DIRS. They have different roles.
WhiteNoise for simple deployments
WhiteNoise lets Django serve its own static files efficiently in production without a separate web server. It adds proper caching headers, Gzip or Brotli compression, and content-addressable filenames.
pip install whitenoise
Add it to middleware, directly after SecurityMiddleware:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# ... other middleware
]
Configure the storage backend:
STORAGES = {
'staticfiles': {
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
}
}
WhiteNoise generates hashed filenames like main.a1b2c3d4.css, enabling aggressive cache headers (often one year). When the file content changes, the hash changes, and browsers fetch the new version automatically.
For an authoritative reference on HTTP caching and asset delivery fundamentals, the MDN Web Docs provide excellent coverage of the underlying mechanisms.
CDN integration
For high-traffic sites, a CDN (Content Delivery Network) serves static files from edge locations close to users. The pattern is:
- Run
collectstaticduring deployment - Upload the collected files to a CDN or object storage (S3, GCS, Azure Blob)
- Set
STATIC_URLto the CDN origin
STATIC_URL = 'https://cdn.prodjango.com/static/'
STORAGES = {
'staticfiles': {
'BACKEND': 'storages.backends.s3boto3.S3StaticStorage',
}
}
Django-storages provides backends for AWS S3, Google Cloud Storage, and Azure. The configuration depends on your cloud provider, but the pattern is always the same: collect, upload, point the URL.
User-uploaded media files
Media files are uploaded by users at runtime. Django stores them according to MEDIA_ROOT and serves them at MEDIA_URL:
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
In a model, use FileField or ImageField:
class Profile(models.Model):
user = models.OneToOneField('auth.User', on_delete=models.CASCADE)
avatar = models.ImageField(upload_to='avatars/%Y/%m/')
The upload_to parameter creates a directory structure based on date, preventing any single directory from containing too many files.
Media file security
Accepting user uploads introduces real security risks:
Never serve media files through Django in production. Use a web server (Nginx) or object storage with appropriate access controls.
Validate file types. Do not trust the file extension alone. Check the MIME type and, for images, verify the file is actually a valid image:
from django.core.validators import FileExtensionValidator
class Document(models.Model):
file = models.FileField(
upload_to='documents/',
validators=[FileExtensionValidator(allowed_extensions=['pdf', 'docx'])]
)
Set file size limits. Configure DATA_UPLOAD_MAX_MEMORY_SIZE and add custom validation to prevent oversized uploads from consuming server resources.
Never execute uploaded files. Ensure your web server does not interpret uploaded files as executable code. Configure Nginx or Apache to serve the media directory with Content-Type headers only, no script execution.
Storage backends for production media
In production, storing media files on the application server’s local filesystem is fragile. If the server is replaced, the files are lost. Use cloud storage:
STORAGES = {
'default': {
'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage',
},
'staticfiles': {
'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
}
}
With django-storages, media files upload directly to S3 or equivalent. The FileField returns a URL pointing to the cloud storage origin.
Template usage
Reference static files in templates with the {% static %} tag:
{% load static %}
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<img src="{% static 'img/logo.png' %}" alt="Logo" width="120" height="40">
Always use the {% static %} tag, never hard-code paths. The tag resolves to the correct URL whether you are using local development, WhiteNoise, or a CDN.
For media files:
{% if profile.avatar %}
<img src="{{ profile.avatar.url }}" alt="{{ profile.user.username }}" width="100" height="100">
{% endif %}
Build pipeline integration
Modern Django projects often include a frontend build step for CSS and JavaScript. Tools like Vite, Webpack, or esbuild compile source files into optimized bundles that land in a static directory.
frontend/
src/
main.js
main.css
dist/ # Build output
main.bundle.js
main.bundle.css
Point Django’s STATICFILES_DIRS at the build output:
STATICFILES_DIRS = [BASE_DIR / 'frontend' / 'dist']
Run the frontend build before collectstatic in your deployment pipeline.
Frequently asked questions
Why do my static files work locally but break in production?
Most likely you forgot to run collectstatic, or STATIC_ROOT is misconfigured. Check that the web server or WhiteNoise is pointing at the correct directory and that STATIC_URL matches the serving path.
Should I commit collected static files to the repository? No. Generate them during the deployment process. Committing build artifacts creates merge conflicts and bloats the repository.
How do I handle static files with Docker?
Run collectstatic during the Docker image build step, not at container startup. This keeps startup fast and makes the image self-contained.
What about serving media files from a different domain?
Setting MEDIA_URL to a full URL pointing at a CDN or storage bucket is common and recommended. It offloads bandwidth from your application servers and enables edge caching.
Can I use the same storage backend for static and media? Technically yes, but keep them in separate buckets or prefixes. Static files have different caching policies and deployment lifecycles than user uploads.