2026年5月26日
2026年5月26日
Pythonのデプロイメントとパッケージ化の方法|pyproject.tomlでwheelを作りサーバーへ配置する
Python のスクリプトをサーバーで安定して動かすには、「コードをパッケージ(wheel)にまとめる」「依存ライブラリのバージョンを固定する」「サーバー側では仮想環境にインストールして起動する」という3つを分けて考えると整理しやすくなります。この記事では、pyproject.toml を使ったパッケージの作り方、python -m build による wheel の作成、依存関係の固定、サーバーへの配置と systemd での常駐化、Docker イメージにする場合の考え方までを順に説明します。
なぜ「ファイルのコピー」ではなくパッケージにするのか
小さなスクリプトなら、.py ファイルを scp でサーバーに置いて動かすこともできます。ただ、ファイルが増えてくると次のような問題が起きます。
- どのファイルをコピーすればよいか分からなくなり、本番だけ古いファイルが残る
importのパスが開発環境と本番で食い違い、ModuleNotFoundErrorになる- 必要なライブラリやそのバージョンが記録されておらず、サーバーごとに動作が変わる
パッケージにすると、配布物は wheel(.whl)という1つのファイルになり、pip install するだけで必要なファイルと依存ライブラリが揃います。コマンドとして実行する入口も定義できるので、サーバー側での起動方法も統一できます。
プロジェクトの構成
ここでは、ディスク使用率をチェックするコマンド disk-alert を例にします。
disk-alert/
├── pyproject.toml
├── README.md
├── src/
│ └── disk_alert/
│ ├── __init__.py
│ └── cli.py
└── tests/
└── test_cli.py
コードを src/ の下に置く「src レイアウト」にしているのは、テストや動作確認が「インストールされたパッケージ」に対して行われるようにするためです。プロジェクト直下にパッケージを置くと、インストールし忘れていてもカレントディレクトリから読み込めてしまい、配布物にファイルが入っていない問題に気づけません。
pyproject.toml を書く
pyproject.toml は、パッケージ名やバージョン、依存ライブラリ、ビルドに使うツールを記述する標準の設定ファイルです。以前は setup.py に書くのが一般的でしたが、現在はこちらが推奨されています。
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "disk-alert"
version = "0.1.0"
description = "ディスク使用率を確認して警告を出すツール"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"psutil>=5.9",
]
[project.scripts]
disk-alert = "disk_alert.cli:main"
各項目の役割は次のとおりです。
- **
[build-system]**:wheel を作るときに使うビルドツール。ここでは setuptools を使っています。hatchling など別のツールを指定することもでき、どれを使っても[project]の書き方は共通です - **
requires-python**:対応する Python のバージョン。サーバーの Python が古いとインストール時に拒否されるので、本番環境のバージョンと合わせて決めます - **
dependencies**:実行に必要なライブラリ。ここには「この範囲なら動く」という幅を書き、厳密なバージョン固定は後述のロックファイルで行います - **
[project.scripts]**:インストール時に作られるコマンド名と、呼び出す関数。上の例ではdisk-alertと打つとdisk_alert/cli.pyのmain()が実行されます
cli.py は、たとえば次のように書きます。
import sys
import psutil
THRESHOLD = 90
def main() -> int:
percent = psutil.disk_usage("/").percent
print(f"使用率: {percent:.1f}%")
return 1 if percent >= THRESHOLD else 0
if __name__ == "__main__":
sys.exit(main())
main() の戻り値は、コマンドの終了コードになります。しきい値を超えたら 1 を返すようにしておくと、cron や systemd、監視ツールの側で「失敗」として扱えます。
wheel を作る
ビルドには、標準的なビルド用フロントエンドの build パッケージを使います。
python -m venv .venv
source .venv/bin/activate # Windows は .venv\Scripts\activate
pip install --upgrade pip build
python -m build
成功すると dist/ に2つのファイルができます。
disk_alert-0.1.0-py3-none-any.whl:wheel。インストールが速く、通常はこちらを使いますdisk_alert-0.1.0.tar.gz:ソース配布物(sdist)。wheel が使えない環境向け
作った wheel は、別の新しい仮想環境に入れて動作を確認します。開発用の仮想環境で試すと、手元にだけあるファイルやライブラリのおかげで動いてしまうことがあるためです。
python -m venv /tmp/check-env
/tmp/check-env/bin/pip install dist/disk_alert-0.1.0-py3-none-any.whl
/tmp/check-env/bin/disk-alert
ここで ModuleNotFoundError が出る場合は、パッケージのディレクトリに __init__.py が無い、[project.scripts] のモジュール名の綴りが違う、といった原因が考えられます。wheel は zip 形式なので、python -m zipfile -l dist/*.whl で中身のファイル一覧を確認できます。
依存関係のバージョンを固定する
dependencies に psutil>=5.9 と書いただけでは、インストールした時期によって入るバージョンが変わります。本番で再現性を持たせるには、実際に動作確認したバージョンの一覧(ロックファイル)を別に作り、それを使ってインストールします。
手軽な方法は、動作確認した仮想環境で pip freeze を使う方法です。
pip freeze --exclude-editable > requirements.lock.txt
このとき、build やテスト用ツールなど開発にだけ使うライブラリまで記録されないよう、実行に必要なものだけを入れた仮想環境(前の節の確認用環境など)で実行します。出力されたファイルに自分のパッケージ自体の行が含まれていたら、その行は削除しておきます。
ほかにも、pip-tools の pip-compile や、uv の uv lock / uv export のように、依存関係の解決と固定を専用に行うツールがあります。Python のロックファイルの標準形式(pylock.toml)も仕様として定められ、対応するツールが増えつつあります(2026年10月現在)。チームで使うツールを1つに決め、ロックファイルをリポジトリに含めておくと、誰がビルドしても同じ環境になります。
サーバーへ配置する
仮想環境にインストールする
サーバーでは、OS の Python に直接 pip install しません。OS のパッケージ管理と衝突するためで、最近の Debian や Ubuntu では誤操作を防ぐ仕組み(externally-managed-environment のエラー)によって拒否されます。アプリごとに仮想環境を作り、その中にインストールします。
sudo mkdir -p /opt/disk-alert
sudo python3 -m venv /opt/disk-alert/venv
sudo /opt/disk-alert/venv/bin/pip install -r requirements.lock.txt
sudo /opt/disk-alert/venv/bin/pip install --no-deps disk_alert-0.1.0-py3-none-any.whl
ロックファイルで依存ライブラリを先に入れ、wheel 本体は --no-deps で入れると、ロックファイルに無いバージョンが紛れ込むのを防げます。バージョンを上げるときは、新しい wheel を同じ手順で入れ直します。切り戻しに備えて、前のバージョンの wheel とロックファイルはサーバーにも残しておきます。
systemd で定期実行する
disk-alert のように一度実行して終わるコマンドは、systemd のサービス(Type=oneshot)とタイマーを組み合わせて定期実行できます。cron でも構いませんが、systemd にするとログが journal にまとまり、実行履歴を追いやすくなります。
# /etc/systemd/system/disk-alert.service
[Unit]
Description=disk-alert check
[Service]
Type=oneshot
User=diskalert
EnvironmentFile=/etc/disk-alert/env
ExecStart=/opt/disk-alert/venv/bin/disk-alert
# /etc/systemd/system/disk-alert.timer
[Unit]
Description=Run disk-alert every 10 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=10min
[Install]
WantedBy=timers.target
ExecStart には仮想環境の中のコマンドを指定するので、activate は不要です。API キーなどの設定は EnvironmentFile に分け、そのファイルの権限を絞っておきます。実行ユーザーは root ではなく専用のユーザー(ここでは diskalert)を作って使います。ファイルを置いたら sudo systemctl daemon-reload を実行し、sudo systemctl start disk-alert.service で一度手動実行して journalctl -u disk-alert で結果を確認します。問題なければ sudo systemctl enable --now disk-alert.timer でタイマーを有効にし、systemctl list-timers で次回の実行時刻を確認します。
Web アプリやワーカーのように動き続けるプログラムの場合は、Type=simple のサービスにして Restart=on-failure を付け、WantedBy=multi-user.target で自動起動させるのが一般的な形です。
Docker イメージにする場合
サーバーの OS や Python のバージョンに左右されたくない場合は、Docker イメージにして配布する方法もあります。この場合も、上で作った wheel とロックファイルをイメージにコピーしてインストールすると、手元とイメージの中身が揃います。ビルド用のステージで wheel を作り、実行用の小さなイメージにはインストール結果だけを入れる「マルチステージビルド」にすると、イメージのサイズを抑えられます。ベースイメージの Python のバージョンは、pyproject.toml の requires-python と矛盾しないものを選びます。
PyPI に公開する場合の注意
社内で使うツールは、PyPI に公開する必要はありません。社外に公開する場合は、まず TestPyPI でアップロードと pip install を試し、名前の重複や README の表示を確認してから本番の PyPI に上げます。アップロードには twine を使う方法のほか、GitHub Actions などから API トークンを使わずに公開できる Trusted Publishing の仕組みもあります。いずれの場合も、トークンをリポジトリに書かないようにしてください。
まとめ
pyproject.tomlに名前・バージョン・依存関係・コマンドの入口を書き、python -m buildで wheel を作る- src レイアウトにし、wheel は新しい仮想環境に入れて動作を確認する
- 本番の再現性はロックファイル(
pip freeze、pip-tools、uv など)で確保する - サーバーでは OS の Python に直接入れず、アプリごとの仮想環境にインストールする
- 常駐や定期実行は systemd(サービスやタイマー)に任せ、秘密情報は権限を絞った設定ファイルに分ける