Django и Tailwind CSS: стратегия многослойной сборки для легкого Docker‑образа

Для бэкенд‑разработчика CSS часто становится вечной задачей. Когда мы писали style.css вручную, а потом открыли Bootstrap, мы воскликнули от радости. Но с каждым обновлением и кастомизацией Bootstrap становился всё более болезненным.

Сейчас в моде Tailwind CSS. Утилитарный подход, где стили задаются только классами, почти спасает бэкенд‑инженеров. Я сам полностью убрал Bootstrap из проекта и перешёл на Tailwind, и теперь считаю его обязательным инструментом для современных веб‑разработок, особенно в Django.

В этой статье я расскажу, как в Django‑проекте использовать Tailwind CSS и одновременно оптимизировать Docker‑образ для продакшн‑развертывания, особенно как сделать его лёгким с помощью многослойной сборки.


Проблема: рост размера Docker‑образа



Для интеграции Tailwind в Django чаще всего используют пакет django‑tailwind. Он хорошо документирован и легко устанавливается, поэтому в локальной среде проблем нет.

Проблема возникает на этапе развертывания, особенно в Docker‑контейнере.

В локальной среде или на bare‑metal сервере можно просто собрать CSS:

python manage.py tailwind build

Но этот процесс требует Node.js и npm. Поэтому в продакшн‑образе Django приходится ставить Node.js, что неэффективно.

  • Проблема 1: Для однократной сборки CSS в runtime‑образе нужно включать Node.js.
  • Проблема 2: Размер образа растёт ненужно.
  • Проблема 3: Включение лишних бинарных файлов ухудшает безопасность.

Наша цель проста.

"В чистый Python‑runtime без Node.js, только собранные CSS‑файлы"

Для этого используем многослойную сборку Docker.


Решение: 3‑ступенчатая многослойная сборка

Многослойная сборка позволяет разделить процесс на три этапа:

  1. Python Builder: сборка Python‑пакетов в Wheel.
  2. Node Builder: компиляция Tailwind CSS (используем npm).
  3. Final Runtime: копируем только нужные файлы в финальный образ.

Таким образом финальный образ содержит только то, что нужно для запуска, без Node.js и инструментов сборки.


Пример Dockerfile



Ниже пример Dockerfile для Django + Tailwind. Путь и имена файлов можно подстроить под ваш проект (например, приложение theme, расположение статических файлов и т.д.).

# -------------------------------------------------------------------
# Stage 1: Python Builder
# Устанавливаем зависимости и собираем пакеты в Wheel
# -------------------------------------------------------------------
FROM python:3.11-slim as python-builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --upgrade pip && \
    pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt


# -------------------------------------------------------------------
# Stage 2: Node Builder
# Используем Node‑образ для компиляции Tailwind CSS
# -------------------------------------------------------------------
FROM node:20-alpine as node-builder

WORKDIR /app

# Копируем весь проект, чтобы Tailwind JIT мог сканировать шаблоны и Python‑файлы
COPY . .

WORKDIR /app/theme/static_src

# Устанавливаем зависимости и запускаем сборку
RUN npm ci
RUN npm run build

# Предположим, что результат находится в /app/theme/static/css/dist.css


# -------------------------------------------------------------------
# Stage 3: Final Runtime
# Финальный образ без Node.js, только Python и статические файлы
# -------------------------------------------------------------------
FROM python:3.11-slim

WORKDIR /app

# 1. Устанавливаем пакеты из Stage 1
COPY --from=python-builder /app/wheels /wheels
COPY --from=python-builder /app/requirements.txt .
RUN pip install --no-cache-dir /wheels/*

# 2. Копируем исходный код
COPY . .

# 3. Копируем только собранный CSS из Stage 2
COPY --from=node-builder /app/theme/static/css/dist.css \
    /app/theme/static/css/dist.css

# 4. Собираем статические файлы Django
RUN python manage.py collectstatic --noinput

# Запуск
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]

Дополнительные оптимизации:

  • Используйте .dockerignore, чтобы исключить лишние файлы (например, .git, локальные медиа).
  • Добавьте переменные окружения PYTHONDONTWRITEBYTECODE=1, PYTHONUNBUFFERED=1.
  • При использовании Alpine‑образов устанавливайте только необходимые системные библиотеки.

Ключевые моменты

1. Разделение ролей в Node‑builder

В Stage 2 мы занимаемся только сборкой CSS. Важно, чтобы Tailwind мог видеть все шаблоны и Python‑файлы, поэтому в примере копируем весь проект. Если проект большой и content Tailwind ограничен, можно копировать только нужные каталоги (static_src, шаблоны и т.д.) и соответствующим образом настроить tailwind.config.js.

2. Скрипт npm run build

В package.json, созданном django‑tailwind, обычно есть скрипт:

{
  "scripts": {
    "build": "tailwindcss -c tailwind.config.js -o ../static/css/dist.css --minify"
  }
}

В многослойной сборке вы можете просто вызвать npm run build или npm run build:prod. Главное знать, где находится результат, чтобы корректно скопировать его в Stage 3.

3. Легкость и безопасность финального образа

В Stage 3 отсутствуют npm, node, tailwindcss CLI и другие инструменты сборки. В итоге:

  • Размер образа уменьшается.
  • Уменьшается поверхность атаки.
  • Контейнер содержит только то, что нужно для выполнения.

Это приводит к более быстрым CI/CD‑пайплайнам, экономии места в реестре и упрощённой проверке безопасности.


process of build runtime docker image

Итоги

При сборке Docker‑образов часто хочется просто «сделать всё, чтобы работало». Но в продакшн‑окружении важны размер, время сборки и безопасность. Используя описанную 3‑ступенчатую стратегию, вы:

  • Ограничиваете зависимость от Node.js только до этапа сборки.
  • Оставляете в финальном образе чистый Python‑runtime и собранные статические файлы.
  • Делаете развертывание более лёгким, предсказуемым и безопасным.

Если у вас уже есть работающий проект, попробуйте применить эту схему в следующем пайплайне. После того как вы разберётесь с настройками, вы сможете быстро использовать её и в новых проектах.