SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-13

サーバーのPython環境はvenv、pipx、uvのどれ?

Ubuntu 24.04でpip installがexternally-managed-environmentになる理由を確認し、用途別にvenv、pipx、uvを選ぶ方法とsystemdの指定方法を解説します。

Ubuntu の新規サーバーで pip install が失敗する理由

サーバーで Python venv、pipx、uv のどれを使うかは、何をインストールするかで決まります。アプリケーションの依存関係は、アプリケーション専用ディレクトリ内の仮想環境に配置します。名前を入力して実行するコマンドラインツールは pipx に配置します。uv はこの両方に対応し、lockfile も追加します。同じ環境を 2 台目のマシンで構築する必要が生じると、lockfile が重要になります。これらのツールのいずれもシステム Python にはインストールしません。現在の Ubuntu サーバーは、システム Python へのインストールを明確に拒否するためです。

Ubuntu 24.04 で sudo pip install requests を実行すると、pip はファイルを 1 つもダウンロードしないまま停止します。

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、「externally managed environments」)の動作です。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 プロジェクトなど): アプリケーションのディレクトリ内に 1 つの仮想環境(venv)を作成します。
  • PATH 上で使用するコマンドラインツール(ansiblehttpie など): pipx を使います。各ツール専用の環境と、PATH 上のリンクが作成されます。
  • lockfile、高速なインストール、またはディストリビューションが提供していない Python バージョンを必要とするプロジェクト: uv を使います。通常の venv と uv.lock ファイルが作成されます。
  • コードではなく、ディストリビューションのツールが必要とするライブラリ: sudo apt install python3-<name> を使います。システムインタープリターに何かを追加するためにサポートされている唯一の方法です。

pipx と uv tool install は同じ役割を担うため、すでに uv がある環境に pipx も用意する必要はありません。選択した Web フレームワークによる違いはありません。VPS 上の Django と Flaskrequirements.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

存在しないものにも注目してください。sourceactivate もありません。/srv/myapp/.venv/bin/pip がその環境にインストールされるのは、シェルにエクスポートした設定ではなく、バイナリの配置場所によるものです。先に進む前に確認してください。

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

/srv/myapp/.venv と表示されます。/usr と表示された場合は、システムのインタープリターを実行しており、パッケージが意図しない場所にインストールされています。

venv の後続の操作を決める重要な性質は 2 つあります。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.cfghome 行に記録され、bin/python3 はそのバイナリへのシンボリックリンクです。リリースをアップグレードして python3.12 がなくなると、シンボリックリンクの参照先がなくなり、サービスは起動時に No such file or directory で停止します。どちらの場合も、解決策は同じです。venv を削除し、requirements.txt から新しく作成してください。再作成には数秒しかかかりません。マシン間で venv をコピーしないでください。

venv の配置場所と所有者

コードを /srv/myapp/.venv に配置し、その隣に置きます。アプリケーションごとに venv を 1 つ用意してください。これでデプロイ対象が 1 つのディレクトリにまとまり、systemd unit には変わらないパスを指定でき、共有依存関係のアップグレードによって 2 つのアプリケーションが互いに壊れることもありません。Web サーバーがファイルを直接公開する場所には venv を配置しないでください。venv には依存関係が含まれ、多くの場合は設定も含まれるためです。

所有者の設定にも少し時間をかけてください。deploy ユーザーにコードと環境の所有権を持たせ、サービスアカウントには読み取りと実行の権限だけを付与します。

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

これでサービスは依存関係をインポートできますが、依存関係を書き換えることはできません。つまり、Web アプリケーションのコード実行バグがディスク上のライブラリを密かに置き換え、再起動後も残る事態を防げます。マシンの他の部分にも同じ考え方を適用する方法については、最小権限のユーザーでサービスを実行するで説明します。

コマンドラインツール向けのpipx

pipxはライブラリではなく、アプリケーションをインストールします。各ツールには~/.local/share/pipx/venvs/<name>配下に専用の環境が作成され、そのツールの実行ファイルは~/.local/binにリンクされます。そのため、同じライブラリの異なるバージョンを必要とする2つのツールが競合することはありません。

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

pipx ensurepathはシェルの起動ファイルを編集して、~/.local/binPATHに追加します。ただし、現在使用中のシェルは変更できません。そのため、インストール直後にhttp: command not foundとなる場合は、通常、まだログアウトして再ログインしていません。Ubuntuのデフォルトの~/.profileは、ログイン時点ですでにそのディレクトリが存在する場合にだけ~/.local/binを追加します。このため、新しいアカウントでは一度だけ問題になりますが、その後は発生しません。

ライブラリをpipxに指定すると、次のメッセージで始まるエラーを返して拒否します。

No apps associated with package requests or its dependencies.

これは、使用するツールが適切ではないという通知です。ライブラリはアプリケーションのvenvに入れてください。

サーバーでは、インストール先が重要です。通常のpipx installでは、すべてが1ユーザーのホームディレクトリ配下に配置されます。myappとして実行されるsystemdユニットからは参照できません。rootのcronジョブからも参照できず、sudoでも見つかりません。これは、/etc/sudoers内のsecure_pathPATHを固定リストで置き換えるためです。マシン全体で使用するツールは、グローバルにインストールしてください。

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

--globalフラグを指定すると、環境は/opt/pipxに配置され、実行ファイルは/usr/local/binにリンクされます。/usr/local/binはデフォルトのPATHに含まれ、secure_path内にもあります。まずpipx --versionでバージョンを確認してください。Ubuntu 24.04のpipxは1.4.3をパッケージ化しており、--globalより古いため、古いpipxではunrecognized arguments: --globalが返されます。そのバージョンでは、文書化されている2つのディレクトリを自分で設定します。

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配下のパスが表示される場合、そのツールは1ユーザーのアカウント内にインストールされています。その場合、サービスからは見つけられません。

lockfile が必要な場合の uv

uv は Astral が提供する単一バイナリで、pip、venv、pip-tools の機能をカバーし、インタープリターのダウンロードにも対応しています。小規模な 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 を作成し、lockfile に合わせます。サーバー上では --frozen フラグが重要です。ドキュメントでは、このフラグについて、lockfile が最新かどうかを確認するのではなく、lockfile に記録されたバージョンを正しい情報源として使用すると定義されています。これはデプロイに適した動作です。--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 を使用する前に、知っておくべきデフォルト設定が1つあります。python-preference 設定のデフォルト値は managed です。ドキュメントでは、システムにすでに存在するインタープリターではなく、「uv がダウンロードしてインストールしたもの」を選択する設定と説明されています。そのため、3.12 だけがインストールされたサーバーで uv venv --python 3.13 を実行すると、失敗せずに 3.13 を ~/.local/share/uv/python に取得します。これはラップトップでは便利ですが、サーバーでは予期しない動作です。サービスがホームディレクトリ内のインタープリターに依存し、そのインタープリターを apt upgrade が更新できなくなるためです。ディストリビューションが提供するインタープリターを使用する場合は、uv.tomlpython-preferenceonly-system に設定してください。環境をプロジェクトのルート以外に配置する場合は、UV_PROJECT_ENVIRONMENT でプロジェクトの仮想環境に使用するディレクトリを指定します。

venv の interpreter を systemd に指定し、activate は指定しない

Python のデプロイで最も多く問題が起きるのがこの部分です。原因は activate の動作を誤解していることです。

bin/activate は shell script です。venv の bin directory を PATH の先頭に追加し、VIRTUAL_ENV を設定し、deactivate が元の値に戻せるように保存し、prompt を変更します。interpreter 自体が読み取る内容は含まれていません。Activation は、prompt で人が python と入力するための利便機能です。

実際に environment を選択するのは、実行する interpreter file です。/srv/myapp/.venv/bin/python の起動時に、Python の site module は executable を格納している directory と、その 1 つ上の directory にある pyvenv.cfg file を探します。/srv/myapp/.venv/pyvenv.cfg が見つかると sys.prefix が venv に設定され、その venv の site-packagessys.path に追加されます。仕組みはこれだけです。environment variable も shell も必要ありません。

そのため、この unit は起動しません。

[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 command line ではありません。systemd は program を直接実行するため、source は組み込み command として処理されず、&& は文字列の引数として渡されます。展開も行われません。

一方、この unit は起動してから終了します。

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

/usr/bin/python3 は system interpreter であり、その sys.path に対象の venv が設定されたことはありません。SSH session で同じ command が動くのは、その session で venv を activate していたためです。その結果、shell は python3PATH 経由で解決し、実際には .venv/bin/python3 を実行していました。

command を /bin/bash -c 'source ... && gunicorn ...' でラップすれば動作します。ただし、絶対 path を 1 つ指定すれば済む処理のために、systemd と process の間に 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 は、Main PID が gunicorn process になっている active (running) を返すはずです。それ以外の場合は journal を確認してください。

Environment=PATH= の行は、完全な path をすでに持つ ExecStart のためではありません。application が起動する process のために必要です。service は systemd から短い既定の PATH を継承します。そのため、Python code が subprocess.run(["ffmpeg", ...]) を呼び出した場合や、venv の console script を management command から shell 経由で実行した場合、必要な command が見つかりません。venv の bin directory を先頭に追加することが、service が実際に使用する activate の設定です。unit が実際に受け取った内容は systemctl show -p Environment myapp で確認してください。

同じ規則は scheduled work にも適用されます。cron は /usr/bin:/binPATH で job を実行します。そのため、python3 /srv/myapp/cleanup.py と書かれた crontab の行は system interpreter を実行し、午前 3 時に ModuleNotFoundError で失敗します。エラーは、誰も確認していない local mail spool に送られます。ここにも venv の絶対 path を指定してください。代わりに、その出力を journal に記録し、直近の実行結果を残すには、systemd service と timer の組み合わせで同じ ExecStart の行を使用します。

Docker でこの判断は不要になりますか?

コンテナには独自のファイルシステムがあるため、問題はなくなるのではなく、形が変わります。python:3.12-slim などの公式イメージでは、Python は /usr/local に組み込まれており、EXTERNALLY-MANAGED マーカーを持たないため、root として pip install する方法が想定されています。venv を追加しても、ほとんどメリットはありません。代わりに FROM ubuntu:24.04 をビルドすると、イメージ内でも externally-managed-environment に直面します。これはホスト上と同じ理由です。ディストリビューションのインタープリターが、ディストリビューションのマーカーファイルを持っているためです。

それでも、多くのイメージでは venv を使用します。マルチステージビルドを簡単にできるためです。builder ステージでは /opt/venv にインストールし、runtime ステージではそのディレクトリだけをコピーして、コンパイラーを残しません。activate に関する問題も、そのまま引き継がれます。RUN source /opt/venv/bin/activate の行が影響するのは、そのビルドレイヤーの shell だけです。そのため、実行時にはコンテナが system interpreter で起動し、ModuleNotFoundError が発生します。ENV PATH="/opt/venv/bin:$PATH" を設定するか、CMD に絶対パス /opt/venv/bin/gunicorn を指定してください。これは、別のファイルで発生する systemd の問題と同じです。

したがって、コンテナではイメージがインタープリターとその配下のすべてを固定するため、インタープリターに関する問題は置き換わります。しかし、バージョン固定に関する問題は置き換わりません。固定されていない requirements.txt からビルドしたイメージでは、来月になると異なるバージョンが解決される可能性があります。つまり、イメージタグは再現可能でも、それを生成したビルドは再現可能ではありません。uv.lock のような lockfile、または完全に固定した requirements ファイルによって、コンテナを使用するかどうかにかかわらず、この差を埋められます。また、1 台の VPS 上で systemd により 1 つのアプリケーションを実行する場合、コンテナはこの判断を主に Dockerfile へ移すだけです。systemd はすでに、失敗したプロセスを再起動し、その出力を journal に記録するためです。VPS で Docker を実行する は、ビルド済みイメージ自体をデプロイ対象にしたい場合に有効です。

FAQ

--break-system-packages を指定して pip install を実行すればよいですか?

稼働を維持する必要があるサーバーでは避けてください。このフラグはその名前どおりに保護機能を解除し、pip は /usr/local/lib/python3.12/dist-packages に書き込みます。このディレクトリは sys.path の apt 用ディレクトリより先に検索されます。そのため、/usr/bin/python3 で実行されるすべてのシステムスクリプトから、インストールしたバージョンがディストリビューションのバージョンを隠します。一方、apt は自分のバージョンがインストールされていると認識したままなので、何かが壊れるまで競合は検出されません。毎回ゼロから再構築するコンテナイメージ内であれば、影響はそのイメージ内にとどまるため、使用しても差し支えありません。管理対象のマシンでは venv を作成してください。1 コマンドで実行できます。

サーバー上のどこに仮想環境を配置すべきですか?

アプリケーション専用ディレクトリ内に /srv/myapp/.venv として配置し、deploy ユーザーが所有するようにします。サービスアカウントには読み取りと実行の権限だけを付与します。アプリケーションごとに 1 つの venv を使用してください。共有すると、最初のアプリケーションの更新によって 2 つ目のアプリケーションが壊れる可能性があります。作成後の venv を移動またはコピーしないでください。bin/ ディレクトリ内の各スクリプトには、shebang 行にその絶対パスが書き込まれているため、移動した venv は bad interpreter: No such file or directory で失敗します。削除して、requirements.txt から再構築してください。

systemd サービスが ModuleNotFoundError で失敗するのはなぜですか?

unit が venv のインタープリターではないインタープリターを実行しているためです。systemctl cat myapp を実行し、ExecStart を確認してください。そこには、絶対パスで /srv/myapp/.venv/bin/python、または同じ bin/ ディレクトリにある console script を指定する必要があります。unit ファイルで activate を読み込んでも機能しません。ExecStart は shell ではなく、systemd は Failed to locate executable sourcestatus=203/EXEC とともに報告するためです。Environment=PATH=/srv/myapp/.venv/bin:/usr/local/bin:/usr/bin:/bin を追加すると、コードが起動するサブプロセスも venv のツールを見つけられるようになります。

venv と pip の代わりに uv を使用すべきですか?

lockfile が必要な場合、インストール時間が長くて問題になる場合、またはディストリビューションが提供していない Python バージョンが必要な場合は uv を使用してください。uv は通常の venv を作成するため、systemd の unit とファイル配置は変わりません。また、uv sync --frozen により lockfile に記録された内容を正確にインストールできます。1 つのアプリケーションを、固定した requirements.txt を使用して git からデプロイし、インストールが数秒で完了する場合は、python3 -m venv で十分です。サーバー上で更新を続けるバイナリが 1 つ減ります。

#python#venv#pipx#uv#deployment