SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-13

Ubuntu 서버 Python 환경 관리: venv, pipx, uv 비교

Ubuntu 24.04에서 발생하는 externally-managed-environment 오류의 원인을 설명합니다. 애플리케이션용 venv, 도구용 pipx, 그리고 고성능 uv 중 목적에 맞는 도구를 선택하는 기준과 시스템 안정성을 유지하며 패키지를 관리하는 방법을 정리했습니다.

새로운 Ubuntu 서버에서 pip install이 실패하는 이유

서버에서 Python venv, pipx, uv 중 무엇을 선택할지는 한 가지 질문으로 귀결됩니다. 무엇을 설치하려 하는가입니다. 애플리케이션 의존성은 애플리케이션 디렉터리 내부의 가상 환경에 두어야 합니다. 이름으로 직접 호출하려는 명령줄 도구는 pipx에 두어야 합니다. uv는 두 가지 역할을 모두 수행하며 lockfile을 추가하는데, 이는 두 번째 머신에서 동일한 환경을 빌드해야 할 때 중요해집니다. 이들 중 어떤 것도 시스템 Python에 설치하지는 않습니다. 현재의 Ubuntu 서버는 이를 단호하게 거부하기 때문입니다.

Ubuntu 24.04에서 sudo pip install requests를 실행하면 pip는 파일을 하나도 다운로드하기 전에 중단됩니다.

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip.

    If you wish to install a non-Debian packaged Python application,
    it may be easiest to use pipx install xyz, which will manage a
    virtual environment for you.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this behaviour by passing --break-system-packages.

이는 PEP 668(Python enhancement proposal 668, "외부에서 관리되는 환경")이 제 역할을 하고 있는 것입니다. Debian과 Ubuntu는 /usr/lib/python3.12/EXTERNALLY-MANAGED에 있는 인터프리터 옆에 마커 파일을 배치하며, pip는 해당 마커가 있는 인터프리터에는 쓰기를 거부합니다.

이 규칙은 sys.path 순서 때문에 존재합니다. apt는 라이브러리를 /usr/lib/python3/dist-packages에 설치합니다. root 권한으로 시스템 인터프리터에 대해 실행된 pip는 /usr/local/lib/python3.12/dist-packages에 쓰기를 수행하며, Debian의 패키징은 해당 디렉터리를 검색 경로의 앞부분에 배치합니다. python3 -c 'import sys; print(sys.path)'를 사용하여 직접 출력해 보고 순서를 확인해 보십시오. 따라서 pip가 작성한 복사본이 apt가 설치한 복사본을 가리게 되며, 이는 배포판 자체 도구를 포함하여 /usr/bin/python3 하에서 실행되는 시스템의 모든 프로그램에 영향을 미칩니다. cloud-init은 해당 인터프리터에서 requests, jinja2 및 PyYAML을 가져옵니다. pip로 이들 중 하나를 업그레이드하여 호환되지 않는 릴리스가 설치되면, 다음 부팅 시 건드린 적도 없는 무언가가 알지 못했던 패키지를 지목하는 traceback과 함께 실패하게 됩니다. apt는 여전히 자신의 버전을 설치된 것으로 기록하므로 아무런 경고도 주지 않으며, 복구는 sudo apt reinstall python3-requests이 됩니다.

따라야 할 규칙은 간단합니다. 시스템 Python은 배포판의 소유입니다. 시스템 Python에 설치하지 말고, pip로 라이브러리를 업그레이드하지 마십시오. 메시지를 없애기 위해 EXTERNALLY-MANAGED 파일을 삭제해서도 안 됩니다. /usr/bin/python3에 맡겨야 할 유일한 작업은 가상 환경을 빌드하는 것입니다.

venv, pipx, uv 선택 기준

최근에 접한 도구가 아니라, 설치하려는 대상에 따라 도구를 선택해야 합니다.

  • Django나 Flask 프로젝트처럼 서비스로 배포하고 실행하는 애플리케이션: 애플리케이션 디렉터리 내부에 하나의 가상 환경(venv)을 생성합니다.
  • PATH에서 사용하려는 명령줄 도구(예: ansible 또는 httpie): 각 도구에 독립적인 환경을 제공하고 PATH에 링크를 생성하는 pipx를 사용합니다.
  • 락파일(lockfile)이 필요하거나, 더 빠른 설치가 필요하거나, 배포판에서 제공하지 않는 Python 버전이 필요한 프로젝트: 일반적인 venv와 uv.lock 파일을 생성하는 uv를 사용합니다.
  • 사용자의 코드가 아닌 배포판 도구에 필요한 라이브러리: 시스템 인터프리터에 무언가를 추가할 수 있는 유일한 지원 방식인 sudo apt install python3-<name>을 사용합니다.

pipx와 uv tool install은 동일한 역할을 수행하므로, uv가 이미 설치된 환경이라면 pipx는 필요하지 않습니다. 어떤 웹 프레임워크를 선택했는지는 이 결정에 영향을 주지 않습니다. VPS에서의 Django 및 Flask는 환경 구축 방식이 아니라 requirements.txt에 무엇이 포함되는지에 따라 차이가 발생합니다. 아래의 모든 내용은 Ubuntu 24.04와 해당 시스템의 Python 3.12를 기준으로 작성되었으므로, 사용하는 버전이 다르다면 경로 내의 버전을 수정하십시오.

애플리케이션별 venv 빌드

Ubuntu는 venv 모듈을 기본 Python 패키지에서 분리하므로, 최소 설치 이미지에서는 첫 시도 시 누락된 항목을 명시하는 메시지와 함께 실패합니다.

The virtual environment was not created successfully because ensurepip is not
available.  On Debian/Ubuntu systems, you need to install the python3-venv
package using the following command.

    apt install python3.12-venv

해당 모듈을 설치한 뒤, 코드를 소유할 사용자로 환경을 생성하십시오.

sudo apt update
sudo apt install -y python3-venv
sudo install -d -o deploy -g deploy -m 755 /srv/myapp
sudo -u deploy python3 -m venv /srv/myapp/.venv
sudo -u deploy /srv/myapp/.venv/bin/pip install -r /srv/myapp/requirements.txt

없는 항목에 주목하십시오. source도 없고 activate도 없습니다. /srv/myapp/.venv/bin/pip는 셸에 내보낸 설정 때문이 아니라 바이너리가 위치한 경로 덕분에 해당 환경에 설치됩니다. 더 진행하기 전에 이를 확인하십시오.

/srv/myapp/.venv/bin/python -c 'import sys; print(sys.prefix)'

해당 명령은 /srv/myapp/.venv을 출력합니다. 만약 /usr가 출력된다면 시스템 인터프리터를 실행 중인 것이며, 패키지가 의도하지 않은 곳에 설치된 것입니다.

venv의 두 가지 특성이 이후 수행 가능한 작업을 결정합니다. venv는 위치를 옮길 수 없습니다. bin/ 내의 모든 스크립트가 절대 경로로 된 shebang 라인을 포함하기 때문입니다. head -1 /srv/myapp/.venv/bin/pip#!/srv/myapp/.venv/bin/python을 읽습니다. 상위 디렉터리 이름을 바꾸면 해당 스크립트들은 bad interpreter: No such file or directory 오류와 함께 실패합니다. 또한 venv는 이를 생성한 인터프리터를 고정하며, 이는 /srv/myapp/.venv/pyvenv.cfg 내의 home 라인에 기록됩니다. bin/python3은 해당 바이너리에 대한 심볼릭 링크입니다. 릴리스를 업그레이드하여 python3.12가 사라지면 심볼릭 링크는 대상을 잃게 되고, 서비스는 시작 시 No such file or directory 오류로 종료됩니다. 두 경우 모두 해결 방법은 동일합니다. venv를 삭제하고 requirements.txt에서 새로 빌드하십시오. 재빌드는 수 초 내에 완료됩니다. venv를 머신 간에 복사하지 마십시오.

venv의 위치와 소유권

venv는 /srv/myapp/.venv의 코드 옆에 배치하고, 애플리케이션당 하나씩 유지합니다. 이렇게 하면 배포 단위가 하나의 디렉터리로 고정되고, systemd 유닛의 경로도 변경할 필요가 없으며, 하나의 의존성 업그레이드로 인해 두 애플리케이션이 서로 영향을 주는 일을 방지할 수 있습니다. venv에는 의존성 패키지와 설정 파일이 포함되므로, 웹 서버가 직접 파일을 서비스하는 경로에는 절대 배치하지 마십시오.

소유권 설정은 잠시 시간을 들여 신중히 처리해야 합니다. deploy 사용자가 코드와 환경을 소유하게 하고, 서비스 계정에는 읽기 및 실행 권한만 부여하십시오.

sudo adduser --system --group --no-create-home myapp
sudo chown -R deploy:myapp /srv/myapp
sudo chmod -R o-rwx /srv/myapp

이제 서비스는 의존성을 불러올 수는 있지만 수정할 수는 없습니다. 즉, 웹 애플리케이션에 코드 실행 취약점이 발생하더라도 디스크상의 라이브러리를 몰래 교체하여 재시작 후에도 악성 코드가 유지되도록 할 수 없습니다. 시스템의 나머지 부분에 동일한 원칙을 적용하는 방법은 최소 권한 사용자로 서비스 실행하기에서 다룹니다.

명령줄 도구를 위한 pipx

pipx는 라이브러리가 아닌 애플리케이션을 설치합니다. 각 도구는 ~/.local/share/pipx/venvs/<name> 아래에 고유한 환경을 할당받으며, 해당 도구의 실행 파일은 ~/.local/bin로 링크됩니다. 따라서 동일한 라이브러리의 서로 다른 버전을 요구하는 두 도구 간에 충돌이 발생하지 않습니다.

sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install httpie

pipx ensurepath는 셸 시작 파일을 수정하여 PATH~/.local/bin을 추가합니다. 이는 현재 사용 중인 셸에는 즉시 적용되지 않으므로, 설치 직후 http: command not found가 나타난다면 로그아웃 후 다시 로그인하지 않았을 가능성이 큽니다. Ubuntu의 기본 ~/.profile은 로그인 시점에 해당 디렉터리가 존재할 때만 ~/.local/bin를 추가합니다. 이 때문에 새 계정에서 처음 한 번만 문제가 발생하고 이후에는 발생하지 않습니다.

라이브러리를 대상으로 pipx를 실행하면 다음과 같은 메시지가 출력되며 거부됩니다.

No apps associated with package requests or its dependencies.

이는 해당 도구가 올바른 사용법이 아님을 알리는 것입니다. 라이브러리는 애플리케이션의 venv 내부에 있어야 합니다.

서버에서 중요한 세부 사항은 설치 위치입니다. 일반적인 pipx install는 모든 것을 특정 사용자의 홈 디렉터리 아래에 배치합니다. myapp으로 실행되는 systemd 유닛은 이를 볼 수 없으며, root cron 작업도 마찬가지입니다. /etc/sudoerssecure_pathPATH을 고정된 목록으로 대체하므로 sudo 또한 이를 찾지 못합니다. 시스템 전체에서 사용해야 하는 도구라면 전역으로 설치하십시오.

sudo pipx install --global ansible
sudo pipx ensurepath --global

--global 플래그를 사용하면 환경은 /opt/pipx에 배치되고 실행 파일은 /usr/local/bin으로 링크됩니다. 이 경로는 기본 PATH에 포함되어 있으며 secure_path 내부에 있습니다. 먼저 pipx --version으로 버전을 확인하십시오. Ubuntu 24.04는 --global보다 오래된 pipx 1.4.3 버전을 패키지로 제공하며, 이전 버전의 pipx는 unrecognized arguments: --global로 응답합니다. 해당 버전에서는 다음 두 디렉터리를 직접 설정해야 합니다.

sudo env PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ansible
command -v ansible

command -v ansible를 실행하면 /usr/local/bin/ansible이 출력되어야 합니다. 만약 /home 아래의 경로가 출력된다면, 해당 도구는 단일 사용자 계정 내부에 설치된 것이며 어떤 서비스도 이를 찾을 수 없습니다.

lockfile이 필요할 때 uv 사용하기

uv는 pip, venv, pip-tools가 수행하는 작업을 모두 처리하며 인터프리터까지 다운로드할 수 있는 Astral의 단일 바이너리입니다. 소형 VPS에서도 차이가 느껴질 만큼 속도가 빠르며, 실제 lockfile을 생성합니다.

공식 설치 프로그램은 uvuvx~/.local/bin에 배치합니다:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

서버에서 스크립트를 셸로 바로 파이핑할 때는 잠시 주의를 기울여야 합니다. URL의 버전을 고정하고 실행하기 전에 파일 내용을 확인하십시오:

curl -LsSf https://astral.sh/uv/0.12.3/install.sh -o uv-install.sh
less uv-install.sh
sh uv-install.sh

pipx가 이미 설치되어 있다면 pipx install uv을 사용할 수도 있습니다. uv는 자체적인 Python 의존성이 없는 독립형 바이너리이므로, /usr/local/bin에 복사해 두면 서버의 모든 사용자가 공유할 수 있습니다.

pyproject.toml을 사용하는 프로젝트의 워크플로우는 4개의 명령어로 구성되며, 마지막 명령어만 서버에서 실행합니다.

uv init myapp
uv add flask gunicorn
uv lock
uv sync --frozen --no-dev

uv lock는 정확하게 해결된 버전 정보를 담은 크로스 플랫폼 lockfile인 uv.lock을 생성하며, 이를 코드와 함께 커밋합니다. uv sync은 이에 맞춰 프로젝트 루트에 .venv를 빌드합니다. 서버에서는 --frozen 플래그가 중요합니다. 문서에 따르면 이 플래그는 lockfile이 최신인지 확인하는 대신 lockfile에 명시된 버전을 진실의 원천(source of truth)으로 사용하도록 정의되어 있으며, 이는 배포 환경에서 요구하는 동작입니다. --no-dev는 개발 의존성 그룹을 제외합니다.

기존 requirements.txt 프로젝트는 변환이 필요 없습니다. uv는 pip의 언어를 지원하기 때문입니다.

uv venv /srv/myapp/.venv
uv pip install --python /srv/myapp/.venv/bin/python -r /srv/myapp/requirements.txt

결과물은 일반적인 가상 환경입니다. .venv/bin/pythonpython3 -m venv이 빌드했을 때와 동일하게 동작하므로, 이 가이드의 이후 내용에서 변경되는 점은 없습니다.

서버에서 uv를 사용하기 전에 알아두어야 할 기본 설정이 하나 있습니다. python-preference 설정의 기본값은 managed이며, 이는 시스템에 이미 존재하는 인터프리터보다 "uv가 다운로드하고 설치한 인터프리터"를 우선하도록 문서화되어 있습니다. 따라서 3.12 버전만 포함된 서버에서 uv venv --python 3.13을 실행하면 실패하는 대신 ~/.local/share/uv/python에 3.13 버전을 조용히 가져옵니다. 이는 노트북에서는 편리하지만 서버에서는 당혹스러울 수 있습니다. 서비스가 홈 디렉터리에 위치한 인터프리터에 의존하게 되며, apt upgrade가 이를 패치하지 않기 때문입니다. 배포판의 인터프리터를 사용하려면 uv.toml에서 python-preferenceonly-system로 설정하십시오. 가상 환경을 프로젝트 루트가 아닌 다른 곳에 두려면 UV_PROJECT_ENVIRONMENT으로 프로젝트 가상 환경을 위한 디렉터리를 지정할 수 있습니다.

systemd에서 activate가 아닌 venv 인터프리터를 지정하십시오

대부분의 Python 배포가 실패하는 지점이며, 이는 activate의 역할을 오해하기 때문에 발생합니다.

bin/activate은 셸 스크립트입니다. 이 스크립트는 venv의 bin 디렉터리를 PATH의 맨 앞에 추가하고, VIRTUAL_ENV을 설정하며, 나중에 deactivate가 복구할 수 있도록 기존 값을 저장하고, 프롬프트를 변경합니다. 인터프리터 자체가 읽는 내용은 포함되어 있지 않습니다. 활성화(activation)는 사람이 프롬프트에서 python을 입력할 때의 편의를 위한 기능일 뿐입니다.

실제로 환경을 결정하는 것은 실행하는 인터프리터 파일입니다. /srv/myapp/.venv/bin/python가 시작되면 Python의 site 모듈은 실행 파일이 있는 디렉터리와 그 상위 디렉터리에서 pyvenv.cfg 파일을 찾습니다. /srv/myapp/.venv/pyvenv.cfg을 찾으면 sys.prefix이 venv로 설정되고, 해당 venv의 site-packagessys.path에 추가됩니다. 이것이 메커니즘의 전부입니다. 환경 변수나 셸은 필요하지 않습니다.

따라서 다음 유닛은 절대 시작되지 않습니다.

[Service]
ExecStart=source /srv/myapp/.venv/bin/activate && gunicorn app:app
myapp.service: Failed to locate executable source: No such file or directory
myapp.service: Failed at step EXEC spawning source: No such file or directory
myapp.service: Main process exited, code=exited, status=203/EXEC

ExecStart은 셸 명령줄이 아닙니다. systemd는 프로그램을 직접 실행하므로 source 내장 명령은 존재하지 않으며, &&은 리터럴 인수로 전달될 뿐 아무것도 확장되지 않습니다.

다음 유닛은 시작된 직후 종료됩니다.

[Service]
ExecStart=/usr/bin/python3 /srv/myapp/app.py
ModuleNotFoundError: No module named 'flask'

/usr/bin/python3는 시스템 인터프리터이며, 해당 sys.path에는 venv가 포함되어 있지 않습니다. 이 명령이 SSH 세션에서 작동하는 이유는 사용자가 venv를 활성화했기 때문이며, 그 결과 셸이 python3PATH을 통해 .venv/bin/python3로 해석했기 때문입니다.

명령을 /bin/bash -c 'source ... && gunicorn ...'로 감싸면 작동은 합니다. 하지만 이는 systemd와 프로세스 사이에 불필요한 셸을 끼워 넣는 것이며, 절대 경로를 사용하면 간단히 해결됩니다.

[Unit]
Description=myapp web service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp
Environment=PYTHONUNBUFFERED=1
Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/srv/myapp/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -n 50 --no-pager

systemctl status myappMain PID가 gunicorn 프로세스인 active (running)을 출력해야 합니다. 다른 결과가 나온다면 저널을 확인하십시오.

Environment=PATH= 라인은 이미 전체 경로를 포함하고 있는 ExecStart를 위해 존재하는 것이 아닙니다. 이는 애플리케이션이 시작하는 프로세스들을 위해 존재합니다. 서비스는 systemd로부터 짧은 기본 PATH를 상속받으므로, subprocess.run(["ffmpeg", ...])을 호출하는 Python 코드나 venv의 콘솔 스크립트를 실행하는 관리 명령은 필요한 파일을 찾지 못합니다. venv의 bin 디렉터리를 맨 앞에 두는 것은 서비스가 실제로 사용하는 activate의 유일한 부분입니다. systemctl show -p Environment myapp를 사용하여 유닛이 실제로 무엇을 받았는지 확인하십시오.

동일한 규칙이 예약 작업에도 적용됩니다. cron은 PATH/usr/bin:/bin로 설정하여 작업을 실행하므로, crontab 라인에 python3 /srv/myapp/cleanup.py라고 적으면 시스템 인터프리터가 실행되어 새벽 3시에 ModuleNotFoundError 오류가 발생하며, 이 오류는 아무도 읽지 않는 로컬 메일 스풀로 전송됩니다. 여기에도 venv의 절대 경로를 작성하십시오. 저널에 출력을 기록하고 마지막 실행 기록을 남기려면 systemd 서비스 및 타이머 쌍을 사용하고 동일한 ExecStart 라인을 지정하십시오.

Docker가 이 결정을 대체합니까?

컨테이너는 자체 파일 시스템을 가지므로, 질문이 사라지는 대신 형태가 바뀝니다. python:3.12-slim와 같은 공식 이미지에서 Python은 /usr/local에 빌드되어 있으며 EXTERNALLY-MANAGED 마커를 포함하지 않습니다. 따라서 패키지를 추가할 때는 pip install를 root로 사용하는 것이 의도된 방식이며, venv는 큰 이점을 주지 못합니다. 대신 FROM ubuntu:24.04를 빌드하면 호스트와 동일한 이유로 이미지 내부에서 다시 externally-managed-environment를 마주하게 됩니다. 이는 배포판의 마커 파일을 포함하는 배포판의 인터프리터이기 때문입니다.

많은 이미지가 여전히 venv를 사용하는데, 이는 멀티 스테이지 빌드를 단순하게 만들기 때문입니다. 빌더 단계에서 /opt/venv에 설치하고, 런타임 단계에서는 해당 디렉터리만 복사하여 컴파일러를 제외합니다. 이때 activate 문제가 함께 따라옵니다. RUN source /opt/venv/bin/activate 라인은 해당 빌드 레이어의 셸에만 영향을 미치므로, 런타임에 컨테이너가 시작되면 시스템 인터프리터를 사용하게 되어 ModuleNotFoundError 오류가 발생합니다. ENV PATH="/opt/venv/bin:$PATH"를 설정하거나, CMD에 절대 경로인 /opt/venv/bin/gunicorn을 지정하십시오. 이는 systemd 문제와 동일한 버그이며, 단지 파일만 다를 뿐입니다.

결론적으로 컨테이너는 인터프리터 문제를 대체합니다. 이미지가 인터프리터와 그 하위의 모든 것을 고정(pinning)하기 때문입니다. 하지만 고정 문제 자체를 없애지는 않습니다. 고정되지 않은 requirements.txt로 빌드된 이미지는 다음 달에 다른 버전을 가져올 수 있습니다. 이는 이미지 태그는 재현 가능하지만, 그 이미지를 생성한 빌드 과정은 재현할 수 없음을 의미합니다. 컨테이너 사용 여부와 관계없이 uv.lock과 같은 락파일이나 완전히 고정된 requirements 파일이 이러한 격차를 해소합니다. 또한 하나의 애플리케이션을 systemd 하의 VPS에서 실행할 때, 컨테이너는 이 결정을 대부분 Dockerfile로 옮길 뿐입니다. systemd가 이미 실패한 프로세스를 재시작하고 그 출력을 저널에 기록하기 때문입니다. VPS에서 Docker 실행하기는 빌드된 이미지 자체를 배포 단위로 사용하고자 할 때 그 가치를 발휘합니다.

FAQ

--break-system-packages 플래그를 사용하여 pip install을 실행해도 됩니까?

운영 중인 서버에서는 절대 안 됩니다. 이 플래그는 이름 그대로 안전장치를 제거하며, pip는 /usr/local/lib/python3.12/dist-packages 경로에 파일을 기록합니다. 이 경로는 sys.path에서 apt 디렉터리보다 우선순위가 높습니다. 따라서 /usr/bin/python3 하위에서 실행되는 모든 시스템 스크립트에서 배포판의 패키지 대신 사용자가 설치한 버전이 호출됩니다. apt는 여전히 자신의 버전이 설치된 것으로 인식하므로, 문제가 발생하기 전까지는 충돌을 감지할 수 없습니다. 매번 새로 빌드하는 컨테이너 이미지 내부라면 해당 이미지 내에서만 피해가 발생하므로 허용될 수 있습니다. 하지만 직접 관리하는 서버라면 반드시 venv를 생성하십시오. 명령어 한 줄이면 충분합니다.

서버에서 가상 환경은 어디에 두어야 합니까?

애플리케이션 디렉터리 내부에 /srv/myapp/.venv이라는 이름으로 생성하십시오. 배포용 사용자가 소유권을 가지며, 서비스 계정은 읽기 및 실행 권한만 갖도록 설정합니다. 애플리케이션당 하나의 venv를 유지하십시오. 공유 venv를 사용하면 한 애플리케이션의 업그레이드가 다른 애플리케이션을 고장 낼 수 있습니다. venv를 생성한 후에는 이동하거나 복사하지 마십시오. bin/ 디렉터리의 모든 스크립트에는 절대 경로가 shebang 라인에 기록되어 있으므로, 이동하면 bad interpreter: No such file or directory 오류가 발생합니다. 이 경우 삭제 후 requirements.txt을 사용하여 다시 빌드하십시오.

systemd 서비스가 ModuleNotFoundError로 실패하는 이유는 무엇입니까?

유닛 파일이 venv의 인터프리터가 아닌 다른 인터프리터를 실행하고 있기 때문입니다. systemctl cat myapp을 실행하고 ExecStart을 읽어보십시오. 유닛 파일에는 /srv/myapp/.venv/bin/python 또는 동일한 bin/ 디렉터리의 콘솔 스크립트가 절대 경로로 지정되어야 합니다. 유닛 파일에서 activate을 source하는 방식은 작동하지 않습니다. ExecStart는 셸이 아니며, systemd는 status=203/EXEC와 함께 Failed to locate executable source 오류를 보고합니다. Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin를 추가하여 코드에서 시작하는 모든 하위 프로세스가 venv의 도구를 찾을 수 있도록 하십시오.

venv와 pip 대신 uv를 사용해야 합니까?

lockfile이 필요하거나, 설치 속도가 너무 느려 개선이 필요하거나, 배포판에서 제공하지 않는 Python 버전이 필요한 경우 uv를 사용하십시오. uv는 일반적인 venv를 생성하므로 systemd 유닛이나 파일 구조는 변경되지 않으며, uv sync --frozen은 lockfile에 기록된 내용을 정확하게 설치합니다. 단일 애플리케이션을 git에서 고정된 requirements.txt 버전으로 배포하고 설치가 몇 초 만에 끝난다면, python3 -m venv만으로도 충분하며 서버에서 업데이트해야 할 바이너리를 하나 줄일 수 있습니다.

#python#venv#pipx#uv#deployment