pip: error: externally-managed-environment
안녕하세요, BlueByte입니다. 새로 올린 Ubuntu 24.04나 Debian 12 서버에서 pip3 install requests를 치면 다운로드 대신 error: externally-managed-environment와 함께 apt·venv·pipx를 권하는 긴 문단이 나옵니다. 고장 난 것도, pip이 이상해진 것도 아닙니다. 배포판이 자기 Python을 OS 패키지 관리자 소유로 표시했고, pip은 PEP 668이 요구하는 대로 그 표시를 존중하고 있을 뿐입니다. 오늘은 이 메시지가 어디서 오는지, 왜 sudo와 --user로는 못 넘어가는지, 지원되는 설치 경로 세 가지, 우회 옵션이 허용되는 경우, 그리고 제대로 된 인터프리터에 들어갔는지 확인하는 법까지 하나씩 짚어보겠습니다.
오류 메시지의 생김새와 각 줄을 누가 썼는지
Ubuntu 24.04, Python 3.12에서의 전체 출력입니다:
$ pip3 install requests
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. Make
sure you have python3-full installed.
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. Make sure you have pipx installed.
See /usr/share/doc/python3.12/README.venv for more information.
note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.pip이 직접 쓴 줄은 첫 줄과 note:, hint:뿐입니다. 들여쓰기된 문단은 배포판이 함께 배포하는 파일에서 그대로 복사한 것이라, Debian·Fedora·Arch·Homebrew마다 문구가 다릅니다. sudo pip3 install도 pip3 install --user도 같은 메시지를 냅니다. 이 표시를 처음 존중한 버전이 pip 23.0이라, 이전 배포판 릴리스에서 업그레이드한 서버는 어느 날 갑자기 거부하기 시작합니다.
거부를 켜는 마커 파일
PEP 668은 인터프리터의 표준 라이브러리 디렉터리에 EXTERNALLY-MANAGED라는 파일 하나를 정의합니다. pip은 두 조건이 동시에 참일 때 거부합니다. 그 파일이 존재하고, 가상환경 밖(sys.prefix == sys.base_prefix)이라는 조건입니다. 직접 확인해 보면:
STDLIB=$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')
cat "$STDLIB/EXTERNALLY-MANAGED"[externally-managed]
Error=To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
...단순한 INI 파일이고, Error 키의 값이 pip이 출력한 그 문단입니다. Ubuntu에서는 이 파일이 libpython3.12-stdlib 패키지 소유라서, 지워도 다음 Python 보안 업데이트가 되살려 놓습니다. Debian 릴리스 노트가 의도를 설명합니다. apt가 함께 배포하는 라이브러리를 pip uninstall하거나 업그레이드하면 apt 소유 파일이 지워질 수 있고, Ubuntu에서 그 인터프리터 위에서 도는 도구에는 unattended-upgrade와 cloud-init이 포함됩니다. 이 거부는 바로 그것을 지키기 위한 장치입니다.
sudo와 --user가 통하지 않는 이유
두 경로 모두 시스템 인터프리터의 sys.path에 패키지를 놓습니다. root는 /usr/lib/python3/dist-packages, --user는 ~/.local/lib/python3.12/site-packages인데, 둘 다 apt가 관리하는 모듈을 똑같이 가리거나 충돌시키므로 PEP 668은 둘 다 의도적으로 막습니다. 여기엔 고칠 권한 문제가 없습니다. 인터프리터를 잘못 겨눈 것이고, 아래는 전부 올바른 인터프리터를 고르는 이야기입니다.
프로젝트 의존성이라면: 가상환경
venv는 자체 sys.prefix를 가지므로 그 안의 pip은 마커를 아예 보지 않습니다:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install requestsInstalling collected packages: urllib3, idna, charset_normalizer, certifi, requests
Successfully installed certifi-2026.7.22 charset_normalizer-3.5.1 idna-3.19 requests-2.34.2 urllib3-2.8.0activate가 필수는 아닙니다. .venv/bin/pip install requests와 .venv/bin/python app.py처럼 경로로 바로 실행할 수 있고, cron이나 systemd 유닛에서는 이쪽이 오히려 안전합니다. python3 -m venv가 ensurepip is not available이라며 실패하면 venv 모듈이 아직 없는 것이니 sudo apt install python3-venv(메시지가 권하는 대로 python3-full도 가능)로 해결됩니다.
도구는 pipx, OS가 직접 쓰는 스크립트는 apt
import해서 쓰는 게 아니라 실행하는 것들, 예를 들어 black·ansible·httpie는 pipx가 애플리케이션마다 venv를 하나씩 만들고 실행 파일을 PATH에 올려 줍니다:
sudo apt install pipx
pipx ensurepath
pipx install blackpipx는 설치한 패키지와 노출한 앱 이름을 출력합니다. ensurepath 뒤에는 새 셸을 열어야 ~/.local/bin이 PATH에 잡힙니다. 도구마다 독립적으로 업그레이드·삭제되고, 서로를 망가뜨릴 수 없습니다.
반대로 /usr/bin/python3로 root 권한에서 도는 스크립트(모니터링 훅, 백업 작업 등)가 import하는 모듈이라면, 정직한 답은 배포판 패키지입니다. 그래야 보안 업데이트도 apt가 같이 챙깁니다:
apt-cache policy python3-requests
sudo apt install python3-requests우회 옵션과 그것이 실제로 감수하는 위험
pip의 비상구는 --break-system-packages(환경변수로는 PIP_BREAK_SYSTEM_PACKAGES=1)입니다. 이름이 이런 데는 이유가 있습니다. 패키징 명세는 이 플래그가 "위험하다는 뉘앙스를 담도록" 설치 도구에 요구합니다. ubuntu:24.04로 만든 일회용 컨테이너처럼 이미지 자체를 버리는 환경이라면 변명이 되는 지름길이지만, 오래 운영하는 서버에서는 훗날의 pip install --upgrade가 unattended-upgrade가 의존하는 모듈을 바꿔치기할 수 있고, 그 사실은 다음 패치 시간에 드러납니다. Homebrew 문서도 macOS에서 같은 경고를 하며, 기본 인터프리터에 pip install --upgrade pip을 하지 말라고 못 박습니다.
실제 사례: Ubuntu 24.04로 옮긴 뒤의 cron 리포트 스크립트
22.04에서 pip3 install --user requests openpyxl로 몇 년을 돌던 야간 리포트가 있었습니다. 24.04에 다시 세우자 첫 pip3 install --user가 위 문구로 거부됐고, cat /usr/lib/python3.12/EXTERNALLY-MANAGED로 마커를 확인했습니다. 해결은 스크립트 옆에 venv를 두고 crontab에서 경로로 호출하는 것이었습니다:
sudo python3 -m venv /opt/report/.venv
sudo /opt/report/.venv/bin/pip install -r /opt/report/requirements.txt# /etc/cron.d/report
15 2 * * * report /opt/report/.venv/bin/python /opt/report/run.py스크립트는 한 줄도 바꾸지 않고 돌았고, apt는 /opt를 건드리지 않으므로 venv는 다음 Python 보안 업데이트에도 살아남습니다.
인터프리터를 확인하고, 그 상태를 유지하기
이후에 생기는 문제 대부분은 한 인터프리터에 설치하고 다른 인터프리터로 실행하는 경우입니다. Python에게 직접 물어보세요:
.venv/bin/python -c 'import sys, requests; print(sys.prefix != sys.base_prefix, requests.__version__)'
.venv/bin/pip --versionTrue 2.34.2
pip 24.0 from /home/you/app/.venv/lib/python3.12/site-packages/pip (python 3.12)True면 가상환경이고, pip 경로가 .venv 아래면 설치가 실행 위치로 갑니다. 이 상태를 유지하려면 프로젝트마다 requirements.txt와 venv를 두고, 서비스는 activate에 기대지 말고 .venv/bin/python 절대 경로를 가리키게 하세요. 마커 파일은 그냥 두십시오. 어차피 apt가 되돌려 놓습니다. Dockerfile에서는 공식 python:3.12 이미지를 쓰거나(직접 python:3.12-slim을 확인해 보니 표준 라이브러리 디렉터리에 마커가 없었습니다) 이미지 안에 venv를 만드세요. 시스템 pip을 pip으로 업그레이드하지 마십시오.
EACCES·ModuleNotFoundError와 어떻게 다른지
/usr/lib/python3/dist-packages에 대한 PermissionError: [Errno 13] Permission denied는 파일 시스템 권한 문제이고 pip 메시지에도 경로가 찍힙니다. 마커 오류는 경로를 전혀 언급하지 않습니다. 설치가 성공했는데 나오는 ModuleNotFoundError: No module named 'requests'는 정반대 상황입니다. 설치는 됐지만 스크립트를 돌리는 인터프리터가 다른 것이고, which python과 스크립트의 shebang을 비교하면 풀립니다. 다음에 이 오류를 만나면 질문을 거꾸로 따라가 보세요. 어느 인터프리터인지, 어떤 종류의 패키지인지, 지원되는 세 경로 중 어디에 해당하는지.
관련 질문
EXTERNALLY-MANAGED 파일을 그냥 지우면 안 되나요?
지울 수는 있고 pip도 다시 설치를 시작하지만, 그 파일을 소유한 패키지가 업데이트되는 순간까지만입니다. Ubuntu 24.04에서는 libpython3.12-stdlib 소유라 다음 Python 보안 업데이트가 되살려 놓고, 고친 줄 알았던 것이 조용히 원복됩니다. 파일이 지키던 안전장치도 함께 잃습니다. venv나 pipx는 1분이면 되고 되돌아가지 않습니다.
OS를 업그레이드했더니 pip install --user가 안 됩니다. 왜죠?
마커는 배포판 릴리스(Debian 12, Ubuntu 23.04 이후)와 함께 들어왔고, pip 23.0이 이를 처음 존중한 버전입니다. PEP 668은 --user를 의도적으로 막습니다. 사용자 site-packages도 dist-packages와 마찬가지로 시스템 인터프리터의 sys.path 위에 있기 때문입니다.
Dockerfile 안에서 --break-system-packages를 써도 괜찮나요?
배포판 베이스로 만든 일회용 이미지라면 피해가 이미지 안에 갇히므로 변명이 되는 지름길입니다. 더 깔끔한 선택은 공식 python 이미지(python:3.12-slim의 표준 라이브러리 디렉터리에는 마커가 없습니다)를 쓰거나 이미지 안에 venv를 만드는 것입니다.
macOS Homebrew에서도 같은 오류가 납니다. 해결도 같나요?
네. Homebrew 문서는 자기 Python을 PEP 668에 따라 externally managed로 표시한다고 밝히고, 프로젝트 의존성은 venv, 애플리케이션은 pipx를 권하며, 기본 인터프리터에 pip install --upgrade pip을 하지 말라고 안내합니다.
venv의 pip으로 설치했는데 스크립트는 여전히 ModuleNotFoundError를 냅니다.
스크립트가 다른 인터프리터로 실행되고 있는 것입니다. 스크립트 첫 줄의 shebang과 which python을 비교하고, .venv/bin/python script.py처럼 명시적으로 호출하세요. 어느 인터프리터가 활성인지 추측할 필요가 사라집니다.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.