Deploy Django with Vite Frontend Assets
Ship Vite-built JavaScript and CSS with Django in production: manifest-based asset tags with django-vite, a Node build stage, collectstatic, cache headers, and avoiding dev-server leaks.
Problem statement
Many Django projects now use Vite to bundle JavaScript and CSS: TypeScript, component frameworks, or just modern tooling for a few interactive pages. In development, templates load assets from the Vite dev server with hot reload. In production, the dev server must not exist, assets must be built once with hashed filenames, and templates must reference those exact files.
The common failures are pages that still point at localhost:5173, a missing or mislocated manifest.json, collectstatic not picking up the build output, Node installed in the production image for no reason, and CSS or JavaScript served with short cache lifetimes. This guide sets up a clean build-and-deploy path.
- Configure Vite with
build.manifest, a fixedoutDir, explicitrollupOptions.input, andbaseequal to yourSTATIC_URLplus any prefix. - Use
django-vitetemplate tags; set itsdev_modefromDEBUGso production reads the manifest. - Add Vite's output directory to
STATICFILES_DIRSsocollectstaticincludes the bundles. - Build assets in CI or a Docker build stage with
npm ci && npm run build; do not ship Node in the runtime image. - Serve the collected files with long-lived cache headers (WhiteNoise or Nginx).
- Verify the rendered HTML contains no
localhost:5173and all asset URLs return200.
Step-by-step solution
1. Vite configuration
// frontend/vite.config.js
import { defineConfig } from "vite";
import { resolve } from "node:path";
export default defineConfig({
base: "/static/",
build: {
manifest: "manifest.json",
outDir: resolve(__dirname, "dist"),
emptyOutDir: true,
rollupOptions: {
input: {
main: resolve(__dirname, "src/main.js"),
admin_extras: resolve(__dirname, "src/admin-extras.js"),
},
},
},
server: {
origin: "http://localhost:5173",
},
});
manifest: "manifest.json"writes the manifest todist/manifest.json. Recent Vite versions put it indist/.vite/manifest.jsonwhenmanifest: true, which is a common reason Django cannot find it.base: "/static/"makes Vite generate URLs (for chunks, fonts, and images referenced from CSS) under Django'sSTATIC_URL. If you serve static files from a CDN,basemust match the final URL prefix.- Explicit inputs give each entry point a stable name that templates refer to.
2. Django settings
pip install django-vite
# settings/base.py
INSTALLED_APPS += ["django_vite"]
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "frontend" / "dist"]
DJANGO_VITE = {
"default": {
"dev_mode": DEBUG,
"manifest_path": BASE_DIR / "frontend" / "dist" / "manifest.json",
}
}
The configuration format has changed between django-vite major versions (older versions used flat DJANGO_VITE_* settings), so match the documentation for the version you install.
3. Template tags
{% load django_vite %}
<!doctype html>
<html>
<head>
{% vite_hmr_client %}
{% vite_asset 'src/main.js' %}
</head>
<body>...</body>
</html>
With dev_mode on, these tags point at the Vite dev server with hot module replacement. With dev_mode off, vite_hmr_client renders nothing and vite_asset reads the manifest, emitting a <script type="module"> tag for the hashed bundle plus <link> tags for its CSS and preloads for imported chunks.
4. Build in a Docker stage
# Stage 1: build frontend assets
FROM node:22-slim AS assets
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Python runtime
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
COPY --from=assets /app/frontend/dist /app/frontend/dist
RUN DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput
USER nobody
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
Add frontend/node_modules and frontend/dist to .dockerignore so local builds do not leak into the image. The final image contains the built files but no Node, node_modules, or source maps you did not intend to publish.
For non-Docker deploys, run npm ci && npm run build in CI, then ship frontend/dist with the release artifact, or run the build on the server before collectstatic if Node is installed there.
5. Static storage and caching
Vite already puts content hashes in filenames. With ManifestStaticFilesStorage (or WhiteNoise's compressed manifest storage), Django adds its own hash as well; that works, but Django also rewrites url() references inside CSS and fails if a referenced file is missing. If Vite's CSS references assets that Django cannot resolve, make sure they are in the build output and that base matches STATIC_URL. Alternatively, since Vite already hashes its files, use a storage without Django's manifest step (such as WhiteNoise's CompressedStaticFilesStorage), accepting that Django's own static files, such as the admin's, are then not hashed.
Serve everything under /static/ with long cache lifetimes:
location /static/ {
alias /srv/myapp/current/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
}
WhiteNoise sets far-future cache headers for files with hashed names automatically.
6. Guard against dev-mode leaks
Add a test that renders a page with production settings and checks for dev-server URLs:
from django.test import TestCase, override_settings
class ViteProductionTest(TestCase):
@override_settings(DEBUG=False)
def test_no_dev_server_urls(self):
html = self.client.get("/").content.decode()
self.assertNotIn("localhost:5173", html)
self.assertNotIn("@vite/client", html)
Because django-vite reads dev_mode at start-up from settings, a test that toggles DEBUG alone may not flip it; point dev_mode at an environment variable or test with a production settings module in CI.
Verification check:
curl -s https://example.com/ | grep -Eo '(src|href)="/static/[^"]+"' | head
curl -sI https://example.com/static/assets/main-3b1c9a0d.js | grep -iE "^(HTTP|cache-control|content-type)"
Expect hashed filenames, 200, application/javascript (or text/javascript), and a long Cache-Control lifetime.
Explanation
Vite's manifest is a JSON map from source entry points to built files, including the CSS and chunks each entry imports. django-vite uses it at render time to emit the correct tags, so templates refer to stable source names while browsers receive hashed, cacheable files. collectstatic then treats Vite's output like any other static directory, which keeps the production path identical to a plain Django project: one STATIC_ROOT, served by WhiteNoise, Nginx, or a CDN.
Edge cases / notes
Source maps
Production source maps help error tracking but expose source code if published. Either do not build them, or upload them to your error tracker and exclude them from the static directory.
Multiple frontends
django-vite supports several named app configurations, each with its own manifest, which is useful when the admin area and the public site are separate Vite projects.
Rollback
Because assets are part of the release (image or artifact), rolling back code rolls back assets too. Avoid building assets on the server in place, which breaks this pairing.
When to automate this
Build assets in CI once per commit and include them in the artifact that is deployed everywhere. Add the dev-server leak test and a post-deploy check that fetches the entry bundle's URL from the rendered page.
Next steps
- Understand hashed filenames and caching in Long-Lived Caching for Django Static Files.
- Serve assets from the app with How to Serve Django Static Files with WhiteNoise.
- Keep images small with Build Smaller, Faster Django Docker Images.
- If
collectstaticfails on CSS references, see Fix collectstatic Errors in Django Deployments.
FAQ
Why does my production page still load assets from localhost:5173?
django-vite is in dev mode. Set dev_mode to False in production settings, typically by tying it to DEBUG or an environment variable.
Where does Vite put manifest.json?
With manifest: true, recent Vite versions write outDir/.vite/manifest.json. Set manifest: "manifest.json" or point manifest_path at the actual location.
Do I need Node.js on the production server for Vite?
No. Build assets in CI or a Docker build stage and ship only the output.
Why does collectstatic fail after adding Vite?
Manifest storage rewrites url() references in CSS and raises errors for missing files. Make sure referenced assets exist in the build output and that base matches STATIC_URL.