Django static files
last modified November 13, 2026 Python 3.15
In this article we work with static files in Django: stylesheets, scripts,
and images. We configure STATIC_URL, STATICFILES_DIRS,
and STATIC_ROOT, load files with the {% static %} tag,
collect them with collectstatic, and create file names with content hashes. The
examples use Django 6.1 and Python 3.15.
Static files
Static files are the files that the server sends as they are, without
processing: CSS, JavaScript, images, and fonts. They differ from the pages that views
generate and from media files, which are uploaded by users at run
time. Django handles static files with the django.contrib.staticfiles
app, which is installed by default. It finds the files in the apps and in the
project, serves them during development, and gathers them into one directory for
deployment.
The example project
The project is created with uv; the steps are explained in
Django introduction. The commands below create a project
directory, install Django, generate the project with the configuration package
config, and start the app landing.
$ mkdir staticfiles $ cd staticfiles $ uv init --bare $ uv add django $ uv run django-admin startproject config . $ uv run python manage.py startapp landing
The trailing dot in startproject config . creates manage.py and the
config package in the current directory instead of one level deeper. A new app is
ignored by Django until it is listed in INSTALLED_APPS, so we add
the app to the list in config/settings.py; the other
entries are generated by startproject.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'landing',
]
Every listing in this article is a file of the application, shown with a solid blue border and the label Project file.
The app landing has one page. The page uses a stylesheet that belongs to the app,
and a script and an image that belong to the project. We create the directories for the
templates and the static files first.
$ mkdir -p landing/templates/landing landing/static/landing static/js static/img
The finished project is laid out as follows; the next sections create the files.
.
├── config
│ ├── settings.py
│ └── urls.py
├── landing
│ ├── static
│ │ └── landing
│ │ └── style.css
│ ├── templates
│ │ └── landing
│ │ └── index.html
│ ├── urls.py
│ └── views.py
├── manage.py
└── static
├── img
│ └── logo.svg
└── js
└── app.js
Static files settings
Three settings control the static files. The generated
settings.py contains only STATIC_URL; we add the other two.
STATIC_URL = 'static/' STATICFILES_DIRS = [BASE_DIR / 'static'] STATIC_ROOT = BASE_DIR / 'staticfiles'
STATIC_URL is the URL prefix under which the static files are
served; the generated value static/ is relative to the site root, so
the files are available at /static/.... STATICFILES_DIRS
lists additional directories with project-wide static files, such as our
static directory. STATIC_ROOT is the directory where the
collectstatic command copies all files for deployment; it is not used
by the development server, and it must be different from the directories in
STATICFILES_DIRS.
How files are found
Django finds the files with finders, listed in the
STATICFILES_FINDERS setting. By default there are two:
FileSystemFinder searches the STATICFILES_DIRS directories,
and AppDirectoriesFinder searches the static
subdirectory of every installed app. As with templates, all files share one
namespace, so an app puts its files in a subdirectory named after the app:
landing/static/landing/style.css is addressed as
landing/style.css. Without the extra directory, two apps with a
style.css would clash and the first one would win.
body {
font-family: sans-serif;
margin: 2rem;
}
.logo {
color: #0c4b33;
background: url('../img/logo.svg') no-repeat left center;
padding-left: 80px;
}
The stylesheet colors the heading and puts an image behind it. The image is
referenced with the relative path ../img/logo.svg; the browser
resolves it against the URL of the stylesheet, so it points to
/static/img/logo.svg.
The static template tag
Templates should not contain hard-coded static URLs. The
{% static %} tag builds the URL from STATIC_URL and
the relative path of the file. It lives in the static library, which
we load with {% load static %} at the top of the template (or in the
child templates; the load is not inherited). With the manifest storage, shown
later, the tag also inserts the hashed name.
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Static files</title>
<link rel="stylesheet" href="{% static 'landing/style.css' %}">
</head>
<body>
<h1 class="logo">Static files</h1>
<img src="{% static 'img/logo.svg' %}" alt="Logo" width="32">
<script src="{% static 'js/app.js' %}"></script>
</body>
</html>
The page loads the stylesheet from the app, the script and the image from the
project's static directory. The view only renders the template.
from django.shortcuts import render
def index(request):
return render(request, 'landing/index.html')
The view only renders the template. The URLconf of the app routes the root URL to it, and the URLconf of the project includes the one of the app.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
This is the whole routing of the app: one route for the page.
from django.urls import include, path
urlpatterns = [
path('', include('landing.urls')),
]
The script of the project only writes a message to the browser console, so that we can see that the browser loaded it.
console.log('static files work');
The image is a small SVG logo that the stylesheet uses as a background.
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"> <circle cx="32" cy="32" r="28" fill="#0c4b33"/> </svg>
These are all the files of the example; the static files are served by the development server.
The static files are served during development without any configuration, as
long as DEBUG is True and the
django.contrib.staticfiles app is installed: runserver
uses its own static file handler. The page below shows the URLs produced by the
tag.
$ curl -s http://127.0.0.1:8000/ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Static files</title> <link rel="stylesheet" href="/static/landing/style.css"> </head> <body> <h1 class="logo">Static files</h1> <img src="/static/img/logo.svg" alt="Logo" width="32"> <script src="/static/js/app.js"></script> </body> </html>
Serving files in development
The development server finds a requested file with the finders, so the files can be edited and reloaded without any copy step. We request the stylesheet from the app and the image from the project directory; the first command prints the response headers and the file, the second one only the status line and the content type. The server sets the content type from the file extension.
$ curl -s -i http://127.0.0.1:8000/static/landing/style.css
HTTP/1.1 200 OK
Date: Fri, 13 Nov 2026 20:08:27 GMT
Server: WSGIServer CPython/3.15.2
Content-Type: text/css
Content-Length: 173
Content-Disposition: inline; filename="style.css"
Last-Modified: Fri, 13 Nov 2026 20:08:25 GMT
body {
font-family: sans-serif;
margin: 2rem;
}
.logo {
color: #0c4b33;
background: url('../img/logo.svg') no-repeat left center;
padding-left: 80px;
}
$ curl -s -i http://127.0.0.1:8000/static/img/logo.svg | grep -i -E "^(HTTP|content-type)"
HTTP/1.1 200 OK
Content-Type: image/svg+xml
findstatic
The findstatic command shows which file Django uses for a
relative path. It is the first tool to try when a static file does not load. With
-v 2 it also lists the directories that were searched. A name that does
not exist is reported as not found.
$ uv run python manage.py findstatic landing/style.css img/logo.svg Found 'landing/style.css' here: ./landing/static/landing/style.css Found 'img/logo.svg' here: ./static/img/logo.svg $ uv run python manage.py findstatic nothing.css No matching file found for 'nothing.css'.
collectstatic
In production the Django process should not serve the static files; a web
server such as Nginx or a specialized middleware does it much faster. For that,
the collectstatic command copies the files from all finders into the
STATIC_ROOT directory, so the web server needs to know only one
location. The admin site ships its own static files, which is why the copy
contains more than our three. We use --noinput to skip the
confirmation prompt.
$ uv run python manage.py collectstatic --noinput 133 static files copied to './staticfiles'.
The collected files
We list the top level of the target directory and then our own files. The directory tree of the admin files is not shown.
$ ls staticfiles admin img js landing $ find staticfiles -type f -not -path "staticfiles/admin/*" | sort staticfiles/img/logo.svg staticfiles/js/app.js staticfiles/landing/style.css
Hashed file names
Browsers cache static files. After a deployment, users may keep seeing an old
stylesheet, unless the file name changes with its content. The
ManifestStaticFilesStorage backend appends a hash of the content to
every collected file name, such as style.5f2c0b4d7a9e.css, and writes
the mapping to a staticfiles.json manifest. The
{% static %} tag reads the manifest and emits the hashed names, so
the files can be cached forever; a changed file gets a new name. The backend also
rewrites the url() references inside the CSS files. The storage is
configured with the STORAGES setting. We keep it in a separate settings
module that imports the development settings and changes what differs in
production.
from .settings import *
DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
},
'staticfiles': {
'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage',
},
}
The settings module turns off DEBUG and allows the local host
names. The staticfiles entry of STORAGES selects the
manifest storage. The --settings option selects the module for one
command. The command below collects the files again; the output tells how many
files were copied and how many were post-processed, which means that their
names were hashed and their references were rewritten.
$ uv run python manage.py collectstatic --noinput --clear --settings=config.settings_prod 133 static files deleted, 133 static files copied to './staticfiles', 133 post-processed.
The manifest
Our own files now have hashed names. The staticfiles.json manifest
maps the original names to the hashed ones. The listing shows the files outside
of the admin directory and the paths entries of the manifest for our
files.
$ find staticfiles -type f -not -path "staticfiles/admin/*" | sort
staticfiles/img/logo.eada26879de7.svg
staticfiles/img/logo.svg
staticfiles/js/app.fc885cc87057.js
staticfiles/js/app.js
staticfiles/landing/style.9eee754dafd7.css
staticfiles/landing/style.css
staticfiles/staticfiles.json
$ python3 -c "import json; m = json.load(open('staticfiles/staticfiles.json'))['paths']; [print(k, '->', m[k]) for k in ('landing/style.css', 'js/app.js', 'img/logo.svg')]"
landing/style.css -> landing/style.9eee754dafd7.css
js/app.js -> js/app.fc885cc87057.js
img/logo.svg -> img/logo.eada26879de7.svg
The rewritten stylesheet
The url('../img/logo.svg') reference in the collected
stylesheet was replaced with the hashed name of the image, so the cached image
also changes when the file changes. We print the collected stylesheet; the
cat command expands the pattern to the hashed file name.
$ cat staticfiles/landing/style.*.css
body {
font-family: sans-serif;
margin: 2rem;
}
.logo {
color: #0c4b33;
background: url("../img/logo.eada26879de7.svg") no-repeat left center;
padding-left: 80px;
}
The static tag with the manifest
With the production settings the {% static %} tag returns the
hashed names. A server with DEBUG = False does not serve static
files by itself; the --insecure option of runserver
forces the static handler. We start such a server on port 8001, and, as a
stand-in for a real web server, the http.server module of the standard
library on port 8002, which serves the STATIC_ROOT directory.
The page links to the hashed files. The hashed file is not found by the
development static handler: it searches the source directories with the finders
and knows only the original names. The file is found by the stand-in web server,
because it is present in STATIC_ROOT; it serves the directory at
its root, while a real server would map the /static/ prefix to the
directory. This is the reason why the hashed names require a real web server in
production.
$ curl -s http://127.0.0.1:8001/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Static files</title>
<link rel="stylesheet" href="/static/landing/style.9eee754dafd7.css">
</head>
<body>
<h1 class="logo">Static files</h1>
<img src="/static/img/logo.eada26879de7.svg" alt="Logo" width="32">
<script src="/static/js/app.fc885cc87057.js"></script>
</body>
</html>
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8001/static/landing/style.9eee754dafd7.css
404
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://127.0.0.1:8002/landing/style.9eee754dafd7.css
200 text/css
Serving static files in production
In production the web server answers the requests for /static/
directly from the STATIC_ROOT directory, and passes the other
requests to Django. With Nginx, a location /static/ block with an
alias to the directory is enough; the hashed names allow a long
expires header. When there is no separate web server, a package
such as WhiteNoise serves the files from the application. The usual deployment
step is to run collectstatic after each release.
Two details are easy to forget. The STATIC_URL must end with a
slash, and the user running the web server needs read
permission for STATIC_ROOT. Do not put the directory under version
control; it is generated.
Testing static files
The tests check that the static() helper (the function behind
the tag) builds the URL, that the finders locate both the app file and the
project file, and that the page contains the links. The test client does not
serve the files themselves; the test runner runs with DEBUG = False
and the staticfiles handler is part of runserver only.
from django.contrib.staticfiles import finders
from django.templatetags.static import static
from django.test import TestCase
from django.urls import reverse
class StaticFilesTests(TestCase):
def test_static_helper_builds_url(self):
self.assertEqual(static('landing/style.css'), '/static/landing/style.css')
def test_finders_locate_app_and_project_files(self):
self.assertIsNotNone(finders.find('landing/style.css'))
self.assertIsNotNone(finders.find('img/logo.svg'))
self.assertIsNone(finders.find('missing.css'))
def test_page_links_to_static_files(self):
response = self.client.get(reverse('index'))
self.assertContains(response, 'href="/static/landing/style.css"')
self.assertContains(response, 'src="/static/js/app.js"')
The finders.find() function returns the absolute path of the file
or None. Tests of the real delivery of static files need a live
server; Django provides StaticLiveServerTestCase for that.
$ uv run python manage.py test Found 3 test(s). System check identified no issues (0 silenced). Creating test database for alias 'default'... ... ---------------------------------------------------------------------- Ran 3 tests in 0.006s OK Destroying test database for alias 'default'...
Source
Django documentation: Managing static files
Django documentation: The staticfiles app
Django documentation: Deploying static files
In this article we have configured the static files of a Django project, loaded
them with the {% static %} tag, found them with
findstatic, gathered them with collectstatic, and
created hashed file names with the manifest storage. The pages that use the files are
built with Django templates; the following
articles describe the database layer, beginning with
Django models.
Author
List all Django tutorials.