Build Tailwind CSS for Django in Production
Build a minified Tailwind CSS file for Django releases: scanning templates and Python code, the standalone CLI without Node, Docker build stages, collectstatic hashing, and missing-class bugs.
Problem statement
Tailwind CSS generates only the utility classes your project uses, by scanning source files for class names. In development, a watcher rebuilds the stylesheet as you edit templates. In production, you need one minified file built from the exact templates being deployed.
When that step is missing or misconfigured, the symptoms are specific: pages render unstyled because the built file was never deployed, some elements lose their styles because Tailwind never saw their class names, or visitors keep a stale stylesheet because the URL did not change. This guide builds Tailwind as part of the release and avoids those failures.
- Keep a small input file (
@import "tailwindcss";plus@sourcelines) in your static source directory. - Build with
--minifyinto a file inside aSTATICFILES_DIRSdirectory. - Make sure every template directory, and any Python code that emits class names, is scanned.
- Build during CI or in a Docker build stage; the standalone CLI avoids needing Node.
- Run
collectstaticwith manifest storage so the CSS gets a hashed filename and long cache lifetime. - Never generate class names by string concatenation; write complete class names so the scanner can find them.
Step-by-step solution
1. Input file
With Tailwind v4, configuration lives in CSS:
/* assets/css/input.css */
@import "tailwindcss";
@source "../../templates";
@source "../../*/templates";
@source "../../*/forms.py";
@source "../../*/templatetags";
Tailwind v4 detects sources automatically in many projects, skipping files ignored by Git. Explicit @source lines make the scanned paths obvious and cover app-level template directories and Python files that contain class names, such as form widget attributes. Paths are relative to the input file.
Tailwind v3 projects use tailwind.config.js with a content array instead; the same rule applies: list every place class names appear.
2. Build command
With the npm package:
npm install -D tailwindcss @tailwindcss/cli
npx @tailwindcss/cli -i assets/css/input.css -o static_src/css/site.css --minify
With the standalone executable (no Node required), download the binary for your platform from the Tailwind CSS releases page, pin the version, and run:
./tailwindcss -i assets/css/input.css -o static_src/css/site.css --minify
The django-tailwind-cli package wraps the standalone CLI in management commands (python manage.py tailwind build), which is convenient for teams that prefer to keep tooling in Python.
3. Django settings and template
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static_src"]
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}">
With manifest storage, {% static %} renders css/site.4f2a91c3e7b0.css. The name changes whenever the content changes, so browsers can cache it for a year and still get new styles immediately after a deploy.
Add static_src/css/site.css to .gitignore if you build it in CI; commit it only if your deploy process has no build step.
4. Build in Docker
FROM python:3.12-slim AS base
WORKDIR /app
FROM base AS css
ARG TAILWIND_VERSION=v4.1.13
ADD https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-x64 /usr/local/bin/tailwindcss
RUN chmod +x /usr/local/bin/tailwindcss
COPY . .
RUN tailwindcss -i assets/css/input.css -o static_src/css/site.css --minify
FROM base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
COPY --from=css /app/static_src/css/site.css static_src/css/site.css
RUN DJANGO_SETTINGS_MODULE=config.settings.build python manage.py collectstatic --noinput
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
Pin the Tailwind version you have tested (the value above is an example) and choose the binary that matches the build platform (linux-arm64 for ARM builders). Copying the whole project into the CSS stage ensures every template is scanned.
For non-container deploys, run the same command in the deploy script before collectstatic, or in CI with the result shipped in the release artifact.
5. Avoid missing classes
The scanner reads files as plain text and extracts anything that looks like a class name. It cannot run code, so this fails:
{# Bad: "bg-red-500" never appears as a complete string #}
<div class="bg-{{ color }}-500">
Write complete names and choose between them:
{% if level == "error" %}
<div class="bg-red-500 text-white">
{% else %}
<div class="bg-green-500 text-white">
{% endif %}
or map values to full class names in Python:
BADGE_CLASSES = {
"error": "bg-red-500 text-white",
"ok": "bg-green-500 text-white",
}
Keep that Python file inside a scanned path. For classes that come from the database or an external source, safelist them explicitly (in v4, with an inline source such as @source inline("bg-red-500 bg-green-500");).
Verification check:
ls -lh staticfiles/css/site.*.css
curl -s https://example.com/ | grep -o '/static/css/site\.[a-f0-9]*\.css'
curl -sI https://example.com/static/css/site.4f2a91c3e7b0.css | grep -i cache-control
Expect a compact file (often tens of kilobytes before compression), a hashed URL in the page, and a long cache lifetime. Load a few pages that use rarely used components and check that they are styled.
Explanation
Tailwind's build is a function of your source files: it scans them for candidate class names and emits CSS only for those it recognises. That keeps the stylesheet small, but it means the build must run against the same templates that are deployed, and every class must appear literally in a scanned file. Manifest static storage then makes the output safe to cache aggressively by giving each version a unique URL.
Edge cases / notes
Third-party app templates
Templates shipped inside installed packages (in site-packages) are not scanned by default. If you style them with Tailwind classes, add an @source pointing at the package directory, or override the templates in your project.
Development workflow
Run the CLI with --watch during development, or python manage.py tailwind watch with django-tailwind-cli. Do not rely on a development build for production; always build with --minify in the release process.
When to automate this
Put the build in CI and fail the pipeline if the command fails or the output is unexpectedly small (a sign the scanner found no sources). A visual regression test on a few key pages catches missing classes that a size check cannot.
Next steps
- For a JavaScript bundler setup, see Deploy Django with Vite Frontend Assets.
- Understand hashed filenames in Long-Lived Caching for Django Static Files.
- Put the build into a multi-stage image with How to Dockerize a Django App for Production.
- If the stylesheet does not load at all, see Django Static Files Not Loading in Production: Fix Guide.
FAQ
Why are some Tailwind classes missing in production?
The class name never appears as a complete string in a scanned file, often because it is built dynamically in a template or lives in a file outside the source paths.
Do I need Node.js to use Tailwind with Django?
No. The standalone Tailwind CLI is a single executable. django-tailwind-cli wraps it in management commands.
Should I commit the built Tailwind CSS file?
Prefer building it in CI or during the image build. Commit it only if your deploy process cannot run a build step.
Why do users see old styles after a deploy?
The CSS URL did not change. Use manifest static storage so {% static %} produces a hashed filename for each version.