Static Files & Media

Difficulty: Advanced

Django distinguishes between static files (CSS, JavaScript, images that ship with your code) and media files (user-uploaded content like profile pictures and documents). Each has different configuration, storage, and serving strategies.

Static files are configured with STATIC_URL (the URL prefix, like '/static/') and STATICFILES_DIRS (additional directories to search). Each app can have its own static/ directory. During development, Django's staticfiles app serves files directly. For production, 'python manage.py collectstatic' copies all static files from app directories and STATICFILES_DIRS into STATIC_ROOT - a single directory that a web server can serve efficiently.

Whitenoise is a popular middleware for serving static files in production without a separate web server. It compresses files, adds cache headers, and serves them directly from Python. While not as fast as Nginx, it is simpler to deploy and works well for small to medium applications. Install it with 'pip install whitenoise' and add it to MIDDLEWARE.

Media files are user-uploaded content stored in MEDIA_ROOT and served at MEDIA_URL. In development, Django can serve media files using the static() helper in urls.py. In production, media files should be served by Nginx, Apache, or a cloud storage service like AWS S3.

For cloud deployments, django-storages provides backends for AWS S3, Google Cloud Storage, and Azure Blob Storage. This lets Django upload media files directly to cloud storage and serve them through a CDN. This is the recommended approach for production applications because it separates file storage from application servers.

Manifest static files storage (ManifestStaticFilesStorage) appends a hash to static file names (e.g., style.abc123.css). This enables aggressive browser caching because the file name changes whenever the content changes, forcing browsers to download the new version. Whitenoise includes its own manifest storage.

Code examples

Static Files Configuration

# settings.py
import os

# URL prefix for static files
STATIC_URL = '/static/'

# Additional directories for static files (beyond app static/ dirs)
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),  # Project-level static files
]

# Where collectstatic copies files to (production only)
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# Static file finders (default)
STATICFILES_FINDERS = [
    'django.contrib.staticfiles.finders.FileSystemFinder',  # STATICFILES_DIRS
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',  # App static/ dirs
]

# Directory structure:
# project/
#     static/                  # Project-level (in STATICFILES_DIRS)
#         css/main.css
#         js/app.js
#     blog/
#         static/              # App-level (found by AppDirectoriesFinder)
#             blog/
#                 style.css
#     staticfiles/             # STATIC_ROOT (created by collectstatic)

STATIC_URL is the URL prefix. STATICFILES_DIRS lists project-level static directories. STATIC_ROOT is where collectstatic copies everything for production. App-level static files go in app/static/appname/.

Whitenoise for Production

# pip install whitenoise

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # After SecurityMiddleware
    # ... rest of middleware
]

# Use compressed and cached static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

# Whitenoise automatically:
# - Serves static files efficiently
# - Compresses files with gzip and brotli
# - Adds cache headers (immutable, max-age=1 year)
# - Hashes file names for cache busting

# Collect static files before deployment:
# python manage.py collectstatic --noinput

Whitenoise serves static files directly from Python. CompressedManifestStaticFilesStorage adds hashes and compression. Place WhiteNoiseMiddleware right after SecurityMiddleware.

Media Files Configuration

# settings.py
import os

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# urls.py (development only)
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ... your URL patterns
]

# Serve media files in development
if settings.DEBUG:
    urlpatterns += static(
        settings.MEDIA_URL,
        document_root=settings.MEDIA_ROOT,
    )

# In templates:
# <img src="{{ product.image.url }}" alt="{{ product.name }}">

# In models:
# class Product(models.Model):
#     image = models.ImageField(upload_to='products/%Y/%m/')

MEDIA_ROOT stores uploaded files on the filesystem. MEDIA_URL is the URL prefix. In development, static() serves files. In production, use Nginx or cloud storage. upload_to can use date-based subdirectories.

AWS S3 Storage

# pip install django-storages boto3

# settings.py
INSTALLED_APPS = [
    # ...
    'storages',
]

# AWS S3 configuration
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = 'my-project-media'
AWS_S3_REGION_NAME = 'us-east-1'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_DEFAULT_ACL = None
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400',
}

# Media files storage
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/'

# Static files can also use S3
# STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'

django-storages provides S3, GCS, and Azure storage backends. Media files are uploaded directly to S3. Use environment variables for credentials. AWS_DEFAULT_ACL=None uses the bucket's default policy.

Key points

Concepts covered

STATIC_ROOT, STATIC_URL, collectstatic, MEDIA_ROOT, Whitenoise, CDN