Static Files¶
For production deployments, we recommend letting Nginx handle static files at the reverse proxy layer — this offloads static traffic entirely so Django-Bolt focuses on API requests. That said, Django-Bolt also serves static files directly from Rust using Actix-files, delivering high-performance static file serving without Python overhead — a solid option when you want a simpler setup without configuring static file handling in your reverse proxy.
Serving user uploads?
This page covers static assets (CSS, JS, admin) from STATIC_ROOT / STATICFILES_DIRS. For user-uploaded media (MEDIA_ROOT), see Media Files — it shares this handler but applies stricter, upload-specific defaults.
Overview¶
Static files are served with:
- Streaming responses - Memory efficient for large files
- ETag and Last-Modified headers - Automatic caching support
- Conditional requests - 304 Not Modified responses
- Range requests - Resumable downloads
- Content-Type detection - Automatic MIME type handling
- Content Security Policy - Configurable CSP headers
Configuration¶
Django-Bolt reads your existing Django static files settings:
# settings.py
# URL prefix for static files
STATIC_URL = "/static/"
# Directory where collectstatic gathers files (primary lookup)
STATIC_ROOT = BASE_DIR / "staticfiles"
# Additional directories to search (optional)
STATICFILES_DIRS = [
BASE_DIR / "static",
]
File lookup order¶
When a static file is requested, Django-Bolt searches in this order:
- STATIC_ROOT - Collected static files (fastest)
- STATICFILES_DIRS - Additional configured directories
- Django staticfiles finders (debug mode only) - App-level static files (e.g., admin)
The first match is returned. Files in STATIC_ROOT take priority.
Finders disabled in production
For security, Django's staticfiles finders fallback is only enabled when DEBUG=True. In production, only files in STATIC_ROOT and STATICFILES_DIRS are served. Run collectstatic to copy app-level static files (like admin assets) into STATIC_ROOT.
Django admin integration¶
In development (DEBUG=True), Django admin static files are automatically served via Django's staticfiles finders — no extra configuration needed.
In production (DEBUG=False), you must run collectstatic so admin assets are copied into STATIC_ROOT:
# Admin works out of the box in development
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.staticfiles",
# ...
]
Security¶
Directory traversal protection¶
Django-Bolt blocks directory traversal attacks:
- Paths containing
..are rejected with400 - Symlink targets are verified to stay within allowed directories
- Only files (not directories) are served
Dotfile deny¶
Any request whose path has a leading-dot component — .env, .git/config, .htaccess, .ssh/id_rsa — returns 404, matching the nginx/Apache default. This keeps stray dotfiles left in STATIC_ROOT from being reachable over HTTP. Backslash-separated components (foo\.env) are caught as well.
nosniff¶
X-Content-Type-Options: nosniff is applied to every response, including 404s, so a crafted miss can't be MIME-sniffed into HTML/JS execution.
User uploads need more
Static files keep their native content types because they're admin-curated. Media uploads are untrusted, so the media handler additionally force-downloads script-bearing files (.html, .svg, .js, …). See Media Files — Security model.
Content Security Policy (CSP)¶
Django-Bolt applies CSP headers to all static file responses. This protects against XSS attacks in HTML files and ensures scripts/styles are loaded only from trusted sources.
Configure CSP using Django 6.0+'s native SECURE_CSP setting:
# settings.py
from django.utils.csp import CSP
SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF],
"style-src": [CSP.SELF, CSP.UNSAFE_INLINE],
"img-src": [CSP.SELF, "data:"],
}
You can also use raw strings:
SECURE_CSP = {
"default-src": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'", "'unsafe-inline'"],
"img-src": ["'self'", "data:"],
}
CSP constants¶
Django provides constants via django.utils.csp.CSP:
| Constant | Value | Purpose |
|---|---|---|
CSP.SELF |
'self' |
Same origin only |
CSP.NONE |
'none' |
Block all resources |
CSP.UNSAFE_INLINE |
'unsafe-inline' |
Allow inline scripts/styles |
CSP.UNSAFE_EVAL |
'unsafe-eval' |
Allow eval() |
CSP.STRICT_DYNAMIC |
'strict-dynamic' |
Trust scripts loaded by trusted scripts |
How CSP works¶
- CSP configuration is read once at server startup
- The header is pre-built for performance (no per-request overhead)
CSP.NONCEvalues are automatically filtered out (static files cannot inject nonces)
Nonces not supported
CSP.NONCE requires per-request nonce injection which isn't possible for static files. Use CSP.NONCE only for dynamic responses served by your Django views.
Common CSP directives¶
| Directive | Purpose | Example |
|---|---|---|
default-src |
Default policy for all content | [CSP.SELF] |
script-src |
JavaScript sources | [CSP.SELF, "https://cdn.example.com"] |
style-src |
CSS sources | [CSP.SELF, CSP.UNSAFE_INLINE] |
img-src |
Image sources | [CSP.SELF, "data:", "https:"] |
font-src |
Font sources | [CSP.SELF, "https://fonts.gstatic.com"] |
connect-src |
XHR/WebSocket destinations | [CSP.SELF, "https://api.example.com"] |
frame-ancestors |
Who can embed this page | [CSP.NONE] |
upgrade-insecure-requests |
Upgrade HTTP to HTTPS | [] (boolean directive) |
block-all-mixed-content |
Block mixed HTTP/HTTPS | [] (boolean directive) |
Using with templates¶
The Django {% static %} template tag works as expected:
{% load static %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<img src="{% static 'img/logo.png' %}" alt="Logo">
<script src="{% static 'js/app.js' %}"></script>
</body>
</html>
Caching¶
Set BOLT_STATIC_MAX_AGE (seconds) to emit a Cache-Control header on successful static responses:
This produces Cache-Control: public, max-age=31536000. Static uses public (unlike media's private) because assets are identical for every user, so CDNs and shared proxies can cache them aggressively.
- The header is only set on
2xxresponses — a404is never cached with a longmax-age. - A missing or unset
BOLT_STATIC_MAX_AGEmeans noCache-Controlheader — this is the default and emits no warning. Startup warnings are only emitted for present-but-invalid values (non-integer, boolean, or negative), which are ignored. - This is independent of the ETag / Last-Modified validators, which are always sent.
Performance¶
Static file serving runs primarily in Rust:
- No Python GIL for configured directories - Files in
STATIC_ROOTandSTATICFILES_DIRSare served without touching Python - Django finders fallback (debug mode only) - App-level static files (like admin) require a Python call; disabled in production for security
- Async I/O - Non-blocking file reads via Actix
- Smart read modes - Sync reads for small files (<256KB), async for large files
- Efficient caching - Proper HTTP caching headers reduce server load
Production deployment¶
For production, collect static files (including admin assets) into STATIC_ROOT:
Then run Django-Bolt:
Static files are served from STATIC_ROOT with async I/O, ETag caching, and range request support.
Troubleshooting¶
Static files not found¶
-
Verify
STATIC_ROOTis set and contains files: -
Check
STATIC_URLmatches your requests: -
For development without collectstatic, ensure
STATICFILES_DIRSis configured.
Admin styles missing¶
Ensure django.contrib.staticfiles is in INSTALLED_APPS and run collectstatic:
CSP blocking resources¶
Check browser console for CSP violations. Adjust your CSP directives to allow required sources: