Django 어플리케이션 컨테이너화하기

Django 어플리케이션 컨테이너화하기

이 가이드는 Docker를 사용해 Django 어플리케이션을 컨테이너화하는 방법을 보여줘요. uv로 프로젝트를 스캐폴드하고, Docker Hardened Image를 사용해 프로덕션 준비가 된 Dockerfile을 만든 다음, 개발 스테이지와 Compose Watch를 추가해 빠르게 반복할 거예요.

출처: 문서

본문

사전 요구사항

  • 최신 버전의 Docker Desktop 을 설치했어야 해요.
  • uv 가 설치되어 있거나, Docker를 사용해 로컬 Python이나 uv 설치 없이 프로젝트를 스캐폴드할 수 있어요.

팁: Docker를 처음 접한다면 Docker 기초 가이드 로 시작해 이미지, 컨테이너, Dockerfile 같은 핵심 개념에 익숙해지세요.

개요

이 가이드는 Docker로 Django 어플리케이션을 컨테이너화하는 과정을 안내해요. 끝나면:

  • 로컬 또는 Docker Hardened Image 컨테이너 안에서 uv로 Django 프로젝트를 초기화해요.
  • Docker Hardened Images(DHI) 를 사용해 프로덕션 준비가 된 Dockerfile을 만들어요.
  • Dockerfile에 development 스테이지를 추가하고 자동 코드 동기화를 위해 Compose Watch를 구성해요.

Django 프로젝트 만들기

로컬 uv 설치로 프로젝트를 부트스트랩하거나, 로컬 Python 없이 Dockerfile이 사용하는 것과 같은 DHI 이미지를 사용해 완전히 컨테이너 안에서 부트스트랩할 수 있어요.

로컬(uv)

Python 3.14에 고정된 프로젝트를 초기화하고 그 안으로 이동해주세요:

$ uv init --python 3.14 django-docker
$ cd django-docker

Django와 Gunicorn을 추가한 다음 Django 프로젝트를 스캐폴드해주세요:

$ uv add django gunicorn
$ uv run django-admin startproject myapp .

컨테이너(DHI)

DHI dev 이미지에는 이미 Python 3.14가 있으므로 부트스트랩된 프로젝트가 Dockerfile과 정확히 일치해요.

프로젝트 디렉터리를 만들고 그 안으로 이동해주세요:

$ mkdir django-docker && cd django-docker

한 번의 컨테이너 실행으로 프로젝트를 초기화하고, 의존성을 추가하고, 스캐폴드하세요:

$ docker run --rm -v $PWD:$PWD -w $PWD \
  -e UV_LINK_MODE=copy \
  dhi.io/python:3.14-alpine3.23-dev \
  sh -c "pip install --quiet --root-user-action=ignore uv && uv init --name django-docker --python 3.14 . && uv add django gunicorn && uv run django-admin startproject myapp ."

참고: 이전 명령은 Mac/Linux 셸 구문을 사용해요. Windows에서는 경로를 조정하세요: PowerShell은 ${PWD} , Command Prompt는 %cd% , Git Bash는 $(pwd -W) 와 함께 MSYS_NO_PATHCONV=1 이 필요해요.

내 디렉터리에 이제 다음 파일이 포함되어야 해요:

├── .python-version
├── main.py
├── manage.py
├── myapp/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── pyproject.toml
├── uv.lock
└── README.md

프로덕션 Dockerfile 만들기

Docker Hardened Images는 Docker가 유지 관리하는 프로덕션 준비가 된 베이스 이미지로, 공격 표면을 최소화해요. 자세한 내용은 Docker Hardened Images 를 참고하세요.

DHI 레지스트리에 로그인해주세요:

$ docker login dhi.io

빌드 컨텍스트에서 로컬 아티팩트를 제외하는 .dockerignore 파일을 만들어주세요:

.venv/
__pycache__/
*.pyc
.git/

다음 내용으로 Dockerfile 을 만들어주세요:

# syntax=docker/dockerfile:1
# Build stage: the -dev image includes tools needed to install packages.
FROM dhi.io/python:3.14-alpine3.23-dev AS builder
# Prevent Python from writing .pyc files to disk.
ENV PYTHONDONTWRITEBYTECODE=1
# Prevent Python from buffering stdout/stderr so logs appear immediately.
ENV PYTHONUNBUFFERED=1

RUN pip install --quiet --root-user-action=ignore uv
# Use copy mode since the cache and build filesystem are on different volumes.
ENV UV_LINK_MODE=copy

WORKDIR /app

# Install dependencies into a virtual environment using cache and bind mounts
# so neither uv nor the lock files need to be copied into the image.
RUN --mount=type=cache,target=/root/.cache/uv \
  --mount=type=bind,source=uv.lock,target=uv.lock \
  --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
  uv sync --frozen --no-install-project

# Runtime stage: minimal DHI image with no shell or package manager,
# already runs as the nonroot user.
FROM dhi.io/python:3.14-alpine3.23
# Prevent Python from buffering stdout/stderr so logs appear immediately.
ENV PYTHONUNBUFFERED=1
# Activate the virtual environment copied from the build stage.
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
# Copy the pre-built virtual environment and application source code.
COPY --from=builder /app/.venv /app/.venv
COPY . .
EXPOSE 8000
# Run Gunicorn as the production WSGI server.
CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"]

compose.yaml 파일을 만들어주세요:

services:
  web:
    build: .
    ports:
      - "8000:8000"

어플리케이션 실행하기

django-docker 디렉터리에서 실행해주세요:

$ docker compose up --build

브라우저를 열고 http://localhost:8000 으로 이동해주세요. Django 환영 페이지가 보여야 해요.

ctrl + c 를 눌러 어플리케이션을 중지해주세요.

개발 환경 설정하기

프로덕션 설정은 Gunicorn을 사용하며 코드 변경을 반영하려면 전체 이미지 재빌드가 필요해요. 개발에서는 Django의 내장 서버를 사용하는 development 스테이지를 Dockerfile에 추가하고, 재빌드 없이 실행 중인 컨테이너로 코드 변경을 자동으로 동기화하도록 Compose Watch를 구성할 수 있어요.

Dockerfile 업데이트

production 옆에 development 스테이지를 추가하는 멀티 스테이지 버전으로 Dockerfile 을 교체해주세요:

# syntax=docker/dockerfile:1
# Build stage: the -dev image includes tools needed to install packages.
FROM dhi.io/python:3.14-alpine3.23-dev AS builder
# Prevent Python from writing .pyc files to disk.
ENV PYTHONDONTWRITEBYTECODE=1
# Prevent Python from buffering stdout/stderr so logs appear immediately.
ENV PYTHONUNBUFFERED=1

RUN pip install --quiet --root-user-action=ignore uv
# Use copy mode since the cache and build filesystem are on different volumes.
ENV UV_LINK_MODE=copy

WORKDIR /app

# Install dependencies into a virtual environment using cache and bind mounts
# so neither uv nor the lock files need to be copied into the image.
RUN --mount=type=cache,target=/root/.cache/uv \
  --mount=type=bind,source=uv.lock,target=uv.lock \
  --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
  uv sync --frozen --no-install-project

# The development stage inherits the -dev image and virtual environment from
# the builder. Django's built-in server reloads when Compose Watch syncs files.
FROM builder AS development
ENV PATH="/app/.venv/bin:$PATH"
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

# The production stage uses the minimal runtime image, which has no shell,
# no package manager, and already runs as the nonroot user.
FROM dhi.io/python:3.14-alpine3.23 AS production
# Prevent Python from buffering stdout/stderr so logs appear immediately.
ENV PYTHONUNBUFFERED=1
# Activate the virtual environment copied from the build stage.
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
# Copy only the pre-built virtual environment and application source code.
COPY --from=builder /app/.venv /app/.venv
COPY . .
EXPOSE 8000
# Run Gunicorn as the production WSGI server.
CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"]

Compose 파일 업데이트

다음으로 compose.yaml 을 교체해주세요. development 스테이지를 대상으로 하고, PostgreSQL 데이터베이스를 추가하며, Compose Watch를 구성해요:

services:
  web:
    build:
      context: .
      # Build the development stage from the multi-stage Dockerfile.
      target: development
    ports:
      - "8000:8000"
    environment:
      # Enable Django's verbose debug error pages (the dev server always auto-reloads).
      - DEBUG=1
      # Database connection settings passed to Django via environment variables.
      - POSTGRES_DB=myapp
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_HOST=db
      - POSTGRES_PORT=5432
    # Wait for the database to pass its healthcheck before starting the web service.
    depends_on:
      db:
        condition: service_healthy
    develop:
      watch:
        # Sync source file changes directly into the container so Django's
        # dev server can reload them without a full image rebuild.
        - action: sync
          path: .
          target: /app
          ignore:
            - __pycache__/
            - "*.pyc"
            - .git/
            - .venv/
        # Rebuild the image when dependencies change.
        - action: rebuild
          path: pyproject.toml
        - action: rebuild
          path: uv.lock
  db:
    image: dhi.io/postgres:18
    restart: always
    volumes:
      # Persist database data across container restarts.
      - db-data:/var/lib/postgresql
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    # Expose the port only to other services on the Compose network,
    # not to the host machine.
    expose:
      - 5432
    # Only report healthy once PostgreSQL is ready to accept connections,
    # so the web service doesn't start before the database is available.
    healthcheck:
      test: ["CMD", "pg_isready"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db-data:

sync 작업은 파일 변경을 실행 중인 컨테이너로 직접 밀어넣어 Django의 dev 서버가 자동으로 리로드하게 해요. pyproject.toml 이나 uv.lock 의 변경은 대신 전체 이미지 재빌드를 트리거해요.

참고: Compose Watch에 대해 더 알려면 Compose Watch 사용하기 를 참고하세요.

PostgreSQL 드라이버 추가

프로젝트에 psycopg 어댑터를 추가해주세요:

$ uv add 'psycopg[binary]'

컨테이너(DHI)를 사용한다면:

$ docker run --rm -v $PWD:$PWD -w $PWD \
  -e UV_LINK_MODE=copy \
  dhi.io/python:3.14-alpine3.23-dev \
  sh -c "pip install --quiet --root-user-action=ignore uv && uv add 'psycopg[binary]'"

그런 다음 myapp/settings.py 를 업데이트해 환경 변수에서 DEBUG 와 DATABASES 를 읽도록 해주세요:

import os

DEBUG = os.environ.get('DEBUG', '0') == '1'

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("POSTGRES_DB", "myapp"),
        "USER": os.environ.get("POSTGRES_USER", "postgres"),
        "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "password"),
        "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
        "PORT": os.environ.get("POSTGRES_PORT", "5432"),
    }
}

Compose Watch로 실행

개발 스택을 시작해주세요:

$ docker compose watch

브라우저를 열고 http://localhost:8000 으로 이동해주세요.

myapp/views.py 에 뷰를 추가하는 등 파일을 편집해보세요. Compose Watch가 변경 사항을 컨테이너로 동기화하고 Django의 dev 서버가 자동으로 리로드해요. pyproject.toml 이나 uv.lock 을 업데이트하면 Compose Watch가 전체 이미지 재빌드를 트리거해요.

stop하려면 ctrl + c 를 누르세요.

요약

이 가이드에서:

  • 로컬과 컨테이너화된 설정 옵션 모두에서 uv를 사용해 Django 프로젝트를 부트스트랩했어요.
  • Docker Hardened Images와 uv 의존성 관리를 사용해 프로덕션 준비가 된 Dockerfile을 만들었어요.
  • Dockerfile 에 development 스테이지를 추가하고 PostgreSQL 데이터베이스로 빠른 반복 개발을 위해 Compose Watch를 구성했어요.

관련 정보:

  • Dockerfile 참조
  • Compose 파일 참조
  • Compose Watch 사용하기
  • Docker Hardened Images
  • 멀티스테이지 빌드
  • uv 문서

더 알아보기 (Learn more)

  • Docker Hardened Images
  • Compose Watch
  • Django