# pyeasydeploy
Deploy a Python app to a Linux server with a few lines of Python and no sudo. The server only needs SSH and systemd: the deploy brings everything else.
from pyeasydeploy import App, Key, Server, deploy_app, ensure_python, ensure_uv
server = Server(address="203.0.113.10", user="deploy", auth=Key(path="~/.ssh/id_ed25519"))
python = ensure_python(ensure_uv(server), "3.13")
deploy_app(python, App(name="web", source="./web", command="web"))
That is the whole deploy, and it:
- Brings its own Python. It uploads uv, which installs the Python you ask for. The distribution, its packages and its Python never matter.
- Installs exactly your
uv.lock. The server never picks versions on its own. - Keeps the app running while it installs. Each deploy is a new release, switched to with one restart. A failed install leaves the running one untouched.
- Runs it as a service. systemd keeps it alive and starts it at boot. Its output goes to the journal, which rotates on its own.
- Stays in its folder. Everything lives in
~/.pyeasydeploy, and nothing outside it is ever removed.
Immutable values and plain functions on top of Fabric, made for a few servers and a team that writes Python.
# What a deploy does
The introductory example is a complete deploy. Each call is one step, run over SSH in order, and each returns the value the next one takes:
Serverdescribes the server: its address, user, and either aPasswordor aKey. Nothing connects yet; the value is only validated. The library opens one SSH connection perServerthe first time a function needs it, and reuses it.ensure_uvputs the uv binary in~/.pyeasydeploy/bin/uv. It asks GitHub for the latest release and compares it withuv --versionon the server: when they match, nothing is downloaded. Otherwise your machine downloads the build for the server's processor (x86_64oraarch64) and uploads it.ensure_pythonhas uv install the Python you ask for in~/.pyeasydeploy/python/, upgraded to its latest patch release. The system's Python is never used: a distribution upgrade cannot change or remove the interpreter your app runs on.deploy_apppacks your project, uploads it to a new release directory, and runsuv sync --frozenthere, so the server gets exactly the versions of youruv.lock. Then it pointscurrentat the new release, writes a systemd user service for the app, and restarts it. The two newest releases are kept.
The app runs while the new release installs, and stops only for the restart. Its output goes to the systemd journal, which rotates it on its own: read it with app_logs.
On the server, everything lives in one folder, and the library refuses to remove anything outside it:
~/.pyeasydeploy/
bin/uv
python/ Pythons installed by uv
cache/ uv's cache
apps/web/
releases/20261108T183000Z/ the code and its .venv
current -> releases/20261108T183000Z
pyeasydeploy-web.service the systemd user service
# Requirements
- Your machine: Windows, macOS or Linux, with SSH access to the server and to GitHub.
- Your project: a
pyproject.tomland auv.lock(runuv lockin it). - The server: Linux on
x86_64oraarch64, with SSH, systemd and access to GitHub and PyPI. No Python, no packages, no sudo. - Once, as root on the server:
loginctl enable-linger deploy(with your user), so the user's services start when the server boots and keep running after you log out.deploy_appchecks it first and tells you the command if it is missing.
# Where to go next
- Guide: each part of a deploy in detail, with the options that change it.
- Reference: every public name.
- Reproducibility: how far "same script, same server" reaches.
- Design: the principles, and what the library deliberately does not do.