Dockerized Flask / Django 앱에서 pip 대신 Uv로 전환하기
(nickjanetakis.com)- uv로 전환 시 Python 디펜던시 설치 속도가 pip 대비 약 10배 빨라지고, 별도의 venv 없이 비루트(non-root) 사용자로도 실행 가능함
- pyproject.toml 기반으로 상위 의존성만 명시하면 uv가 자동으로 lock 파일을 관리하며, 의존성 트리와 정확한 버전 관리가 pip freeze보다 우수함
- Dockerfile에서는 uv 및 uvx 바이너리 복사, pyproject.toml/uv.lock 파일 사용, 환경 변수 설정 등 단계별 변경이 필요
- uv sync/add/remove, uv:outdated와 같은 명령어로 쉽게 의존성 추가·삭제·업데이트 및 패키지 최신 버전 확인 등 다양한 관리가 가능
- 규칙적으로 lock 파일 관리 및 의존성 업데이트가 가능해져 협업 및 배포 환경에서 일관성 확보에 장점
10배 빨라진 의존성 설치, venv 미사용, 비루트 환경 구성
- uv는 기존 pip 대비 Python 프로젝트의 의존성 설치 속도를 크게 개선하는 툴임
- uv 도입으로 Flask/Django 등 다양한 프로젝트에서 기존 pip 대비 약 10배 빠른 설치 속도를 경험할 수 있음
- 별도의 가상환경(venv) 없이도 컨테이너 내에서 비루트 사용자로 안전하게 실행할 수 있음
pyproject.toml vs requirements.txt
- 기존의 requirements.txt 대신 pyproject.toml 파일에 상위 의존성만 명시하면 uv가 자동으로 uv.lock 파일을 생성함
- pyproject.toml에
[project] dependencies
항목 추가 - 기존 requirements.txt 삭제
- pyproject.toml에
- uv의 lock 파일은 pip freeze 결과와 유사하지만 정확한 의존성 트리와 버전 정보를 갖추고 있음
Dockerfile 구성 변경
- uv 및 uvx 바이너리를 컨테이너에 복사해 사용(정적 컴파일된 Rust 바이너리 사용)
- 기존 requirements*.txt 대신 pyproject.toml, uv.lock* 파일 복사
- 환경 변수 추가:
-
UV_COMPILE_BYTECODE=1
: 빌드 단계에서 바이트코드로 미리 컴파일 -
UV_PROJECT_ENVIRONMENT="/home/python/.local"
: 별도의 venv를 생성하지 않고 특정 경로에 패키지 설치
-
- 의존성 설치 명령어도 기존
pip3-install
대신uv-install
로 변경- 예:
RUN chmod 0755 bin/* && bin/uv-install
- 예:
의존성 추가, 삭제, 업데이트 등 관리
- 별도의 run 스크립트로 컨테이너 내 uv 명령어를 실행할 수 있음
-
./run deps:install
: 이미지 빌드 후 lock 파일을 호스트로 내보내면서 설치 -
./run deps:install --no-build
: 빌드 없이 lock 파일만 갱신 -
./run uv add mypackage --no-sync
: pyproject.toml 및 lock 파일만 갱신, 실제 설치는 별도 실행 -
./run uv remove mypackage --no-sync
: 패키지 제거 -
./run uv:outdated
: 현재 의존성의 최신 버전 확인
-
영상 및 실습 가이드 제공
- uv 도입, pyproject.toml 작성, Dockerfile 변경, lock/sync 명령, 의존성 추가/삭제, 최신 버전 확인 등 실제 데모 및 git diff 예시 제공
- Flask, Django 두 프로젝트의 마이그레이션 diff도 참고 가능
Hacker News 의견
-
uv는 pyenv, virtualenv, pip을 직접 대체하는 워크플로우를 지원한다는 점을 주목할 필요 있음. lockfile이나 pyproject.toml로 강제되는 방식이 아님.
uv python pin <version>
명령어로 현재 디렉토리에 .python-version 파일 생성,uv virtualenv
로 pyenv처럼 해당 버전의 파이썬을 다운로드 후 .venv 가상환경 생성,uv pip install -r requirements.txt
로 requirements.txt의 패키지 설치,uv run <command>
로 .env 파일의 환경변수 포함해 명령 실행 가능. 단, 환경변수 우선순위 문제 주의 (관련 이슈)- uv의 유연성은 정말 감탄스러운 수준임. pip으로 10분 걸릴 작업을 uv로 20~30초만에 처리하는 경험
- uv 사용 계기는 바로 이 부분임. 엄청 편리함. 다만 uv pip이 느린 경우가 있어 원인을 모르겠는 상황, 혹시 회사의 네트워크 환경 문제인 것 같음
- python 버전 정보가 pyproject.toml에도 저장되는 것으로 아는데, .python-version 파일이 꼭 필요한가라는 궁금증
-
# 항상 최신 lock file을 보장하는 스크립트 if ! test -f uv.lock || ! uv lock --check 2>/dev/null; then uv lock fi
이런 방식은 lock file의 존재 의미를 무색하게 만듦. 파일이 없거나 무효할 경우, lock file에 심각한 문제가 발생한 상태라 관련 프로젝트에 익숙한 사람이 직접 대응하는 게 바람직함. 그렇지 않다면 lock file을 둘 이유가 없음. CI에서 lock file을 자동으로 교체해 혼동 발생 가능성 있음
- (작성자 답변) lock file이 무효한 경우, 조용히 넘어가서 새 파일을 만드는 게 아님.
uv lock
에서 친절한 메시지로 실패 처리, 쉘 스크립트의 errexit로 바로 중단됨.uv lock --check
에러 리다이렉트는 같은 에러가 두 번 출력되는 것 방지 목적. lock 파일을 일부러 잘못 만들고 스크립트 실행 시, 구체적 에러 메시지와 함께 빌드가 멈춤. 스크립트는 if-else로 바꿔 더 명확하게 고침. lock 파일이 없으면 새로 생성하는 게 맞는 흐름임. 이때 생성해서 커밋하면 됨 -
uv sync --locked
옵션에서 이 부분 커버됨. lock file이 없거나 오래되면 명확히 실패시킴. 항상 --locked 옵션과 함께 빌드하는 걸 제안 - 파이썬 세계에서는 lock file을 버전 관리에 잘 안 올리고, 설치 과정의 "이상한 단계"로 다루는 경우가 많음
- 이 방식엔 심각한 버그가 있음. --frozen 플래그를 쓰면 lock file이 갱신 안 되는 것이 맞는데, 실제로는 반대로 동작. lock file이 없거나 맞지 않으면 사람이 개입해야 한다는 점 동의
- 그래도 lock file이 없으면 첫 실행이거나, 어차피 git upstream을 통해 덮어쓰이게 됨. 깨진 경우는 누군가 설치에 실수한 것이고, 새로 만드는 방법이 사실상 유일하게 합리적이라고 생각. 드문 예외지만 간단한 처리로서 충분함
- (작성자 답변) lock file이 무효한 경우, 조용히 넘어가서 새 파일을 만드는 게 아님.
-
파이썬 툴이 파이썬 외의 언어로 개발되는 건 완전 반대 입장임. C가 이미 있어서 CPython이 표준화되어 있는데 굳이 새로운 언어(예: Rust)가 필요하지 않음. Pendulum 패키지가 3.13 지원을 7개월 넘게 지연했는데 Rust 네이티브 때문에 해당 문제를 고칠 줄 아는 사람이 부족해서라고 봄. 만약 C였다면 직접 고쳤을 것. (관련 이슈) 이상적으로는, Rust와 같은 외부 언어로 빠른 datetime을 만들고 싶다면 FFI로 여러 언어에서 쓸 수 있는 형식으로 만드는 게 맞음. Rust 기반은 아직 썩 마음에 들지 않고, 리눅스 커뮤니티가 꺼리는 것도 이해하게 됨
- 이 관점 존중하지만, uv 같은 툴을 Rust로 만드는 건 좋은 아이디어라고 생각. 파이썬 관리 도구를 파이썬으로 만들면 "닭이 먼저냐 달걀이 먼저냐" 상황이 생김. 파이썬 도구를 쓰려면 파이썬 자체가 먼저 설치되어 있어야 하고, 어떤 파이썬 버전이 쓰이는지, 도구가 사용하는 라이브러리와 실제 앱 간의 충돌 가능성, 환경 변수 관리, 디버깅 모두 복잡해짐. 반면 Rust 등으로 빌드된 바이너리 도구는 그냥 받아서 쓰면, 이런 걸 신경쓸 필요 없이 즉시 동작. 사용자는 툴이 어떤 언어로 만들어졌는지 크게 신경 안 써도 됨
- 파이썬을 좋아하지만, uv의 간편함과 속도는 비교 불가. EOL된 서버에서 최신 파이썬 필요할 때, 작은 스크립트에 종속성만 빠르게 설치하고 싶을 때 모두 uv가 베스트임. 동의하는 부분도 있는데, 예전에는 pure python으로만 짜다가, 점차 C extension을 쓰고, 한계 느끼면 아예 거의 모두 C로 쓰고 싶어지더라. C가 어려워서 최근엔 Rust로 리팩토링하는 중. 외부 코드가 내부보다 많아지면 그냥 전부 다른 언어로 바꾸는 게 나음
- 파이썬만으로 도구를 만들어야 한다는 생각이 강하다면, 느린 Pylint 기다릴 때 본인은 산책하고 있겠음
- 다양한 언어 지원은 사용자에게 별 부담 아님. 도구는 빠르고 문제를 잘 해결하면 충분. 실제로 속도가 훨씬 빠름. 관리도구는 개발자가, 사용 대상을 위한 도구임
- 나는 어떤 언어로 만들어졌든 기능만 잘하면 됨. 파이썬 사용자가 도구에 기여할 수 있다는 점은 있지만, 도구가 목적을 잘 수행하면 언어는 상관 없음. 오히려 환경 문제에 봉착했을 때, 파이썬으로 만든 도구는 그 문제까지 영향을 받아 버릴 수도 있음
-
pip 대신 uv를 쓸 땐 조심해야 함. 기본으로는 pyc 파일을 생성하지 않으니 서비스 시작이 느려질 수도 있음 (참고)
- 컨테이너에서 uv 쓸 때는 가이드 문서가 더 도움이 됨 (Docker 안내 문서)
-
uv를 flask 컨테이너에 써보면, 빌드 타임 차이가 지루할 만큼 클 뿐만 아니라, 설치 과정이 매우 예측 가능해짐. pip으로 종속성 버전이 바뀌는 당혹스러움이 없음. pyproject.toml 쓰고, uv lock 하면 끝. docker에서는 pyproject.toml, uv.lock 파일만 복사(HOT COPY)하고 uv sync --frozen --no-install-project 실행하면 앱 코드는 건너뛰고 설치 레이어는 캐싱 가능. 패키지 하나만 바뀌어도 전체 레이어 재빌드를 할 때의 고통을 알면 이 기능이 왜 중요한지 느낌. 환경변수 UV_PROJECT_ENVIRONMENT=/home/python/.local 사용시 venv 없이 베이스 이미지를 pre-warm 하면 빌드 공유 및 인프라 비용 절감. UV_COMPILE_BYTECODE=1 옵션으로 빌드 시 .pyc 파일 생성. mutable environment 소멸 및 reproducibility 강제, 이제 빌드가 안 되면 lockfile 책임으로 원인 명확해짐
-
2025년이 되어도 파이썬 패키징과 의존성 관리는 여전히 혼란스러운 상태
- uv를 모두가 쓰지 않아서 계속 혼란스러운 거라 생각함
- 언어 설계 초기부터 이런 부분을 제대로 세팅하는 게 중요하다는 교훈임. v2.0 이후로 미루지 말 것, metadata를 실행 스크립트에 넣기 전에 여러번 고민할 것, 어떤 언어에는 맞지만 파이썬에는 좋지 않은 방식일 수 있음
- 나는 의존성 문제를 한 번도 겪어본 적 없음. requirements.txt와 venv만 써도 충분
- 의존성 관리가 여전히 엉망, 이제는 Rust까지 추가됨
-
uv, pip, conda 등 파이썬 패키지 관리자의 보안성 비교가 궁금함. 속도도 좋지만, 패키지 매니저의 보안이 훨씬 더 중요하다고 생각
-
PyPI에 패키지를 올리는 입장이라서, 개인적으로는 빠른 속도 때문에 uv를 쓰고 싶지만, pip과 완벽히 동일하게 동작한다는 보장이 없다면 쉽사리 바꿀 수 없음. 사용자가 "pip install xxx"로 오류를 겪으면, 나도 동일 환경에서 재현·디버깅해야 하니까
- pip과 100% 동일 방식은 아님. 큰 차이는 호환성 문서에서 다루고 있음. 일부는 표준대로 바뀌는 과정의 차이, 일부는 uv 고유의 디자인 선택임
-
UV는 실행만 하면 괜찮은 결과를 주는, 최근 파이썬 패키징에 가장 긍정적인 변화 중 하나라는 생각
-
생산 환경 컨테이너 구축에 uv를 사용하는 훌륭한 가이드 문서도 소개함 (가이드 보기)