🔥 uv 완전 가이드 (3): 패키지 배포, 마이그레이션, Docker 통합

#python#uv#docker#pypi#github-actions
1430자
14분

uv 패키지 배포와 Docker 통합

지난 두 편에서 uv의 기본기와 프로젝트 관리를 다뤘다. 여기까지만 해도 일상적인 Python 개발에는 충분하다. 하지만 실제 프로덕션 환경에서는 패키지를 배포하고, 기존 프로젝트를 마이그레이션하고, Docker 이미지를 빌드하고, CI/CD 파이프라인을 구성해야 한다. 이번 글에서 그 전부를 다룬다.

패키지 빌드와 배포

빌드 시스템 설정

패키지를 배포하려면 pyproject.toml에 빌드 시스템이 정의되어 있어야 한다. 없으면 uv가 레거시 setuptools로 폴백하는데, 이건 권장하지 않는다.

toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

hatchling, flit, setuptools 중 원하는 빌드 백엔드를 쓰면 된다. uv는 자체 빌드 백엔드도 제공하고 있다.

uv build

bash
uv build
bash
uv build

dist/ 디렉토리에 소스 배포판(.tar.gz)과 바이너리 배포판(.whl)이 생긴다:

dist/
├── hello-world-0.1.0-py3-none-any.whl
└── hello-world-0.1.0.tar.gz
dist/
├── hello-world-0.1.0-py3-none-any.whl
└── hello-world-0.1.0.tar.gz

워크스페이스에서 특정 패키지만 빌드하려면:

bash
uv build --package my-lib
bash
uv build --package my-lib

배포 전에는 --no-sources 플래그를 붙여서 빌드하는 게 좋다. tool.uv.sources 없이도 빌드가 되는지 확인하기 위해서다:

bash
uv build --no-sources
bash
uv build --no-sources

버전 관리

uv version 명령으로 버전을 관리할 수 있다:

bash
# 현재 버전 확인
uv version
# hello-world 0.1.0
 
# 정확한 버전으로 변경
uv version 1.0.0
# hello-world 0.1.0 => 1.0.0
 
# 시맨틱 버전 범프
uv version --bump minor
# hello-world 1.0.0 => 1.1.0
 
uv version --bump patch
# hello-world 1.1.0 => 1.1.1
bash
# 현재 버전 확인
uv version
# hello-world 0.1.0
 
# 정확한 버전으로 변경
uv version 1.0.0
# hello-world 0.1.0 => 1.0.0
 
# 시맨틱 버전 범프
uv version --bump minor
# hello-world 1.0.0 => 1.1.0
 
uv version --bump patch
# hello-world 1.1.0 => 1.1.1

프리릴리스 버전도 된다:

bash
# 패치 + 베타
uv version --bump patch --bump beta
# hello-world 1.3.0 => 1.3.1b1
 
# 베타에서 안정 버전으로
uv version --bump stable
# hello-world 1.3.1b2 => 1.3.1
bash
# 패치 + 베타
uv version --bump patch --bump beta
# hello-world 1.3.0 => 1.3.1b1
 
# 베타에서 안정 버전으로
uv version --bump stable
# hello-world 1.3.1b2 => 1.3.1

--dry-run으로 미리 확인할 수도 있다:

bash
uv version 2.0.0 --dry-run
# hello-world 1.0.0 => 2.0.0 (실제 변경 안 됨)
bash
uv version 2.0.0 --dry-run
# hello-world 1.0.0 => 2.0.0 (실제 변경 안 됨)

uv publish

PyPI에 배포하려면:

bash
uv publish
bash
uv publish

인증은 토큰으로 한다. PyPI는 더 이상 사용자명/비밀번호 방식을 지원하지 않는다:

bash
# 토큰 설정
uv publish --token pypi-AgEIcHlwaS5v...
 
# 또는 환경변수로
export UV_PUBLISH_TOKEN=pypi-AgEIcHlwaS5v...
uv publish
bash
# 토큰 설정
uv publish --token pypi-AgEIcHlwaS5v...
 
# 또는 환경변수로
export UV_PUBLISH_TOKEN=pypi-AgEIcHlwaS5v...
uv publish

TestPyPI에 먼저 테스트 배포하고 싶다면, pyproject.toml에 인덱스를 추가한다:

toml
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
toml
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
bash
uv publish --index testpypi
bash
uv publish --index testpypi

배포 후 설치 테스트:

bash
uv run --with hello-world --no-project -- python -c "import hello_world"
bash
uv run --with hello-world --no-project -- python -c "import hello_world"

GitHub Actions에서 Trusted Publishing

GitHub Actions에서 배포하면 토큰 없이도 PyPI에 올릴 수 있다. Trusted Publishing을 쓰면 된다:

yaml
# .github/workflows/publish.yml
name: "Publish"
on:
  push:
    tags:
      - v*
 
jobs:
  run:
    runs-on: ubuntu-latest
    environment:
      name: pypi
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Install uv
        uses: astral-sh/setup-uv@v8
      - name: Install Python 3.13
        run: uv python install 3.13
      - name: Build
        run: uv build
      - name: Smoke test (wheel)
        run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
      - name: Smoke test (source distribution)
        run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
      - name: Publish
        run: uv publish
yaml
# .github/workflows/publish.yml
name: "Publish"
on:
  push:
    tags:
      - v*
 
jobs:
  run:
    runs-on: ubuntu-latest
    environment:
      name: pypi
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Install uv
        uses: astral-sh/setup-uv@v8
      - name: Install Python 3.13
        run: uv python install 3.13
      - name: Build
        run: uv build
      - name: Smoke test (wheel)
        run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
      - name: Smoke test (source distribution)
        run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
      - name: Publish
        run: uv publish

PyPI 프로젝트 설정에서 GitHub를 Trusted Publisher로 등록하는 걸 잊지 말자.

pip에서 마이그레이션

기존에 pip + requirements.txt로 관리하던 프로젝트를 uv로 옮기는 건 생각보다 간단하다.

핵심 차이 이해하기

pip 워크플로우에서 uv로 넘어갈 때 달라지는 점:

pip + pip-toolsuv
requirements.inpyproject.toml[project] dependencies
requirements.txt (잠긴 버전)uv.lock
requirements-dev.txt[dependency-groups] dev
플랫폼별 여러 락파일하나의 유니버설 uv.lock
python -m venv + source .venv/bin/activateuv run (자동 관리)

가장 큰 차이는 uv.lock크로스 플랫폼이라는 점이다. pip-tools에서는 Linux용, macOS용, Windows용 락파일을 따로 만들어야 했다. uv에서는 하나의 락파일이 모든 플랫폼을 커버한다.

마이그레이션 순서

1단계: 프로젝트 초기화

bash
uv init
bash
uv init

2단계: 기존 의존성 가져오기

requirements.in(또는 requirements.txt)에서 가져온다. 기존 잠긴 버전을 유지하고 싶다면 -c 옵션으로 제약을 건다:

bash
uv add -r requirements.in -c requirements.txt
bash
uv add -r requirements.in -c requirements.txt

이렇게 하면 기존 버전이 최대한 유지된 채로 uv.lock이 생성된다.

3단계: 개발 의존성 가져오기

bash
uv add --dev -r requirements-dev.in -c requirements-dev.txt
bash
uv add --dev -r requirements-dev.in -c requirements-dev.txt

requirements-dev.in이 부모 requirements.in-r로 포함하고 있다면, 해당 줄을 제거하고 가져와야 한다:

bash
sed '/^-r /d' requirements-dev.in | uv add --dev -r - -c requirements-dev.txt
bash
sed '/^-r /d' requirements-dev.in | uv add --dev -r - -c requirements-dev.txt

문서 빌드용 의존성 같은 추가 그룹이 있다면:

bash
uv add -r requirements-docs.in -c requirements-docs.txt --group docs
bash
uv add -r requirements-docs.in -c requirements-docs.txt --group docs

4단계: 플랫폼별 제약 처리

기존에 플랫폼별 락파일이 있었다면, 마커를 추가해야 한다:

bash
# Windows용 락파일에 마커 추가
uv pip compile requirements.in -o requirements-win.txt \
  --python-platform windows --no-strip-markers
 
# 모든 플랫폼 제약을 합쳐서 가져오기
uv add -r requirements.in -c requirements-win.txt -c requirements-linux.txt
bash
# Windows용 락파일에 마커 추가
uv pip compile requirements.in -o requirements-win.txt \
  --python-platform windows --no-strip-markers
 
# 모든 플랫폼 제약을 합쳐서 가져오기
uv add -r requirements.in -c requirements-win.txt -c requirements-linux.txt

5단계: 검증

bash
uv sync
uv run pytest  # 테스트가 있다면
bash
uv sync
uv run pytest  # 테스트가 있다면

이제 requirements.in, requirements.txt, requirements-dev.in, requirements-dev.txt 파일들은 삭제해도 된다. pyproject.tomluv.lock이 그 역할을 대신한다.

프로젝트 환경의 차이

pip과 달리 uv는 "활성화된 가상환경" 개념에 의존하지 않는다. 각 프로젝트의 .venv를 자동으로 관리하고, uv run으로 실행하면 항상 올바른 환경에서 명령이 돌아간다.

bash
# pip 방식
source .venv/bin/activate
pytest
 
# uv 방식
uv run pytest
bash
# pip 방식
source .venv/bin/activate
pytest
 
# uv 방식
uv run pytest

uv run을 쓰면 락파일과 환경이 항상 동기화되어 있는 게 보장된다. 가상환경을 직접 활성화해서 쓸 수도 있지만, uv run이 더 안전하다.

Docker 통합

uv와 Docker의 조합은 강력하다. 빌드 속도가 빨라지고, 이미지 크기를 줄이기 위한 최적화 옵션도 풍부하다.

기본 설정

가장 간단한 방법은 공식 distroless 이미지에서 바이너리를 복사하는 거다:

dockerfile
FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
dockerfile
FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/

버전을 꼭 핀하자. latest는 재현성을 깨뜨린다.

uv에서 제공하는 파생 이미지를 직접 사용할 수도 있다:

dockerfile
FROM ghcr.io/astral-sh/uv:0.11.7-python3.12-trixie-slim
dockerfile
FROM ghcr.io/astral-sh/uv:0.11.7-python3.12-trixie-slim

Alpine, Debian, Python 버전별로 다양한 이미지가 준비되어 있다.

프로젝트 설치

dockerfile
FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
 
WORKDIR /app
COPY . /app
 
ENV UV_NO_DEV=1
RUN uv sync --locked
 
CMD ["uv", "run", "my_app"]
dockerfile
FROM python:3.12-slim-trixie
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
 
WORKDIR /app
COPY . /app
 
ENV UV_NO_DEV=1
RUN uv sync --locked
 
CMD ["uv", "run", "my_app"]

.dockerignore.venv를 반드시 추가하자. 로컬 가상환경을 이미지에 넣으면 플랫폼 불일치로 문제가 생긴다.

레이어 최적화 (핵심)

의존성과 프로젝트 코드를 분리하면 빌드 캐시를 훨씬 효율적으로 쓸 수 있다:

dockerfile
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
 
WORKDIR /app
 
# 1단계: 의존성만 먼저 설치 (캐시 활용)
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 --locked --no-install-project
 
# 2단계: 프로젝트 코드 복사 후 설치
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked
dockerfile
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
 
WORKDIR /app
 
# 1단계: 의존성만 먼저 설치 (캐시 활용)
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 --locked --no-install-project
 
# 2단계: 프로젝트 코드 복사 후 설치
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked

의존성이 바뀌지 않으면 1단계가 캐시되고, 코드만 변경되면 2단계만 다시 실행된다. 빌드 시간이 극적으로 줄어든다.

멀티스테이지 빌드

프로덕션 이미지에서 소스 코드를 제외하고 싶다면:

dockerfile
# 빌드 스테이지
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
 
ENV UV_PYTHON_DOWNLOADS=0
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 --locked --no-install-project --no-editable
 
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-editable
 
# 프로덕션 스테이지
FROM python:3.12-slim
COPY --from=builder /app/.venv /app/.venv
 
CMD ["/app/.venv/bin/my_app"]
dockerfile
# 빌드 스테이지
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11.7 /uv /uvx /bin/
 
ENV UV_PYTHON_DOWNLOADS=0
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 --locked --no-install-project --no-editable
 
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked --no-editable
 
# 프로덕션 스테이지
FROM python:3.12-slim
COPY --from=builder /app/.venv /app/.venv
 
CMD ["/app/.venv/bin/my_app"]

--no-editable를 쓰면 소스 코드에 대한 의존성 없이 패키지가 설치되니까, .venv만 복사하면 된다.

추가 최적화

바이트코드 컴파일 — 시작 시간을 줄인다:

dockerfile
ENV UV_COMPILE_BYTECODE=1
RUN uv sync --locked
dockerfile
ENV UV_COMPILE_BYTECODE=1
RUN uv sync --locked

캐시 마운트 — 빌드 간 캐시를 유지한다:

dockerfile
ENV UV_LINK_MODE=copy
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked
dockerfile
ENV UV_LINK_MODE=copy
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --locked

GitHub Actions 통합

기본 설정

공식 setup-uv 액션을 쓴다:

yaml
name: CI
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Install uv
        uses: astral-sh/setup-uv@v8
        with:
          version: "0.11.7"
      - name: Install the project
        run: uv sync --locked --all-extras --dev
      - name: Run tests
        run: uv run pytest tests
yaml
name: CI
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Install uv
        uses: astral-sh/setup-uv@v8
        with:
          version: "0.11.7"
      - name: Install the project
        run: uv sync --locked --all-extras --dev
      - name: Run tests
        run: uv run pytest tests

멀티 Python 버전 테스트

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v6
      - name: Install uv
        uses: astral-sh/setup-uv@v8
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install and test
        run: |
          uv sync --locked --all-extras --dev
          uv run pytest tests
yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v6
      - name: Install uv
        uses: astral-sh/setup-uv@v8
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install and test
        run: |
          uv sync --locked --all-extras --dev
          uv run pytest tests

캐싱

setup-uv에 캐시 기능이 내장되어 있다:

yaml
      - name: Install uv
        uses: astral-sh/setup-uv@v8
        with:
          enable-cache: true
yaml
      - name: Install uv
        uses: astral-sh/setup-uv@v8
        with:
          enable-cache: true

그 외 통합

uv는 이 외에도 다양한 도구와 통합된다:

  • GitLab CI/CD — 공식 Docker 이미지를 사용해 GitLab에서도 동일하게 활용 가능
  • Jupyteruv run --with jupyter jupyter lab으로 프로젝트 환경에서 바로 노트북 실행
  • pre-commitastral-sh/uv-pre-commit 훅으로 락파일 검증 자동화
  • FastAPIuv add fastapi --extra standarduv run fastapi dev
  • PyTorch — 가속기(CPU/CUDA/ROCm)별 인덱스 설정 지원
  • Renovate / Dependabot — 자동 의존성 업데이트 지원
  • AWS Lambda — 멀티스테이지 빌드로 Lambda 배포

정리하며

세 편에 걸쳐 uv를 처음부터 끝까지 다뤘다. Python 설치부터 스크립트 실행, 프로젝트 관리, 도구 활용, 패키지 배포, 마이그레이션, Docker 통합까지.

솔직히 처음에는 "또 하나의 패키지 매니저"라고 생각했다. Python 생태계에는 이미 pip, Poetry, PDM, Rye가 있으니까. 그런데 직접 써보니 차원이 다르다. 단순히 빠른 게 아니라, 흩어져 있던 도구들을 하나로 통합하면서도 각각의 기능을 제대로 구현해냈다.

2026년 현재, 새 Python 프로젝트를 시작한다면 uv를 안 쓸 이유가 없다. 기존 프로젝트 마이그레이션도 uv add -r requirements.in 한 줄로 시작할 수 있으니, 한번 시도해 볼 만하다.


시리즈: uv 완전 가이드

참고 자료

YouTube 영상

채널 보기
행렬의 가장 중요한 연산 - 행렬 곱셈 | 선형대수학
인공지능은 세상을 어떻게 숫자로 읽는가? - 이미지, 소리 그리고 텍스트가 행렬이 되는 원리 | 선형대수학
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학
벡터의 정의와 덧셈 연산 | 선형대수학
AI를 위한 선형대수학 - 소개 | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기