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를 설치할 수 있다:
FROM python:3.12-slim-bookworm
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
또는 인스톨러를 사용해 설치할 수도 있다:
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를 사용해 프로젝트를 관리한다면, 이미지에 프로젝트를 복사하고 설치할 수 있다:
# 프로젝트를 이미지로 복사
ADD . /app
# 프로젝트를 새로운 환경에 동기화하고, 고정된 lockfile 사용
WORKDIR /app
RUN uv sync --frozen
Important
.venv
를 .dockerignore
파일에 추가하는 것이 좋다. 이를 통해 이미지 빌드 시 포함되지 않도록 한다. 프로젝트 가상 환경은 로컬 플랫폼에 의존하므로 이미지 내부에서 처음부터 생성해야 한다.
그런 다음, 기본적으로 애플리케이션을 시작하려면:
# 프로젝트에서 제공하는 `my_app` 커맨드가 있다고 가정
CMD ["uv", "run", "my_app"]
Tip
의존성 설치와 프로젝트 자체를 분리하는 중간 레이어를 사용하면 Docker 이미지 빌드 시간을 단축할 수 있다.
완전한 예제는 uv-docker-example
프로젝트에서 확인할 수 있다.
환경 사용하기¶
프로젝트를 설치한 후, 여러분은 프로젝트의 가상 환경을 _활성화_할 수 있다. 이를 위해 바이너리 디렉토리를 경로의 맨 앞에 추가하면 된다:
ENV PATH="/app/.venv/bin:$PATH"
또는, 환경이 필요한 명령어를 실행할 때 uv run
을 사용할 수도 있다:
RUN uv run some_script.py
Tip
또는, UV_PROJECT_ENVIRONMENT
설정을 동기화 전에 지정하면 시스템 Python 환경에 설치하고 환경 활성화를 완전히 건너뛸 수 있다.
설치된 도구 사용하기¶
설치된 도구를 사용하려면 도구 bin 디렉터리가 경로에 있는지 확인한다:
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
명령을 실행해 확인할 수 있다.
또는 상수 위치로 설정할 수도 있다:
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
를 설정할 수 있다:
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
플래그를 사용한다:
RUN uv sync --compile-bytecode
또는 UV_COMPILE_BYTECODE
환경 변수를 설정해 Dockerfile 내의 모든 명령어가 바이트코드를 컴파일하도록 할 수 있다:
ENV UV_COMPILE_BYTECODE=1
캐싱¶
빌드 성능을 향상시키기 위해 캐시 마운트를 사용할 수 있다:
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
명령어를 실행해 캐시 디렉토리의 위치를 확인할 수 있다.
또는 캐시를 고정된 위치로 설정할 수도 있다:
ENV UV_CACHE_DIR=/opt/uv-cache/
중간 계층¶
프로젝트 관리에 uv를 사용한다면, --no-install
옵션을 통해 이전에 설치된 의존성을 별도의 계층으로 분리해 빌드 시간을 단축할 수 있다.
uv sync --no-install-project
명령어는 프로젝트 자체는 설치하지 않고 프로젝트의 의존성만 설치한다. 프로젝트는 자주 변경되지만 의존성은 일반적으로 정적이기 때문에 이 방법은 상당한 시간을 절약할 수 있다.
# 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 sync
와 uv run
은 모두 --no-editable
플래그를 지원한다. 이 플래그를 사용하면 프로젝트를 비 편집형 모드로 설치할 수 있으며, 소스 코드에 대한 의존성을 제거한다.
다단계 Docker 이미지에서 --no-editable
를 사용하면 한 단계에서 프로젝트를 동기화된 가상 환경에 포함시킨 후, 최종 이미지에 소스 코드가 아닌 가상 환경만 복사할 수 있다.
예시:
# 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가 필요하지 않은 경우, 각 실행 시 바이너리를 마운트할 수 있다:
RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \
uv sync
pip 인터페이스 사용하기¶
패키지 설치¶
컨테이너가 이미 격리되어 있기 때문에 시스템 Python 환경을 사용해도 안전하다. --system
플래그를 사용하면 시스템 환경에 패키지를 설치할 수 있다:
RUN uv pip install --system ruff
기본적으로 시스템 Python 환경을 사용하려면 UV_SYSTEM_PYTHON
변수를 설정한다:
ENV UV_SYSTEM_PYTHON=1
또는 가상 환경을 생성하고 활성화할 수도 있다:
RUN uv venv /opt/venv
# 가상 환경을 자동으로 사용
ENV VIRTUAL_ENV=/opt/venv
# 환경의 진입점을 경로의 맨 앞에 배치
ENV PATH="/opt/venv/bin:$PATH"
가상 환경을 사용할 때는 uv 호출에서 --system
플래그를 생략한다:
RUN uv pip install ruff
요구사항 설치¶
요구사항 파일을 설치하려면 컨테이너에 복사한다:
COPY requirements.txt .
RUN uv pip install -r requirements.txt
프로젝트 설치¶
프로젝트를 요구 사항과 함께 설치할 때는 요구 사항 복사와 소스 코드 복사를 분리하는 것이 좋다. 이렇게 하면 자주 변경되지 않는 프로젝트의 의존성을 프로젝트 자체(매우 자주 변경되는 부분)와 별도로 캐싱할 수 있다.
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
)에 대한 증명을 검증하는 것이 더 좋은 방법이다.