Django serves static files only in development (DEBUG=True). In production, the admin looks unstyled, and CSS returns 404, until you configure static files properly. You also need a plan for media (user uploads). Here’s how both work, with Nginx and with WhiteNoise.

Full deployment: deploy Django with Gunicorn and Nginx.

Static vs media

Type What Settings
Static files Your CSS, JS, images, and the admin’s assets (part of the code) STATIC_URL, STATIC_ROOT, STATICFILES_DIRS
Media files Files uploaded by users at runtime MEDIA_URL, MEDIA_ROOT

1. Settings

# settings.py
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"          # collectstatic writes here
STATICFILES_DIRS = [BASE_DIR / "static"]        # your project's own static sources

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

2. Collect static files on deploy

python manage.py collectstatic --noinput

This copies every app’s static files (including the Django admin) into STATIC_ROOT. Run it on every deploy.

server {
    server_name example.com;

    location /static/ {
        alias /opt/app/staticfiles/;
        expires 30d;
        add_header Cache-Control "public";
        access_log off;
    }

    location /media/ {
        alias /opt/app/media/;
        expires 7d;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;      # gunicorn
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Note the trailing slashes on location /static/ and alias …/. A mismatch is the most common cause of 404s. Nginx must be able to read the files (directories need x permission for the Nginx user along the whole path). See Linux file permissions explained and Nginx static file caching.

3b. Or serve static files with WhiteNoise

WhiteNoise lets Gunicorn serve static files efficiently, with compression and caching headers. It’s handy in Docker, or on PaaS platforms:

pip install whitenoise
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # …
]
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

WhiteNoise doesn’t serve media files. Use Nginx or object storage for uploads.

Cache busting with manifest storage

ManifestStaticFilesStorage (or WhiteNoise’s compressed variant) renames files with a content hash (app.4f2a1c.css), so you can cache them for a year, and browsers still get new versions after deploys:

location /static/ {
    alias /opt/app/staticfiles/;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Media files: security and scaling

  • Never serve user uploads from a path that can execute code, and validate file types and sizes. Set client_max_body_size in Nginx for upload limits. See Nginx 413 errors.
  • Private files (invoices, documents) shouldn’t sit in a public /media/ location. Serve them through a Django view that checks permissions, then hands off to Nginx with X-Accel-Redirect.
  • Multiple servers, or containers: store media in S3-compatible object storage with django-storages, so every instance sees the same files. See MinIO.
  • Back up MEDIA_ROOT. Static files can be regenerated, but uploads can’t. See backup strategy.

Troubleshooting

Symptom Cause
Admin has no styling collectstatic not run, or Nginx alias path wrong
404 on /static/… Trailing slash mismatch; wrong STATIC_ROOT
403 on static files Nginx can’t traverse directories (permissions)
Old CSS after deploy No manifest storage, and long cache headers
Uploads 404 MEDIA_ROOT not served; wrong alias

Quick reference

Task Setting / command
Static destination STATIC_ROOT
Gather files python manage.py collectstatic --noinput
Serve (VPS) Nginx location /static/ { alias …/; }
Serve (app) WhiteNoise middleware
Cache busting Manifest static storage
Uploads at scale django-storages + S3/MinIO

Related: Django production settings and Celery workers.

  • Django
  • Python
  • Nginx