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.

Tailwind in a Django releaseTemplates and codeclass namesTailwind buildminified CSScollectstatichashed filenameBrowsercached for a yearTailwind emits only classes it finds in scanned files, so scan every template path.
  1. Keep a small input file (@import "tailwindcss"; plus @source lines) in your static source directory.
  2. Build with --minify into a file inside a STATICFILES_DIRS directory.
  3. Make sure every template directory, and any Python code that emits class names, is scanned.
  4. Build during CI or in a Docker build stage; the standalone CLI avoids needing Node.
  5. Run collectstatic with manifest storage so the CSS gets a hashed filename and long cache lifetime.
  6. 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.

the input file decides what gets scanned@import "tailwindcss";@source "../../templates";@source "../../*/templates";@source "../../*/forms.py";@source "../../*/templatetags";1231Tailwind v4 is configured in CSS; this one line replaces the oldtailwind.config.js entry point.2App-level template folders. Paths are relative to this input file, not tomanage.py.3Python that emits class names, such as widget attrs, must be scanned too orthose classes vanish.Anything not scanned produces no CSS, so a missing path means unstyled elementsdjango-deployment.com
Explicit @source lines make the scanned paths reviewable instead of implicit.

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">
the scanner reads text, it cannot run templatesBuilt at render time✕class="bg-{{ color }}-500"✕the full name never appears in a file✕no CSS generated, element unstyledComplete names chosen by logic✓{% if %} picks "bg-red-500" or"bg-green-500"✓or a Python dict of full class stringssafelist names that come from thedatabaseWrite every class name out in full somewhere the build scansdjango-deployment.com
The template renders fine locally only when a development build happened to include the class.

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

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.

Keep going