Tailwind in a Django Dockerfile
Introduction
In this post, we will explore how to integrate Tailwind CSS into a Django Dockerfile without requiring Node.js in the final image. This approach is particularly useful when your project does not use any JavaScript frameworks,therefore eliminating the need for Node.js. By adding a Tailwind CSS compile stage to our Dockerfile, we can streamline our setup and maintain a lightweight final image. Let's dive in and see how it's done!
If you're deciding between Django and FastAPI for the project this Dockerfile belongs to, see Django vs FastAPI for a comparison. If you're also adding dynamic UI updates, Dynamically Loading Django Messages with HTMX is a natural next step.
Prerequisites
- A Django project with an existing Dockerfile.
- Familiarity with Tailwind CSS's build process.
- Basic knowledge of Docker and Dockerfile syntax.
Setting Up the Project
-
Using the CDN: This approach is simple but not suitable for production and high-performance websites due to potential latency and dependency on external resources.
-
Using the Tailwind CLI: This method is straightforward but may not work well if you have additional dependencies like daisyUI that require Node.js for building.
-
Using npm (requires Node.js): This is the most robust approach for achieving optimal performance. However, to avoid the overhead and increased Docker image sizes associated with installing Node.js in the final image, we will use a multi-stage Docker build to compile Tailwind CSS without including Node.js in the final image.
Got it. Let's fill in the sections with the appropriate content and provide the related Dockerfile code.
Dockerfile Configuration
Base Image:
We start by defining the base image for our Python environment. For example python:3.12.7-slim-bookworm image, which is a lightweight and efficient base for our Django application.
FROM docker.io/python:3.12.7-slim-bookworm AS python-baseNode.js Build Stage:
Next, we set up a Node.js build stage to handle the Tailwind CSS compilation. We use the node:23.1-bookworm-slim image for this purpose. In this stage, we copy the necessary frontend files, install dependencies using npm ci, and run the Tailwind CSS build process using npx tailwindcss.
FROM node:23.1-bookworm-slim AS node-build-stageInstalling Dependencies:
In the Node.js build stage, we install the necessary dependencies for Tailwind CSS using npm ci. This ensures that we have a clean and reproducible environment for building our CSS.
COPY ./frontend/package*.json ./
RUN npm ciConfiguring Tailwind CSS:
We then copy the Tailwind CSS configuration files (tailwind.config.js and postcss.config.js) and the input CSS file (input.css) into the Node.js build stage.
COPY ./frontend/tailwind.config.js ./
COPY ./frontend/postcss.config.js ./
COPY ./frontend/input.css ./Building Tailwind CSS:
We run the Tailwind CSS build process using npx tailwindcss, specifying the input and output files and enabling minification for production use.
RUN npx tailwindcss \
-i input.css \
-o ./tailwind.css \
--minifyCopy the compiled tailwind to django:
FROM python-base AS python-run-stage
#
# Others
#
COPY --from=node-build-stage /src/tailwind.css ${APP_HOME}/path/to/static/compiled/
ENTRYPOINT ["some/entrypoint"]
Final step & Important step:
The final image should be:
FROM docker.io/python:3.12.7-slim-bookworm AS python-base
FROM node:23.1-bookworm-slim AS node-build-stage
WORKDIR /src
# assuming your fronend files are at ./frontend
COPY ./frontend/package*.json ./
RUN npm ci
# these are needed for tailwind build
COPY ./frontend/tailwind.config.js ./
COPY ./frontend/postcss.config.js ./
COPY ./frontend/input.css ./
# Copy the templates for tailwind to work
COPY ./dir/to/templates/ ./templates/
COPY ./dir/to/static/js/ ./js/
# This builds the tailwind's minified version
RUN npx tailwindcss \
-i input.css \
-o ./tailwind.css \
--minify
FROM python-base AS python-run-stage
#
# Others
#
COPY --from=node-build-stage /src/tailwind.css ${APP_HOME}/path/to/static/compiled/
ENTRYPOINT ["some/entrypoint"]
Important step!
You should add this line to your docker compose if you have a volume that mounts your project dir to container. This causes the docker to remove the compiled tailwind as it does not exist in your local files but it does exist inside the docker.
services:
django:
.
.
.
volumes:
- .:/app:z # This mounts your local files inside docker(for live reload)
- /app/path/to/static/compiled # ignore the compiled tailwindConclusion
In this tutorial, we explored how to integrate Tailwind CSS into a Django Dockerfile without requiring Node.js in the final image. By leveraging multi-stage builds, we were able to compile Tailwind CSS in a separate Node.js build stage and then copy the compiled CSS into the final Python image. This approach helps maintain a lightweight and efficient Docker image, streamlining the development workflow and reducing the complexity of managing multiple Docker images. We hope this guide helps you achieve a more efficient setup for your Django projects.
Additional Resources
FAQs
1. Why not use the CDN for Tailwind CSS?
The Tailwind CDN build ships the entire framework unminified and generates styles in the browser at runtime, which isn't suitable for production: no tree-shaking of unused classes, no minification, and an external dependency that adds latency and a point of failure your app doesn't control. For a real deployment you want Tailwind's classes purged down to only what your templates actually use and compiled ahead of time, which means running the Tailwind build step, not the CDN script.
2. Can I use Tailwind CLI without Node.js?
The Tailwind CLI binary itself doesn't strictly require Node.js at runtime (standalone builds exist), but the typical project setup — package.json, plugins, a tailwind.config.js — assumes an npm-based workflow. That's exactly the problem this post's multi-stage Dockerfile solves: run the Node.js-based Tailwind build in an early build stage, then copy only the compiled CSS output into the final image, so the shipped container never needs Node.js installed at all.
3. How do I handle additional Tailwind CSS plugins?
Install them as normal npm dependencies (added to package.json) and register them in tailwind.config.js, the same as you would outside Docker. The only Docker-specific detail is making sure npm install and the Tailwind build both run inside the Node.js build stage of the Dockerfile, before that stage's output gets copied into the final, Node-free image — if a plugin is missing from that stage, the build fails there, not silently in production.
4. What if I need to update my Tailwind CSS configuration?
Edit tailwind.config.js (or your source CSS) as you normally would, then rebuild the Docker image. Because the multi-stage build recompiles Tailwind's output fresh in the build stage every time, there's no cached/stale CSS to worry about — the config change takes effect on the next docker build, the same as it would in a non-Docker workflow.
5. How do I ensure my Docker image remains lightweight?
Multi-stage builds are the mechanism: Tailwind compilation (and Node.js, npm, and all the JS tooling it needs) happens in an intermediate build stage that never ships. The final stage only COPYs the compiled CSS file(s) out of that intermediate stage into the production image, so the image you actually deploy contains your app plus a static CSS file — no Node.js runtime, no node_modules, no build tooling.