Skip to content

Docker에서 uv 사용하기

시작하기

Tip

Docker에서 uv를 사용해 애플리케이션을 빌드할 때의 모범 사례를 확인하려면 uv-docker-example 프로젝트를 참고한다.

uv는 distroless Docker 이미지와 인기 있는 베이스 이미지에서 파생된 이미지를 모두 제공한다. Distroless 이미지는 uv 바이너리만 포함하며, 이를 사용해 여러분의 이미지 빌드에 uv 바이너리를 복사할 수 있다. 반면, 파생된 이미지는 uv가 사전 설치된 운영체제를 포함한다.

예를 들어, Debian 기반 이미지를 사용해 컨테이너에서 uv를 실행하려면 다음과 같이 입력한다:

$ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help

사용 가능한 이미지

다음과 같은 디스트로리스 이미지를 사용할 수 있다:

  • ghcr.io/astral-sh/uv:latest
  • ghcr.io/astral-sh/uv:{major}.{minor}.{patch}, 예: ghcr.io/astral-sh/uv:0.6.2
  • ghcr.io/astral-sh/uv:{major}.{minor}, 예: ghcr.io/astral-sh/uv:0.6 (최신 패치 버전)

그리고 다음과 같은 파생 이미지도 사용할 수 있다:

  • alpine:3.20 기반:
    • ghcr.io/astral-sh/uv:alpine
    • ghcr.io/astral-sh/uv:alpine3.20
  • debian:bookworm-slim 기반:
    • ghcr.io/astral-sh/uv:debian-slim
    • ghcr.io/astral-sh/uv:bookworm-slim
  • buildpack-deps:bookworm 기반:
    • ghcr.io/astral-sh/uv:debian
    • ghcr.io/astral-sh/uv:bookworm
  • python3.x-alpine 기반:
    • ghcr.io/astral-sh/uv:python3.13-alpine
    • ghcr.io/astral-sh/uv:python3.12-alpine
    • ghcr.io/astral-sh/uv:python3.11-alpine
    • ghcr.io/astral-sh/uv:python3.10-alpine
    • ghcr.io/astral-sh/uv:python3.9-alpine
    • ghcr.io/astral-sh/uv:python3.8-alpine
  • python3.x-bookworm 기반:
    • ghcr.io/astral-sh/uv:python3.13-bookworm
    • ghcr.io/astral-sh/uv:python3.12-bookworm
    • ghcr.io/astral-sh/uv:python3.11-bookworm
    • ghcr.io/astral-sh/uv:python3.10-bookworm
    • ghcr.io/astral-sh/uv:python3.9-bookworm
    • ghcr.io/astral-sh/uv:python3.8-bookworm
  • python3.x-slim-bookworm 기반:
    • ghcr.io/astral-sh/uv:python3.13-bookworm-slim
    • ghcr.io/astral-sh/uv:python3.12-bookworm-slim
    • ghcr.io/astral-sh/uv:python3.11-bookworm-slim
    • ghcr.io/astral-sh/uv:python3.10-bookworm-slim
    • ghcr.io/astral-sh/uv:python3.9-bookworm-slim
    • ghcr.io/astral-sh/uv:python3.8-bookworm-slim

디스트로리스 이미지와 마찬가지로, 각 파생 이미지는 uv 버전 태그와 함께 ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}ghcr.io/astral-sh/uv:{major}.{minor}-{base} 형식으로 게시된다. 예를 들어, ghcr.io/astral-sh/uv:0.6.2-alpine이다.

더 자세한 내용은 GitHub Container 페이지를 참고한다.

uv 설치하기

이미 uv가 미리 설치된 이미지를 사용하거나, 공식 distroless Docker 이미지에서 바이너리를 복사해 uv를 설치할 수 있다:

Dockerfile
FROM python:3.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

또는 인스톨러를 사용해 설치할 수도 있다:

Dockerfile
FROM python:3.12-slim-bookworm

# 인스톨러는 curl(과 인증서)이 필요하다
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates

# 최신 인스톨러 다운로드
ADD https://astral.sh/uv/install.sh /uv-installer.sh

# 인스톨러 실행 후 제거
RUN sh /uv-installer.sh && rm /uv-installer.sh

# 설치된 바이너리가 `PATH`에 있는지 확인
ENV PATH="/root/.local/bin/:$PATH"

이 경우 curl이 필요하다.

어떤 방법을 사용하든, 특정 uv 버전을 고정하는 것이 좋다. 예를 들어:

COPY --from=ghcr.io/astral-sh/uv:0.6.2 /uv /uvx /bin/

Tip

위 Dockerfile 예제는 특정 태그를 고정하지만, 특정 SHA256을 고정할 수도 있다. SHA256을 고정하는 것은 태그가 다른 커밋 SHA로 이동할 수 있기 때문에 재현 가능한 빌드가 필요한 환경에서 가장 좋은 방법으로 간주된다.

# 예를 들어, 이전 릴리스의 해시 사용
COPY --from=ghcr.io/astral-sh/uv@sha256:2381d6aa60c326b71fd40023f921a0a3b8f91b14d5db6b90402e65a635053709 /uv /uvx /bin/

또는 인스톨러를 사용할 수도 있다:

ADD https://astral.sh/uv/0.6.2/install.sh /uv-installer.sh

프로젝트 설치

uv를 사용해 프로젝트를 관리한다면, 이미지에 프로젝트를 복사하고 설치할 수 있다:

Dockerfile
# 프로젝트를 이미지로 복사
ADD . /app

# 프로젝트를 새로운 환경에 동기화하고, 고정된 lockfile 사용
WORKDIR /app
RUN uv sync --frozen

Important

.venv.dockerignore 파일에 추가하는 것이 좋다. 이를 통해 이미지 빌드 시 포함되지 않도록 한다. 프로젝트 가상 환경은 로컬 플랫폼에 의존하므로 이미지 내부에서 처음부터 생성해야 한다.

그런 다음, 기본적으로 애플리케이션을 시작하려면:

Dockerfile
# 프로젝트에서 제공하는 `my_app` 커맨드가 있다고 가정
CMD ["uv", "run", "my_app"]

Tip

의존성 설치와 프로젝트 자체를 분리하는 중간 레이어를 사용하면 Docker 이미지 빌드 시간을 단축할 수 있다.

완전한 예제는 uv-docker-example 프로젝트에서 확인할 수 있다.

환경 사용하기

프로젝트를 설치한 후, 여러분은 프로젝트의 가상 환경을 _활성화_할 수 있다. 이를 위해 바이너리 디렉토리를 경로의 맨 앞에 추가하면 된다:

Dockerfile
ENV PATH="/app/.venv/bin:$PATH"

또는, 환경이 필요한 명령어를 실행할 때 uv run을 사용할 수도 있다:

Dockerfile
RUN uv run some_script.py

Tip

또는, UV_PROJECT_ENVIRONMENT 설정을 동기화 전에 지정하면 시스템 Python 환경에 설치하고 환경 활성화를 완전히 건너뛸 수 있다.

설치된 도구 사용하기

설치된 도구를 사용하려면 도구 bin 디렉터리가 경로에 있는지 확인한다:

Dockerfile
ENV PATH=/root/.local/bin:$PATH
RUN uv tool install cowsay
$ docker run -it $(docker build -q .) /bin/bash -c "cowsay -t hello"
  _____
| hello |
  =====
     \
      \
        ^__^
        (oo)\_______
        (__)\       )\/\
            ||----w |
            ||     ||

Note

도구 bin 디렉터리의 위치는 컨테이너에서 uv tool dir --bin 명령을 실행해 확인할 수 있다.

또는 상수 위치로 설정할 수도 있다:

Dockerfile
ENV UV_TOOL_BIN_DIR=/opt/uv-bin/

musl 기반 이미지에 Python 설치하기

uv는 이미지에 사용 가능한 Python 버전이 없을 때 호환되는 Python 버전을 설치한다. 하지만 아직 musl 기반 배포판에 Python을 설치하는 기능은 지원하지 않는다. 예를 들어, Python이 설치되지 않은 Alpine Linux 베이스 이미지를 사용한다면, 시스템 패키지 관리자를 통해 Python을 추가해야 한다:

apk add --no-cache python3~=3.12

컨테이너 내에서 개발하기

개발 과정에서 프로젝트 디렉터리를 컨테이너에 마운트하면 유용하다. 이렇게 설정하면 프로젝트를 변경할 때마다 이미지를 다시 빌드하지 않아도 컨테이너화된 서비스에 즉시 반영된다. 하지만 프로젝트의 가상 환경(.venv)은 마운트에 포함하지 않아야 한다. 가상 환경은 플랫폼에 종속적이며, 이미지를 위해 빌드된 가상 환경을 유지해야 하기 때문이다.

docker run으로 프로젝트 마운트하기

작업 디렉토리의 프로젝트를 /app에 바인드 마운트하면서 .venv 디렉토리를 익명 볼륨으로 유지하려면 다음과 같이 실행한다:

$ docker run --rm --volume .:/app --volume /app/.venv [...]

Tip

--rm 플래그를 추가하면 컨테이너가 종료될 때 컨테이너와 익명 볼륨이 자동으로 정리된다.

완전한 예제는 uv-docker-example 프로젝트에서 확인할 수 있다.

docker compose에서 watch 설정하기

Docker compose를 사용할 때 컨테이너 개발을 위한 더 정교한 도구를 활용할 수 있다. watch 옵션은 바인드 마운트보다 더 세밀한 제어를 제공하며, 파일이 변경될 때 컨테이너화된 서비스를 업데이트하도록 트리거할 수 있다.

Note

이 기능은 Docker Desktop 4.24에 포함된 Compose 2.22.0 이상 버전이 필요하다.

프로젝트 디렉터리를 마운트하면서 프로젝트 가상 환경을 동기화하지 않고, 설정이 변경될 때 이미지를 다시 빌드하도록 Docker compose 파일에서 watch를 설정할 수 있다:

compose.yaml
services:
  example:
    build: .

    # ...

    develop:
      # 앱을 업데이트하기 위한 `watch` 설정 생성
      #
      watch:
        # 작업 디렉터리를 컨테이너의 `/app` 디렉터리와 동기화
        - action: sync
          path: .
          target: /app
          # 프로젝트 가상 환경 제외
          ignore:
            - .venv/

        # `pyproject.toml`이 변경될 때 이미지 다시 빌드
        - action: rebuild
          path: ./pyproject.toml

그런 다음, docker compose watch를 실행해 개발 설정으로 컨테이너를 실행한다.

완전한 예제는 uv-docker-example 프로젝트에서 확인할 수 있다.

최적화

바이트코드 컴파일

Python 소스 파일을 바이트코드로 컴파일하면 설치 시간이 늘어나는 대신 시작 시간이 단축된다. 따라서 프로덕션 이미지에서는 일반적으로 바이트코드 컴파일을 사용하는 것이 좋다.

바이트코드 컴파일을 활성화하려면 --compile-bytecode 플래그를 사용한다:

Dockerfile
RUN uv sync --compile-bytecode

또는 UV_COMPILE_BYTECODE 환경 변수를 설정해 Dockerfile 내의 모든 명령어가 바이트코드를 컴파일하도록 할 수 있다:

Dockerfile
ENV UV_COMPILE_BYTECODE=1

캐싱

빌드 성능을 향상시키기 위해 캐시 마운트를 사용할 수 있다:

Dockerfile
ENV UV_LINK_MODE=copy

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync

기본 UV_LINK_MODE를 변경하면 캐시와 동기화 대상이 서로 다른 파일 시스템에 있어 하드 링크를 사용할 수 없다는 경고를 무시할 수 있다.

캐시를 마운트하지 않는다면 --no-cache 플래그를 사용하거나 UV_NO_CACHE를 설정해 이미지 크기를 줄일 수 있다.

Note

컨테이너 내에서 uv cache dir 명령어를 실행해 캐시 디렉토리의 위치를 확인할 수 있다.

또는 캐시를 고정된 위치로 설정할 수도 있다:

Dockerfile
ENV UV_CACHE_DIR=/opt/uv-cache/

중간 계층

프로젝트 관리에 uv를 사용한다면, --no-install 옵션을 통해 이전에 설치된 의존성을 별도의 계층으로 분리해 빌드 시간을 단축할 수 있다.

uv sync --no-install-project 명령어는 프로젝트 자체는 설치하지 않고 프로젝트의 의존성만 설치한다. 프로젝트는 자주 변경되지만 의존성은 일반적으로 정적이기 때문에 이 방법은 상당한 시간을 절약할 수 있다.

Dockerfile
# uv 설치
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# 작업 디렉토리를 `app` 디렉토리로 변경
WORKDIR /app

# 의존성 설치
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

# 프로젝트를 이미지에 복사
ADD . /app

# 프로젝트 동기화
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

pyproject.toml은 프로젝트 루트와 이름을 식별하는 데 필요하지만, 프로젝트 내용은 최종 uv sync 명령어가 실행될 때까지 이미지에 복사되지 않는다.

Tip

워크스페이스를 사용한다면, 프로젝트 모든 워크스페이스 멤버를 제외하는 --no-install-workspace 플래그를 사용한다.

특정 패키지를 동기화에서 제외하려면 --no-install-package <name>을 사용한다.

비 편집형 설치

기본적으로 uv는 프로젝트와 작업 공간 멤버를 편집 가능한 모드로 설치한다. 이렇게 하면 소스 코드를 변경할 때마다 환경에 바로 반영된다.

uv syncuv run은 모두 --no-editable 플래그를 지원한다. 이 플래그를 사용하면 프로젝트를 비 편집형 모드로 설치할 수 있으며, 소스 코드에 대한 의존성을 제거한다.

다단계 Docker 이미지에서 --no-editable를 사용하면 한 단계에서 프로젝트를 동기화된 가상 환경에 포함시킨 후, 최종 이미지에 소스 코드가 아닌 가상 환경만 복사할 수 있다.

예시:

Dockerfile
# uv 설치
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# 작업 디렉토리를 `app` 디렉토리로 변경
WORKDIR /app

# 의존성 설치
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 --no-editable

# 프로젝트를 중간 이미지에 복사
ADD . /app

# 프로젝트 동기화
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-editable

FROM python:3.12-slim

# 환경만 복사 (소스 코드는 제외)
COPY --from=builder --chown=app:app /app/.venv /app/.venv

# 애플리케이션 실행
CMD ["/app/.venv/bin/hello"]

임시로 uv 사용하기

최종 이미지에서 uv가 필요하지 않은 경우, 각 실행 시 바이너리를 마운트할 수 있다:

Dockerfile
RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \
    uv sync

pip 인터페이스 사용하기

패키지 설치

컨테이너가 이미 격리되어 있기 때문에 시스템 Python 환경을 사용해도 안전하다. --system 플래그를 사용하면 시스템 환경에 패키지를 설치할 수 있다:

Dockerfile
RUN uv pip install --system ruff

기본적으로 시스템 Python 환경을 사용하려면 UV_SYSTEM_PYTHON 변수를 설정한다:

Dockerfile
ENV UV_SYSTEM_PYTHON=1

또는 가상 환경을 생성하고 활성화할 수도 있다:

Dockerfile
RUN uv venv /opt/venv
# 가상 환경을 자동으로 사용
ENV VIRTUAL_ENV=/opt/venv
# 환경의 진입점을 경로의 맨 앞에 배치
ENV PATH="/opt/venv/bin:$PATH"

가상 환경을 사용할 때는 uv 호출에서 --system 플래그를 생략한다:

Dockerfile
RUN uv pip install ruff

요구사항 설치

요구사항 파일을 설치하려면 컨테이너에 복사한다:

Dockerfile
COPY requirements.txt .
RUN uv pip install -r requirements.txt

프로젝트 설치

프로젝트를 요구 사항과 함께 설치할 때는 요구 사항 복사와 소스 코드 복사를 분리하는 것이 좋다. 이렇게 하면 자주 변경되지 않는 프로젝트의 의존성을 프로젝트 자체(매우 자주 변경되는 부분)와 별도로 캐싱할 수 있다.

Dockerfile
COPY pyproject.toml .
RUN uv pip install -r pyproject.toml
COPY . .
RUN uv pip install -e .

이미지 출처 검증

Docker 이미지는 빌드 과정에서 서명되어 출처를 증명한다. 이러한 증명을 통해 이미지가 공식 채널에서 생성되었음을 확인할 수 있다.

예를 들어, GitHub CLI 도구 gh를 사용해 증명을 검증할 수 있다:

$ gh attestation verify --owner astral-sh oci://ghcr.io/astral-sh/uv:latest
Loaded digest sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx for oci://ghcr.io/astral-sh/uv:latest
Loaded 1 attestation from GitHub API

The following policy criteria will be enforced:
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
- Source Repository Owner URI must match:... https://github.com/astral-sh
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Subject Alternative Name must match regex: (?i)^https://github.com/astral-sh/

✓ Verification succeeded!

sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx was attested by:
REPO          PREDICATE_TYPE                  WORKFLOW
astral-sh/uv  https://slsa.dev/provenance/v1  .github/workflows/build-docker.yml@refs/heads/main

이 결과는 특정 Docker 이미지가 공식 uv GitHub 릴리스 워크플로우에 의해 빌드되었으며, 이후 변조되지 않았음을 보여준다.

GitHub 증명은 sigstore.dev 인프라를 기반으로 한다. 따라서 cosign 커맨드를 사용해 uv의 (멀티플랫폼) 매니페스트에 대한 증명 블롭을 검증할 수도 있다:

$ REPO=astral-sh/uv
$ gh attestation download --repo $REPO oci://ghcr.io/${REPO}:latest
Wrote attestations to file sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.jsonl.
Any previous content has been overwritten

The trusted metadata is now available at sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.jsonl
$ docker buildx imagetools inspect ghcr.io/${REPO}:latest --format "{{json .Manifest}}" > manifest.json
$ cosign verify-blob-attestation \
    --new-bundle-format \
    --bundle "$(jq -r .digest manifest.json).jsonl"  \
    --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
    --certificate-identity-regexp="^https://github\.com/${REPO}/.*" \
    <(jq -j '.|del(.digest,.size)' manifest.json)
Verified OK

Tip

이 예제에서는 latest를 사용했지만, 특정 버전 태그(예: ghcr.io/astral-sh/uv:0.6.2) 또는 특정 이미지 다이제스트(예: ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f)에 대한 증명을 검증하는 것이 더 좋은 방법이다.