I’m building a static Next.js site inside Docker and hit the dreaded
Error: Cannot find module 'tailwindcss'
Require stack: …next/dist/build/webpack/config/blocks/css/plugins.js
Below is the exact path I took from panic ➜ root cause ➜ repeatable fix plus a few extra tweaks you can practise once the build works.
I Saw The Error
Inside a multistage Dockerfile I run:
npm run build
and the compiler stops cold with that Tailwind message. Next.js’ font loader calls PostCSS → PostCSS looks for the Tailwind plugin → Node can’t require('tailwindcss')
build fails. Because it fails while compiling, Tailwind is clearly a build-time dependency, not a run-time one.
Why it Happens
npm ci
+ NODE_ENV=production
npm ci
installs what’s in package-lock.json
.
If the env var NODE_ENV
is already production, npm ci
silently skips everything in devDependencies
. My docker-compose.prod.yaml
sets NODE_ENV=production
early, so Tailwind never makes it into the image.
Multi Stage Rules
Even if I did sneak Tailwind into the builder stage, I only copy .next
, public
, and node_modules
into the final stage. That’s fine—but every module the compiler needs must exist before next build
runs.
Explain Code
Fix A keep Tailwind in devDependencies
, force-install dev packages in the builder
# --- builder stage ---
FROM node:20-alpine AS builder
WORKDIR /app
ENV NODE_ENV=development # ← dev deps WILL be installed
COPY package*.json ./
RUN npm ci --include=dev # npm v9 flag (or: npm install)
COPY . .
RUN npm run build
# Strip dev deps to keep the final layer slim
RUN npm prune --omit=dev && npm cache clean --force
Promote Tailwind to regular dependencies
install tailwindcss postcss autoprefixer --save
With Tailwind now in "dependencies"
, the original Dockerfile works because those modules install even when NODE_ENV=production
.
My Safer, Slimmer Multistage Dockerfile
############################
# 1) Builder #
############################
FROM node:20-alpine AS builder
WORKDIR /app
# 1-A copy only lockfile & manifest for cache efficiency
COPY package.json package-lock.json* ./
RUN npm ci --include=dev # all build-time tools present
# 1-B copy source and build
COPY . .
RUN npm run build && npx next telemetry disable
# 1-C remove dev bits
RUN npm prune --omit=dev
############################
# 2) Runner #
############################
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
# copy only what the server needs
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
EXPOSE 8080
CMD ["node_modules/.bin/next", "start", "-p", "8080"]
Checklist Code
# | Add this | Why I like it |
---|---|---|
1 | Bundle Analyzer (npm run analyze ) | Find and shrink huge JS chunks before shipping |
2 | Buildx cache (docker buildx build --cache-to=type=inline ) | Big cuts in CI build time |
3 | next export stage | Serve pure HTML from any CDN—no Node needed |
4 | Health-check route (/healthz + `HEALTHCHECK CMD curl -f | |
5 | Tailwind JIT purge (NEXT_PUBLIC_TAILWIND_MODE=build ) | Final CSS often < 10 KB |
Pick one, rebuild with:
compose --profile prod up --build
and watch size and speed improve.
Define a Code
- Decide where a package lives. Build-time → devDependencies, run-time → dependencies.
- Keep
NODE_ENV
out of the builder stage unless I truly need it. - Prune dev deps after the build or copy only the compiled output.
- Cache smartly—copy
package*.json
first, source code later. - Silence telemetry if I don’t want extra outbound calls (
npx next telemetry disable
).
Final Thought
Once I realised the container simply never got Tailwind in the first place, the fix was trivial: install the right packages at the right stage and keep the runtime image lean. Follow the checklist above and the “Cannot find module ‘tailwindcss’” error (or any cousin of it) stays gon for good.