LEARN · DEBUGGING GUIDE

Django Static Files Not Serving in Production

Static files vanishing in production? This guide walks through the real-world causes—from missing collectstatic runs to nginx misconfigurations—and shows how to fix them.

BeginnerPython6 min read

What this usually means

Django's static file serving is designed differently in development vs. production. In development, Django's dev server automatically serves static files from each app's `static/` folder. In production, you must collect all static files into a single directory (STATIC_ROOT) and configure your web server (nginx, Apache) or CDN to serve that directory. When static files aren't served, the root cause is almost always one of: (1) missing or incomplete `collectstatic` run, (2) misconfigured STATIC_ROOT, STATIC_URL, or STATICFILES_DIRS, (3) web server not serving the STATIC_URL path, or (4) file permission issues on the collected files.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check browser console for exact 404 URL: the path after your STATIC_URL prefix tells you where Django expects the file.
  • 2Run `python manage.py collectstatic --dry-run` to see what would be collected without copying.
  • 3Verify STATIC_ROOT directory exists and contains files: `ls -la <STATIC_ROOT>`.
  • 4Check nginx/Apache config: ensure a location block maps STATIC_URL to STATIC_ROOT on disk.
  • 5Test static file directly via full URL: `curl -I https://yourdomain.com/static/css/app.css` — examine response code.
  • 6Check Django logs for any static file related errors; also check web server access/error logs.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchsettings.py — STATIC_URL, STATIC_ROOT, STATICFILES_DIRS, STATICFILES_STORAGE
  • searchnginx.conf (or Apache config) — location block for static files
  • searchProject root — the actual STATIC_ROOT directory and its permissions
  • searchBrowser Developer Tools → Network tab — examine the 404 request and response headers
  • search`python manage.py findstatic <filename>` — see where Django looks for a specific file
  • searchServer logs — /var/log/nginx/error.log or apache error log
  • searchCloud storage console (if using S3/CloudFront) — check bucket policies and file existence
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningSTATIC_ROOT is not set or points to a directory that doesn't exist.
  • warning`collectstatic` hasn't been run after deploying new static files.
  • warningWeb server location block is missing or misconfigured (e.g., wrong alias vs root).
  • warningFile permissions: the web server user (www-data) cannot read static files.
  • warningSTATICFILES_STORAGE points to a non-existent backend (e.g., missing S3 credentials).
  • warningWhiteNoise middleware is not installed or not configured, and the web server is not serving static files.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildSet STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') and run `collectstatic` on deploy.
  • buildAdd a location block in nginx: `location /static/ { alias /path/to/staticfiles/; }`
  • buildEnsure directory permissions: `chown -R www-data:www-data staticfiles/ && chmod -R 755 staticfiles/`
  • buildInstall and configure WhiteNoise: add 'whitenoise.middleware.WhiteNoiseMiddleware' to MIDDLEWARE and set STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'.
  • buildFor S3: configure django-storages properly, ensure bucket policy allows public read, and run collectstatic with proper credentials.
  • buildUse `python manage.py collectstatic --clear` to remove old files before copying fresh ones.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAccess a known static file URL in browser; it should load with 200 status.
  • verifiedCheck the response headers: for nginx, you should see 'Server: nginx' and proper content-type.
  • verifiedRun `curl -I https://yourdomain.com/static/css/app.css` — expect 200 OK.
  • verifiedRun `collectstatic` again and confirm '0 copied' if no changes exist.
  • verifiedCheck page with browser dev tools: no 404 errors for static files.
  • verifiedTest from different network (or incognito) to rule out caching.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningSetting DEBUG=True in production just to serve static files — exposes sensitive info and kills performance.
  • warningUsing STATICFILES_DIRS for production — it's only for development; use STATIC_ROOT.
  • warningForgetting to restart the web server after changing config (nginx -s reload).
  • warningPlacing STATIC_ROOT inside a version-controlled directory that gets cleared on deploy.
  • warningIgnoring the trailing slash in STATIC_URL — must end with '/'.
  • warningAssuming collectstatic works without checking the output for errors (e.g., permission denied).
( 07 )War story

Brand new Django app loads unstyled in production

Junior Backend DeveloperDjango 3.2, nginx, gunicorn, DigitalOcean Ubuntu 20.04

Timeline

  1. 09:00Deployed Django app via git push to production server.
  2. 09:05Opened site – completely unstyled. Browser console shows 404 for /static/css/app.css.
  3. 09:10Checked settings.py: STATIC_URL = '/static/', but STATIC_ROOT is not defined.
  4. 09:12Added STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles'). Ran collectstatic – success, files copied.
  5. 09:15Refreshed site – still no styles. Checked nginx config: location /static/ block points to wrong path.
  6. 09:20Corrected nginx alias to /home/user/project/staticfiles/. Reloaded nginx.
  7. 09:22Refreshed site – styles load correctly. Confirmed with curl 200.
  8. 09:25Added collectstatic to deploy script to prevent future occurrence.

I pushed a new Django app to production, excited to show it off. When I opened the browser, the page was a mess of unstyled HTML – no CSS, no JavaScript. The console showed 404 errors for every static file. I knew Django doesn't serve static files in production by default, but I'd forgotten to configure anything.

I checked settings.py and found STATIC_URL was set but STATIC_ROOT was missing. I added STATIC_ROOT to point to a new 'staticfiles' directory, ran collectstatic, and it copied all files. But the page was still broken. I checked the nginx config and saw the location block for /static/ had an incorrect path – it was pointing to a non-existent directory.

I fixed the nginx alias to match the actual STATIC_ROOT path, reloaded nginx, and refreshed the page. Styles appeared instantly. I added the collectstatic command to my deployment script and wrote a note to always verify nginx config after deploy. Lesson: always double-check the web server's static file mapping.

Root cause

STATIC_ROOT was not defined, and nginx location block pointed to wrong directory.

The fix

Defined STATIC_ROOT in settings.py, ran collectstatic, corrected nginx alias, and reloaded nginx.

The lesson

Static file serving requires three things: STATIC_ROOT set, collectstatic run, and web server configured to serve that path.

( 08 )How Django Static Files Work in Development vs Production

In development (DEBUG=True), Django automatically serves static files from each app's 'static/' directory and any directories listed in STATICFILES_DIRS. This is handled by the 'django.contrib.staticfiles' app and the development server. No additional configuration is needed.

In production (DEBUG=False), Django disables this automatic serving for security and performance reasons. You must collect all static files into a single directory (STATIC_ROOT) using the 'collectstatic' management command. Then, your web server (nginx, Apache) or a middleware like WhiteNoise must serve that directory. If you skip any of these steps, static files will return 404.

( 09 )Common Misconfigurations of STATIC_URL, STATIC_ROOT, and STATICFILES_DIRS

STATIC_URL must end with a slash, e.g., '/static/'. It defines the URL prefix for static files. STATIC_ROOT is an absolute filesystem path where collectstatic copies files. It should be outside your project's version control to avoid clutter. STATICFILES_DIRS is a list of additional directories to search for static files during collectstatic.

A common mistake is setting STATIC_ROOT to the same directory as one of the STATICFILES_DIRS, which causes collectstatic to copy files onto themselves. Another is forgetting to include app-specific static directories in STATICFILES_DIRS if they are not in the default location. Use 'python manage.py findstatic <filename>' to debug where Django looks.

( 10 )Nginx Configuration for Static Files: alias vs root

When configuring nginx to serve static files, you must choose between 'alias' and 'root'. If your STATIC_URL is '/static/' and STATIC_ROOT is '/var/www/staticfiles/', use 'alias': `location /static/ { alias /var/www/staticfiles/; }`. This maps '/static/app.css' to '/var/www/staticfiles/app.css'.

If you accidentally use 'root' instead: `location /static/ { root /var/www/staticfiles/; }`, nginx will look for '/var/www/staticfiles/static/app.css', which doesn't exist. Always use 'alias' when the location path is not the same as the filesystem path.

( 11 )Using WhiteNoise for Self-Serving Static Files

WhiteNoise is a Python middleware that serves static files directly from Django, eliminating the need for a separate web server configuration. It's especially useful for platforms like Heroku that don't allow a separate nginx.

To use WhiteNoise, install it, add 'whitenoise.middleware.WhiteNoiseMiddleware' to MIDDLEWARE (after SecurityMiddleware), and set STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'. Then run collectstatic. WhiteNoise will serve files with proper caching headers and compression. It also handles versioned file names.

( 12 )Cloud Storage (S3, GCS) and CDN Considerations

For large-scale apps, static files are often served from a CDN like Amazon S3 + CloudFront. The 'django-storages' package provides backends for S3, Google Cloud Storage, etc. Configuration includes setting STATICFILES_STORAGE and providing bucket credentials.

Common issues: bucket policy not allowing public read, CORS misconfiguration, or incorrect region. Always test direct S3 URL access. Also ensure your STATIC_URL points to the CDN endpoint. Remember that collectstatic uploads files to the cloud, so it must be run with valid credentials.

Frequently asked questions

Why do static files work in development but not in production?

In development (DEBUG=True), Django's staticfiles app automatically serves files from each app's 'static/' directory. In production (DEBUG=False), this is disabled for security. You must collect static files into a single directory (STATIC_ROOT) and configure your web server to serve that directory.

What does 'collectstatic' actually do?

Collectstatic copies all static files from each app's 'static/' folder and any directories in STATICFILES_DIRS into the directory specified by STATIC_ROOT. It also applies any storage backend transformations (e.g., filename hashing). It does not affect files in development; it's solely for production deployment.

I ran collectstatic and files appear in STATIC_ROOT, but they still 404. Why?

Your web server (nginx, Apache) must be configured to serve the STATIC_ROOT directory at the STATIC_URL path. Check the server config for a location block mapping /static/ to the correct directory. Also ensure file permissions allow the web server user to read the files.

What is WhiteNoise and when should I use it?

WhiteNoise is a Python middleware that serves static files directly from Django, without needing a separate web server. It's ideal for platforms where you can't control the web server (e.g., Heroku). To use it, add the middleware and change STATICFILES_STORAGE. It also provides compression and caching.

How do I debug which file Django is looking for?

Use the `findstatic` management command: `python manage.py findstatic <filename>`. This shows all locations Django searches for that file. Also check the exact URL in the browser's 404 error; the path after STATIC_URL tells you the file name it expects.