PYTHON / DJANGO
Static files, media, and deployment settings
Configure STATIC_URL, STATIC_ROOT, STATICFILES_DIRS, MEDIA_ROOT and DEBUG so assets and uploads work both under runserver and in production.
What you will learn
- Tell apart static sources (STATICFILES_DIRS, app static/) from the STATIC_ROOT target
- Build asset URLs with staticfiles_storage/{% static %} instead of hardcoding /static/
- Serve user uploads from MEDIA_ROOT under MEDIA_URL, never mixed into STATIC_ROOT
- Know what DEBUG=False stops doing: no auto static serving, ALLOWED_HOSTS enforced
Understanding Static files, media, and deployment settings
Django splits asset handling into two unrelated pipelines. Static files are things you wrote and committed: CSS, JS, logos, favicons. They live in each app's static/ directory or in the extra folders you list in STATICFILES_DIRS, and collectstatic copies all of them into the single directory named by STATIC_ROOT. Media files are the opposite: they arrive at runtime from users through FileField and ImageField uploads, get written under MEDIA_ROOT, and are never collected because Django did not put them there.
The URL settings are prefixes, not directories. STATIC_URL="/static/" only tells Django what string to stick in front of a name when {% static %} or staticfiles_storage.url() builds a link; STATIC_ROOT tells collectstatic where to write bytes. That is why {% static 'css/site.css' %} keeps returning /static/css/site.css even when the file does not exist anywhere: the tag does string work with the storage backend, and the actual delivery is somebody else's job. MEDIA_URL and MEDIA_ROOT are the same split for uploads, and FieldFile.url is just MEDIA_URL joined with the value stored in the database column.
In development the staticfiles app quietly patches runserver so it hunts for each request through the finders and streams the file it finds. That behaviour is conditional on DEBUG, so flipping DEBUG=False in the same runserver process makes every stylesheet 404 even though the HTML is unchanged. Production therefore needs two decisions: who serves STATIC_ROOT (nginx, a CDN, or WhiteNoise inside the WSGI app) and who serves MEDIA_ROOT (usually the same web server or object storage). Alongside that, DEBUG=False starts enforcing ALLOWED_HOSTS and stops leaking tracebacks, and a hashing backend such as ManifestStaticFilesStorage rewrites URLs to site.a1b2c3d4.css so browsers can cache assets forever and still see new deploys.
import django
from django.conf import settings
settings.configure(
DEBUG=False,
ALLOWED_HOSTS=["example.com"],
INSTALLED_APPS=["django.contrib.staticfiles"],
STATIC_URL="/static/",
STATIC_ROOT="/srv/app/staticfiles",
MEDIA_URL="/media/",
MEDIA_ROOT="/srv/app/uploads",
)
django.setup()
from django.templatetags.static import static
from django.core.files.storage import FileSystemStorage
print(static("css/site.css"))
media = FileSystemStorage(location=settings.MEDIA_ROOT, base_url=settings.MEDIA_URL)
print(media.url("avatars/ana.png"))
print(media.path("avatars/ana.png"))
print(settings.DEBUG, settings.ALLOWED_HOSTS)
STATIC_URL and MEDIA_URL are URL prefixes used to build links, while STATIC_ROOT and MEDIA_ROOT are filesystem locations that something other than Django must serve in production.
Worked examples
Where the finders actually look
Shows that a static file is resolved from its source directory, not from STATIC_ROOT, and that an unresolvable name simply returns None.
import os
import tempfile
import django
from django.conf import settings
base = tempfile.mkdtemp()
assets = os.path.join(base, "assets", "css")
os.makedirs(assets)
with open(os.path.join(assets, "site.css"), "w") as fh:
fh.write("body { margin: 0 }\n")
settings.configure(
DEBUG=False,
INSTALLED_APPS=["django.contrib.staticfiles"],
STATIC_URL="/static/",
STATIC_ROOT=os.path.join(base, "staticfiles"),
STATICFILES_DIRS=[os.path.join(base, "assets")],
)
django.setup()
from django.contrib.staticfiles import finders
print(finders.find("css/site.css") == os.path.join(assets, "site.css"))
print(finders.find("css/missing.css"))
Example explained
Line 1STATICFILES_DIRS lists source trees, so the name css/site.css is looked up relative to base/assets.
Line 2finders.find returns an absolute filesystem path, which is what collectstatic copies into STATIC_ROOT.
Line 3STATIC_ROOT is never searched by the finders, so pointing it at your source folder is pointless.
Line 4A None result means collectstatic will skip the file and the rendered link will 404 at runtime.
Why a missing collectstatic breaks the whole page
Demonstrates the error ManifestStaticFilesStorage raises when a hashed name is requested but no manifest was ever built.
import os
import tempfile
import django
from django.conf import settings
base = tempfile.mkdtemp()
settings.configure(
DEBUG=False,
INSTALLED_APPS=["django.contrib.staticfiles"],
STATIC_URL="/static/",
STATIC_ROOT=os.path.join(base, "staticfiles"),
STORAGES={
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
},
},
)
django.setup()
from django.contrib.staticfiles.storage import staticfiles_storage
try:
print(staticfiles_storage.url("css/site.css"))
except ValueError as exc:
print("ValueError:", exc)
Example explained
Line 1The manifest backend reads staticfiles.json from STATIC_ROOT to translate site.css into its hashed name.
Line 2collectstatic writes that manifest, so without it the lookup table is empty.
Line 3The backend is strict by default and raises instead of guessing, which surfaces as a 500 on every template using {% static %}.
Line 4The fix is running collectstatic as part of the deploy, not switching the storage backend back.
Important notes
{% static %} and storage.url() return URLs; if you need an on-disk path use finders.find() for static sources or storage.path() for media, and never reverse-engineer one from the other.
MEDIA_URL must not be a prefix of STATIC_URL or vice versa, otherwise one location shadows the other in your web server rules and uploads can be served from your asset path.
Common mistakes
Putting STATIC_ROOT inside a STATICFILES_DIRS entry: collectstatic then treats its own output as a source, so it raises ImproperlyConfigured or endlessly re-collects hashed copies of hashed files.
Writing uploads into the static directory (MEDIA_ROOT under STATIC_ROOT): the next collectstatic --clear or redeploy deletes every user file, and uploaded content ends up served from a path you also treat as trusted code.
Setting DEBUG=False on runserver and concluding static files are broken: the URLs in the HTML are still correct, but nothing is serving STATIC_ROOT because the staticfiles view only activates while DEBUG is True.
Try it yourself
Change, predict, then run
Configure settings with STATIC_URL="/assets/" and MEDIA_URL="/uploads/", then print staticfiles_storage.url("js/app.js") next to a FileSystemStorage url for "docs/report.pdf", and confirm the two prefixes are independent of the directories on disk.
Open the Python workspaceCheck your understanding
A site works locally, then you set DEBUG=False and keep using runserver. The HTML still contains /static/css/site.css but the browser gets a 404. What is happening?
- The staticfiles app only serves files automatically while DEBUG is True, so nothing answers /static/ until collectstatic plus nginx or WhiteNoise takes over
- STATIC_URL is ignored when DEBUG is False, so {% static %} produced an unusable link
- ALLOWED_HOSTS rejects requests for /static/ paths once DEBUG is False
- STATICFILES_DIRS is only read when DEBUG is True, so the file was never found
Show answer
Serving static files from Django is a development convenience wired up only when DEBUG is True; with it off, the request reaches no view. Option 2 is tempting because the symptom looks like a bad URL, but the 404 proves the URL was built correctly from STATIC_URL and simply has nothing behind it.