SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-13

Ubuntu Python pip install 失敗:venv, pipx, uv 選擇指南

在 Ubuntu 伺服器執行 pip install 遇到 externally-managed-environment 錯誤?本文說明如何根據應用需求選擇 venv、pipx 或 uv,並正確設定 systemd 服務以避開系統限制。

為什麼在全新的 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 增強提案 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 會從該直譯器匯入 requestsjinja2 與 PyYAML。若使用 pip 升級其中之一並安裝到不相容的版本,您未曾更動過的程式可能會在下次開機時失敗,並出現包含您未曾察覺的套件名稱的追蹤回報(traceback)。由於 apt 仍記錄其安裝的版本,因此不會有任何警告,而修復方式通常是 sudo apt reinstall python3-requests

隨之而來的規則很簡單。系統 Python 屬於發行版。請勿安裝至其中,請勿使用 pip 升級其函式庫,也請勿為了消除錯誤訊息而刪除 EXTERNALLY-MANAGED 檔案。給 /usr/bin/python3 的唯一任務就是建置虛擬環境。

venv、pipx 與 uv:決策準則

請根據您要安裝的內容進行選擇,而非根據最近讀到的工具來決定。

  • 若為部署並以服務形式執行的應用程式(例如 Django 或 Flask 專案):請在該應用程式目錄內使用一個虛擬環境 (venv)。
  • 若為您希望在 PATH 上使用的命令列工具(例如 ansiblehttpie):請使用 pipx,它會為每個工具提供獨立的環境,並在 PATH 上建立一個連結。
  • 若為需要鎖定檔 (lockfile)、更快速安裝,或發行版未提供之特定 Python 版本的專案:請使用 uv,它會產生一個標準的 venv 以及一個 uv.lock 檔案。
  • 若為發行版工具而非您的程式碼所需要的函式庫:請使用 sudo apt install python3-<name>,這是將任何內容加入系統直譯器 (system interpreter) 的唯一受支援方式。

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 會安裝至該環境中,這是由二進位檔的位置所決定,而非取決於您匯出至 shell 的任何設定。在繼續之前請先確認。

/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。

虛擬環境的存放位置與擁有權

請將虛擬環境放置於 /srv/myapp/.venv 的程式碼旁,並確保每個應用程式各自擁有獨立的 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 會透過編輯 Shell 啟動檔案,將 ~/.local/bin 加入 PATH。它無法變更您當前已開啟的 Shell,因此安裝後若出現 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 工作也無法存取,且 sudo 也找不到它,因為 /etc/sudoers 中的 secure_path 會將 PATH 取代為固定的列表。若該工具應供全機使用,請進行全域安裝。

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

--global 旗標會將環境置於 /opt/pipx,並將可執行檔連結至 /usr/local/bin,該目錄位於預設的 PATH 內且包含於 secure_path 中。請先使用 pipx --version 檢查版本,因為 Ubuntu 24.04 封裝的 pipx 為 1.4.3,版本較 --global 舊,而舊版 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 下,則該工具被安裝在單一使用者的帳號中,任何服務都將無法找到它。

當您需要鎖定檔時使用 uv

uv 是由 Astral 開發的單一執行檔,涵蓋了 pip、venv 與 pip-tools 的功能,並能下載 Python 直譯器。其執行速度極快,在小型 VPS 上差異顯著,且能產生真正的鎖定檔(lockfile)。

官方安裝程式會將 uvuvx 放置於 ~/.local/bin

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

將指令碼透過 pipe 傳送至伺服器的 shell 執行前,請務必謹慎。請鎖定 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 的專案,工作流程僅需四個指令,且只有最後一個指令需要在伺服器上執行。

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

uv lock 會寫入 uv.lock,這是一個跨平台的鎖定檔,記錄了確切的解析版本,請將其與程式碼一同提交。uv sync 會在專案根目錄建立 .venv 以與鎖定檔匹配。在伺服器上,--frozen 是關鍵旗標:文件定義其作用為將鎖定檔中的版本視為唯一來源,而非檢查鎖定檔是否為最新,這正是部署環境所需的行為。--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/python 的行為與 python3 -m venv 建立的環境完全相同,因此本指南後續內容無需調整。

在伺服器上使用 uv 前,有一項預設值值得注意。其 python-preference 設定預設為 managed,文件說明其優先選擇「由 uv 下載並安裝的直譯器」,而非系統現有的直譯器。因此,在僅提供 3.12 的系統上執行 uv venv --python 3.13 時,它會自動將 3.13 下載至 ~/.local/share/uv/python 而不會失敗。這在筆記型電腦上很方便,但在伺服器上卻可能造成困擾,因為您的服務現在依賴於位於家目錄中、且 apt upgrade 永遠不會修補的直譯器。若您希望使用發行版提供的直譯器,請在 uv.toml 中將 python-preference 設定為 only-system。若您希望將環境放置於專案根目錄以外的位置,UV_PROJECT_ENVIRONMENT 可指定專案虛擬環境的目錄。

將 systemd 指向 venv 解譯器,而非 activate

這是大多數 Python 部署失敗的原因,源於對 activate 功能的誤解。

bin/activate 是一個 shell 指令稿。它會將 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-packages 加入 sys.path。這就是完整的運作機制,無需任何環境變數或 shell。

因此,以下單元無法啟動:

[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 並非 shell 命令列。systemd 直接執行程式,因此沒有 source 內建功能,&& 會被視為字面參數傳遞,且不會進行任何展開。

而以下單元會啟動後隨即終止:

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

/usr/bin/python3 是系統解譯器,其 sys.path 從未包含您的 venv。該指令在您的 SSH 連線中能運作,僅是因為您已在該處啟動了 venv,使得 shell 透過 PATHpython3 解析為 .venv/bin/python3

將指令包裝在 /bin/bash -c 'source ... && gunicorn ...' 中確實有效。但這會在 systemd 與您的處理程序之間多加一層 shell,卻毫無實益。直接使用絕對路徑即可解決:

[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 myapp 應回報 active (running),並顯示您的 gunicorn 處理程序作為 Main PID。若顯示其他內容,請查閱日誌。

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,會在凌晨三點執行系統解譯器並因 ModuleNotFoundError 而失敗,且錯誤訊息會發送到無人讀取的本機郵件佇列。請在該處同樣寫入 venv 的絕對路徑。若要將輸出記錄至日誌並保留上次執行紀錄,請改用 systemd 服務與計時器組合,並使用相同的 ExecStart 行。

Docker 是否取代了這項決策?

容器擁有獨立的檔案系統,因此問題的形式改變了,但本質依然存在。在如 python:3.12-slim 這類官方映像檔中,Python 已內建於 /usr/local 且不帶有 EXTERNALLY-MANAGED 標記,因此以 root 身分執行 pip install 是新增套件的預期方式,此時 venv 的幫助有限。若改為建置 FROM ubuntu:24.04,您會在映像檔內再次遇到 externally-managed-environment,原因與主機端相同:這是發行版提供的直譯器,且帶有發行版的標記檔案。

許多映像檔仍會使用 venv,因為它能簡化多階段建置(multi-stage build)。建置階段(builder stage)將套件安裝至 /opt/venv,執行階段(runtime stage)則僅複製該目錄,並將編譯器留在建置階段。此時,啟用(activate)的問題會隨之而來。RUN source /opt/venv/bin/activate 指令僅影響該建置層的 shell,因此在執行階段,容器會以系統直譯器啟動並引發 ModuleNotFoundError。請設定 ENV PATH="/opt/venv/bin:$PATH",或為 CMD 提供絕對路徑 /opt/venv/bin/gunicorn。這與 systemd 的問題相同,只是發生在不同的檔案中。

因此,容器取代了直譯器的問題,因為映像檔會鎖定直譯器及其下方的所有內容。但它並未取代鎖定(pinning)的問題。若映像檔是從未鎖定的 requirements.txt 建置而成,下個月解析出的版本將會不同,這意味著映像檔標籤(tag)雖可重現,但產生該映像檔的建置過程卻無法重現。無論是否使用容器,像 uv.lock 這類鎖定檔(lockfile)或完全鎖定的需求檔案,才是彌補此差距的關鍵。當單一應用程式在 VPS 上透過 systemd 執行時,容器化通常只是將相同的決策轉移至 Dockerfile,因為 systemd 本身已能重啟失敗的處理程序並將輸出擷取至 journal。在 VPS 上執行 Docker 的價值在於,當您希望將建置好的映像檔本身作為部署標的時,它便能發揮作用。

FAQ

我可以直接使用 pip install 並加上 --break-system-packages 嗎?

在需要持續運作的伺服器上,請勿這樣做。該旗標的作用正如其名:它移除了保護機制,讓 pip 將檔案寫入 /usr/local/lib/python3.12/dist-packages,而該路徑在 sys.path 中的優先順序高於 apt 目錄。這會導致您的版本覆蓋系統腳本在 /usr/bin/python3 下執行的版本,且 apt 仍認定其原始版本已安裝,因此在發生故障前,沒有任何機制能偵測到此衝突。若是在每次皆從頭建構的容器映像檔中,損害僅限於該映像檔,因此在該情境下尚可接受。但在您維護的機器上,請建立 venv。這僅需一個指令即可完成。

虛擬環境在伺服器上應該放在哪裡?

應放置在應用程式目錄內,命名為 /srv/myapp/.venv,由部署使用者擁有,並僅賦予服務帳號讀取與執行權限。每個應用程式應維持一個獨立的 venv,因為共用 venv 會導致升級第一個應用程式時損壞第二個應用程式。建立後請勿移動或複製 venv:其 bin/ 目錄下的每個腳本都將絕對路徑寫入其 shebang 行中,因此移動後的 venv 會因 bad interpreter: No such file or directory 而失敗。請改為刪除它並從 requirements.txt 重新建構。

為什麼我的 systemd 服務會出現 ModuleNotFoundError 錯誤?

因為該單元執行的是非 venv 的直譯器。請執行 systemctl cat myapp 並閱讀 ExecStart。它必須以絕對路徑指定 /srv/myapp/.venv/bin/python,或是來自同一個 bin/ 目錄下的控制台腳本。在單元檔案中執行 source activate 是無效的,因為 ExecStart 並非 shell,且 systemd 會回報 Failed to locate executable source 並顯示 status=203/EXEC。請加入 Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin,確保您的程式碼啟動的任何子處理程序也能找到 venv 的工具。

我應該使用 uv 來取代 venv 和 pip 嗎?

當您需要鎖定檔(lockfile)、安裝時間過慢造成困擾,或是需要發行版未提供的 Python 版本時,請使用 uv。它會建立一般的 venv,因此 systemd 單元與檔案配置無需變更,且 uv sync --frozen 會精確安裝鎖定檔中記錄的內容。若單一應用程式是從 git 部署並使用固定的 requirements.txt,且安裝能在數秒內完成,那麼 python3 -m venv 已足夠,且能減少伺服器上需要維護更新的二進位檔案數量。

#python#venv#pipx#uv#deployment