EthioDeploy
DocsPricingDomainsBlogToolsChangelog

Blog/Fix deploy errors

Django static files 404 in production

BS
Brook SolomonPublished November 7, 2026
Share on XShare on LinkedIn
DEBUG = False

THE SHORT ANSWER

Django only serves static files itself while DEBUG is True. Its docs call that method grossly inefficient and probably insecure, so with DEBUG = False in production every /static/ URL returns 404 and the admin loses its styling. The fix has three parts: set STATIC_ROOT, run python manage.py collectstatic during the build, and serve the collected folder, usually with WhiteNoise added to MIDDLEWARE right after SecurityMiddleware.

Why do static files work locally but 404 in production?

In development, runserver serves files from each app's static/ folder for you, but only while DEBUG = True. Django's docs say this method “is grossly inefficient and probably insecure, so it is unsuitable for production.” Set DEBUG = False, as you should in production, and Django stops answering /static/ URLs. Your pages still render, but the CSS and JavaScript they link to come back 404. The admin is the first place people notice it: it loads as plain, unstyled HTML.

Production needs two things Django doesn't do by default: copy every static file into one folder, and have something serve that folder.

Which settings do I need?

  • STATIC_URL is the URL prefix, such as "static/". It must end in a slash.
  • STATIC_ROOT is the folder collectstatic copies everything into. It defaults to None, and collectstatic refuses to run until you set it.
  • STATICFILES_DIRS lists extra folders to collect from, such as a project-level static/ folder that isn't inside an app. Don't put STATIC_ROOT in this list.
# settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]  # only if you have this folder

Then run python manage.py collectstatic --noinput. It copies your apps' files, the admin's files and anything in STATICFILES_DIRS into staticfiles/. Run it during the build, not by hand, so every deploy ships the files that match its code. Add staticfiles/ to .gitignore.

How do I serve the files with WhiteNoise?

On a platform that runs your app in a container, there is no separate Nginx to point at STATIC_ROOT. WhiteNoise lets the Django process serve the collected files itself, with caching headers and compression. Install it (pip install whitenoise, version 6.12 at the time of writing) and add its middleware directly after SecurityMiddleware:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... the rest
]

STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

The storage backend compresses each file and adds a hash of its contents to the name, such as app.3f2a9c1e.css, so browsers can cache it for a long time and still get the new file after a deploy. Use {% static 'app.css' %} in templates, never a hardcoded /static/app.css, or the hashed name is never used.

To use WhiteNoise in development as well, add "whitenoise.runserver_nostatic" to the top of INSTALLED_APPS.

STATICFILES_STORAGE stopped working?

Django 5.1 removed the STATICFILES_STORAGE setting, after deprecating it in 4.2. Older tutorials still show STATICFILES_STORAGE = "whitenoise.storage..."; on 5.1 and later that line is ignored and you get Django's default storage instead. Move it into STORAGES under the "staticfiles" key, as above. When you define STORAGES, include the "default" entry too, since it replaces the whole default dictionary.

ValueError: Missing staticfiles manifest entry

Once manifest storage is on, a page can fail with a 500 and this in the logs:

ValueError: Missing staticfiles manifest entry for 'css/app.css'

Manifest storage looks up every {% static %} path in staticfiles.json, which collectstatic writes. Django's docs: “If a file isn't found in the staticfiles.json manifest at runtime, a ValueError is raised.” The usual causes:

  • collectstatic didn't run in this deploy, so there is no manifest at all.
  • A typo in the path, or a file that only exists on your machine and was never committed.
  • A file in a folder Django doesn't search, such as a project-level static/ that is missing from STATICFILES_DIRS.

Run python manage.py collectstatic --noinput locally and read its output, or python manage.py findstatic css/app.css to see where Django looks. Setting manifest_strict = False on a storage subclass hides the error, but the file still 404s.

What about uploaded files?

Files users upload go to MEDIA_ROOT, which is not static. WhiteNoise's docs say it “is not suitable for serving user-uploaded media files.” Django's settings docs also require MEDIA_ROOT and STATIC_ROOT to be different folders.

On a container platform, a container's disk is replaced on every deploy, so uploads saved there disappear. Store them in object storage instead (S3 or anything S3-compatible, through django-storages), set as the "default" backend in STORAGES.

Can I just use runserver --insecure?

runserver --insecure forces Django to serve static files with DEBUG = False. It is meant for checking a production-like setup on your machine, it doesn't work with manifest storage, and runserver itself is a development server. In production, run Gunicorn or another WSGI server and serve static files with WhiteNoise or a web server in front.

What happens on EthioDeploy?

EthioDeploy detects Django from manage.py and starts it with migrate and Gunicorn, but it doesn't run collectstatic for you. Set the Build command in your project settings to python manage.py collectstatic --noinput, add WhiteNoise as above, and redeploy.

The rest of the production settings, including ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS and Postgres, are in the Django deploy guide.

Questions

Django's built-in static file serving only runs while DEBUG is True, because its docs call it grossly inefficient and probably insecure. In production you run collectstatic and serve STATIC_ROOT with WhiteNoise or a web server.

The admin's CSS is a static file like any other. With DEBUG = False nothing serves it until you set STATIC_ROOT, run collectstatic and add WhiteNoise or another static file server.

Directly after django.middleware.security.SecurityMiddleware, above every other middleware.

The STORAGES setting. Django 5.1 removed STATICFILES_STORAGE; put the backend under STORAGES['staticfiles']['BACKEND'], and keep a 'default' entry for media files.

Make sure collectstatic runs in every deploy, then check that the file exists, is committed and sits in an app's static folder or a STATICFILES_DIRS folder. findstatic shows where Django looks for it.

Sources

Checked on November 7, 2026:

  • Django: How to manage static files
  • Django: Deploying static files
  • Django: The staticfiles app
  • Django: Settings reference
  • Django 5.1 release notes
  • Django downloads
  • WhiteNoise: Using WhiteNoise with Django
  • WhiteNoise on PyPI

Push a repo. Get a URL.

  • Deploy a Django app
  • Deploy a Python app
  • Troubleshooting deploys
Start free

Join the EthioDeploy Telegram

Release notes as they ship, plus deploy help from the team and other developers.

Join on Telegram
BS
WRITTEN BYBrook SolomonFounder, EthioDeploy

Brook Solomon founded EthioDeploy and builds its deploy pipeline, build servers and AI coding agent. The posts here come from errors seen in real deploys on the platform.

All posts by Brook →
On this page
  • Why do static files work locally but 404 in production?
  • Which settings do I need?
  • How do I serve the files with WhiteNoise?
  • STATICFILES_STORAGE stopped working?
  • ValueError: Missing staticfiles manifest entry
  • What about uploaded files?
  • Can I just use runserver --insecure?
  • What happens on EthioDeploy?
  • Questions
  • Sources
0% read

Keep reading

  • Access-Control-Allow-Origin

    Fix deploy errorsNov 7, 2026

    CORS error after deploying frontend and backend separately

    Once your frontend and API are deployed to different domains, the browser treats every API call as cross-origin and blocks the response unless the API sends an Access-Control-Allow-Origin header naming the frontend's exact origin. It worked locally because the Vite dev proxy, or a single localhost origin, hid the problem. The fix goes on the backend: allow the deployed frontend's origin, with scheme and no trailing slash, answer OPTIONS preflight requests, and never combine credentials with a * origin.

  • /* → index.html

    Fix deploy errorsNov 4, 2026

    404 on page refresh in a React or Vite SPA

    A single-page app returns 404 on refresh because the browser asks the server for a path like /dashboard, and the server has no file at that path. Your router only exists in the JavaScript that index.html loads, so the server has to answer every unknown path with index.html and let the router take over. It works locally because Vite's dev server and vite preview already do that. In production you add one rewrite rule to your host: send every path that isn't a real file to /index.html.

  • heap limit

    Fix deploy errorsNov 3, 2026

    JavaScript heap out of memory during next build

    next build fails with "JavaScript heap out of memory" when the Node.js process running the build fills V8's old-space heap. The heap has a size limit of its own, separate from how much RAM the machine has. The quick fix is to raise that limit for the build only, with --max-old-space-size set below the memory the build machine actually has. The lasting fix is to find which build step uses the memory (bundling, TypeScript or static page generation) and turn down that step: source maps, the type check, or webpack's cache.

EthioDeploy

Agentic first development and deployment, started in Ethiopia, built for developers everywhere.

Telegram community
Product
PricingAI coding agentAgent mode docsDomainsCLIFree backend hostingHosting in EthiopiaChangelog
Deploy guides
Next.jsNode.jsPythonSvelteKitTanStack StartDjangoFastAPIFlaskExpressNestJSLaravelRailsGoTelegram botDiscord botLovableBoltReplitv0Google AI StudioBase44Firebase StudioClaude Code & Cursor
Compare
vs Herokuvs Vercelvs Rendervs Railwayvs Fly.iovs Netlifyvs KoyebHeroku alternativesVercel alternativesNetlify alternativesRender alternativesRailway alternativesFly.io alternativesReplit alternatives
Resources
DocsBlogFree toolsAboutSupportContactllms.txt
© 2026 EthioDeployPay by card in USD or bank transfer in ETBTermsPrivacy