Django와 Tailwind CSS: Docker 이미지 경량화를 위한 멀티 스테이지 빌드 전략

백엔드 개발자에게 CSS는 영원한 숙제와도 같습니다. 한 땀 한 땀 style.css를 작성하던 시절을 지나 부트스트랩(Bootstrap)이라는 도구를 만났을 때 우리는 환호했습니다. 하지만 유지보수가 거듭될수록 부트스트랩의 정해진 틀을 벗어나는 커스터마이징은 점점 더 고통으로 다가옵니다.

최근의 대세는 단연 Tailwind CSS입니다. 클래스 조합만으로 스타일링을 끝낼 수 있는 유틸리티 퍼스트(Utility-First) 방식은 백엔드 엔지니어에게 가까운 구원에 가깝습니다. 저 역시 기존 프로젝트의 부트스트랩을 모두 걷어내고 Tailwind로 마이그레이션을 진행했을 만큼, 이 도구는 현대 웹 개발, 특히 Django 프로젝트에서 필수적인 요소가 되었습니다.

이 글에서는 Django 프로젝트에서 Tailwind CSS를 사용할 때, 프로덕션(Production) 배포를 위한 Docker 이미지 최적화 전략, 특히 멀티 스테이지 빌드로 Docker 이미지를 가볍게 유지하는 방법을 공유합니다.


문제 상황: Docker 이미지의 비대화



Django 환경에서 Tailwind를 통합하기 위해 가장 많이 사용하는 패키지는 django-tailwind입니다. 설치와 문서화가 잘 되어 있어 로컬 개발 환경에서는 큰 문제가 없습니다.

문제는 배포(Deployment) 단계, 특히 Docker 컨테이너 환경에서 발생합니다.

로컬이나 베어메탈 서버에서는 다음과 같이 간단히 CSS를 빌드할 수 있습니다.

python manage.py tailwind build

하지만 이 과정은 내부적으로 Node.jsnpm 프로세스를 필요로 합니다. 이 때문에 프로덕션용 Django Docker 이미지에까지 Node.js를 설치해야 하는 상황이 생깁니다. 이는 여러 면에서 비효율적입니다.

  • 문제점 1: 단순히 CSS 빌드(사실상 1회성 작업)를 위해 런타임 이미지에 Node.js 런타임이 포함되어야 합니다.
  • 문제점 2: 이미지 크기가 불필요하게 커집니다.
  • 문제점 3: 보안 측면에서도, 실행 시점에 필요 없는 바이너리가 포함되는 것은 좋지 않습니다.

우리의 목표는 명확합니다.

"Node.js가 설치되지 않은 순수한 Python 런타임 이미지에, 빌드된 CSS 파일만 쏙 가져오기"

이를 위해 Docker의 멀티 스테이지 빌드를 활용합니다.


해결책: 3단계 멀티 스테이지 빌드 (Multi-stage Build)

Docker의 멀티 스테이지 빌드 기능을 활용하면 위 문제를 깔끔하게 해결할 수 있습니다. 이미지를 빌드하는 과정을 다음과 같이 3단계로 분리합니다.

  1. Python Builder: Python 패키지를 Wheel 형태로 빌드
  2. Node Builder: Tailwind CSS 컴파일 (npm 사용)
  3. Final Runtime: 위 두 단계의 결과물만 복사하여 최종 실행 이미지 생성

이 방식을 사용하면 최종 이미지에는 Node.js나 빌드 도구 없이, 오직 실행에 필요한 파일만 남게 됩니다.


Dockerfile 예시



아래는 실제 Django + Tailwind 프로젝트를 기준으로 한 Dockerfile 예시입니다. 경로나 파일명은 각자의 프로젝트 구조(theme 앱 이름, 정적 파일 위치 등)에 맞게 조정하시면 됩니다.

# -------------------------------------------------------------------
# Stage 1: Python Builder
# 리눅스 종속성 설치 및 Python 패키지를 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가 Django 템플릿/py 파일을 스캔할 수 있도록
# 전체 프로젝트를 복사합니다.
COPY . .

WORKDIR /app/theme/static_src

# 종속성 설치 및 빌드
# package.json에 정의된 build 스크립트 실행 (보통 tailwind css 빌드 명령)
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. Python Builder에서 빌드된 패키지 설치
COPY --from=python-builder /app/wheels /wheels
COPY --from=python-builder /app/requirements.txt .
RUN pip install --no-cache-dir /wheels/*

# 2. 애플리케이션 소스 코드 복사
COPY . .

# 3. [핵심] Node Builder에서 빌드된 CSS 파일만 가져오기
# django-tailwind 설정에 맞춰 빌드된 CSS 위치를 정확히 지정해야 합니다.
# 여기서는 theme/static/css/dist.css 로 빌드된다고 가정합니다.
COPY --from=node-builder /app/theme/static/css/dist.css \
    /app/theme/static/css/dist.css

# 4. collectstatic 실행
# Django가 Node 빌드 결과물(css)을 포함하여 최종 static files를 수집합니다.
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 빌더 스테이지의 역할 분리

Stage 2에서는 오직 CSS 빌드만을 담당하도록 역할을 분리합니다. 여기서 중요한 것은:

  • Tailwind의 content 설정이 Django 템플릿(templates/**/*.html)이나 Python 코드까지 스캔하는 경우가 많기 때문에,
  • 예시에서는 전체 프로젝트를 복사하여 JIT 모드에서 제대로 클래스를 감지할 수 있게 했습니다.

프로젝트가 매우 크고, Tailwind content 경로가 명확히 한정되어 있다면:

  • static_src, 템플릿 디렉터리 등 빌드에 꼭 필요한 경로만 선별적으로 복사하는 것도 가능합니다.
  • 이 경우 DockerfileCOPY 경로와 tailwind.config.jscontent 설정을 함께 맞춰야 합니다.

2. npm run build 스크립트 활용

django-tailwind 설치 시 생성되는 package.json에는 보통 다음과 같은 빌드 스크립트가 포함됩니다.

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

멀티 스테이지 빌드에서는:

  • npm run build 또는 npm run build:prod와 같이 미리 정의된 스크립트를 그대로 사용하고,
  • 빌드 결과물의 경로만 정확히 알고 있으면 됩니다.

빌드 결과물의 위치는 tailwind.config.js 또는 postcss.config.js 등에서 확인 후, COPY --from=node-builder 경로를 맞춰주면 됩니다.

3. 최종 이미지의 경량화와 보안

Stage 3를 보면 다음과 같은 특징이 있습니다.

  • npm, node, tailwindcss CLI 등은 최종 이미지에 전혀 포함되지 않습니다.
  • Python 패키지 역시 Wheel을 통해 한 번에 설치하여, 빌드 속도와 캐시 활용에 유리합니다.
  • 결과적으로:

  • 이미지 크기가 줄어들고,

  • 공격 표면(attack surface)이 줄어들며,
  • 컨테이너가 “정말로 실행에 필요한 것만 가진” 상태가 됩니다.

장기적으로 보면, 이 작은 차이가:

  • CI/CD 파이프라인 속도,
  • 레지스트리 저장 비용,
  • 보안 점검 시 제거해야 할 요소의 수

등에서 꾸준히 이득을 주는 구조가 됩니다.


process of build runtime docker image

마무리

Docker 이미지를 빌드할 때, “일단 작동하게만 만들자”는 생각으로 필요한 모든 패키지를 하나의 이미지에 때려 넣는 경우가 많습니다. 그러나 프로덕션 환경에서는 이미지 크기, 빌드 시간, 보안이 모두 중요한 요소입니다.

Django와 Tailwind를 함께 사용할 때, 위에서 소개한 3단계 멀티 스테이지 빌드 전략을 적용해 보세요.

  • Node.js 의존성을 빌드 단계에만 한정하고,
  • 최종 이미지는 순수한 Python 런타임 + 정적 결과물만 포함하도록 구성하면,
  • 배포 환경은 더 가볍고, 예측 가능하며, 유지보수도 쉬워집니다.

이미 운영 중인 프로젝트가 있다면, 다음 배포 파이프라인 작업 때 바로 한 번 적용해 보셔도 좋습니다. 한 번 구조를 잡아두면, 이후 새 프로젝트에서도 거의 그대로 재사용할 수 있는 패턴입니다.