Forget pip — High-Performance Python Package Management in Rust

Run Python scripts and Telegram bots on Ubuntu via uv: total isolation and zero system bloat.

uv is an extremely fast Python package manager and script runner written in Rust. It was developed by the Astral team (the creators of Ruff). Essentially, it is a replacement for pip, pip-tools, and virtualenv, all combined into a single compact binary.

uv is exactly the solution that allows you to use Python in a “binary that just works” style, without turning your system into a junk pile.

For those accustomed to the PHP stack: uv is something between composer and a standalone PHP binary that can pull in required extensions on the fly without altering the system environment. However, it is important to understand the distinction: while uv is ideal for standalone scripts and Telegram bots, running heavy web interfaces on Flask or Django requires other tools (Gunicorn, Supervisor, etc.). For more information on the architecture of such solutions and choosing the right platform, see the article on Python hosting and Gunicorn + Flask setup.

Why is this necessary?

Specifically, why use it on a VPS with a hosting control panel already installed:

  • Zero Maintenance Overhead: It minimizes the need for system compilers (build-essential), python3-dev, and other dependencies that cause VPS bloat.
  • Out-of-the-box Isolation: It completely ignores the system Python. You will never break the hosting control panel or core Linux system scripts because uv operates within its own cache.
  • Speed: Between 10–100x faster than pip. Dependency installation is nearly instantaneous thanks to efficient caching and parallel execution.
  • One-line Execution: It allows you to run scripts by automatically downloading dependencies into a temporary environment, eliminating the need to manually create a venv beforehand.
  • Resource Efficiency: It manages memory optimally during installation, which is critical for low-end VPS instances prone to OOM (Out Of Memory) errors.

How to install?

According to the official documentation, the installation boils down to downloading a single binary. For system-wide use on a server (ensuring that Fastpanel or other control panels can detect the path), it is best to install it into /usr/local/bin:

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

You will receive a message similar to this:

Получим примерно такое сообщение:

downloading uv 0.10.0 x86_64-unknown-linux-gnu
no checksums to verify
installing to /root/.local/bin
  uv
  uvx
everything's installed!
 
To add $HOME/.local/bin to your PATH, either restart your shell or run:
 
    source $HOME/.local/bin/env (sh, bash, zsh)
    source $HOME/.local/bin/env.fish (fish)

Since uv is a statically compiled Rust binary, we can simply move it to the appropriate directory:

mv /root/.local/bin/uv /usr/local/bin/
mv /root/.local/bin/uvx /usr/local/bin/

To verify that the operating system recognizes the binary, run the version check:

uv --version
# Output: uv 0.10.0

Python Telegram Bot Test Code (aiogram 3.x)

Create a file, for example, script.py. This code is straightforward and utilizes a modern asynchronous library.

import asyncio
import logging
import sys
from aiogram import Bot, Dispatcher, html
from aiogram.filters import CommandStart
from aiogram.types import Message

# Logging configuration for Fastpanel
logging.basicConfig(level=logging.INFO, stream=sys.stdout)

TOKEN = "YOUR_BOTFATHER_TOKEN"
dp = Dispatcher()

@dp.message(CommandStart())
async def command_start_handler(message: Message) -> None:
    """Handler for the /start command"""
    user_name = html.bold(message.from_user.full_name)
    await message.answer(f"Hello, {user_name}! I am a bot on Ubuntu 24.04, running via uv.")

@dp.message()
async def echo_handler(message: Message) -> None:
    """Echo bot: sends back the same message received from the user"""
    try:
        # Send a copy of the message back
        await message.send_copy(chat_id=message.chat.id)
    except TypeError:
        # Handling cases where content cannot be simply copied (e.g., specific stickers)
        await message.answer("I received your message, but I cannot simply copy it.")

async def main() -> None:
    bot = Bot(token=TOKEN)
    logging.info("Bot is now online...")
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

For a traditional run, create a requirements.txt file in the root of the project with the following contents:

aiogram==3.17.0

Integration with Fastpanel (via systemd services)

Now for the most important part: how to run this “cleanly” through the panel interface. Go to Fastpanel → Click the Create Site button and select the systemd template.

Fastpanel systemd site.

Once created, go to the site settings and fill in the fields as shown in the screenshot below:

Fastpanel systemd backend.

You only change the Launch command parameter, I left all the other parameters as default.

/usr/local/bin/uv run --with-requirements requirements.txt tg-to-wp2.py

The Result: Fastpanel creates a systemd unit that will monitor the bot’s status. uv will create its local cache specifically within the project folders rather than cluttering /root. If the bot crashes, the panel will automatically restart it. If you update the code, simply click “Restart” in the Services interface of your project. Best of all, not a single new Python package will appear in your operating system’s global pip or apt repositories.

3 Ways to Launch a Python Application (Launch command)

None of these methods require running pip install manually!

In all cases, uv uses a global cache. This means that if you run 10 different bots on a single server using aiogram, the library will only be downloaded once, but each bot will be isolated. This is a huge savings in disk space on cheap VPS.

Option A: Traditional (requirements.txt). Create a requirements.txt file in the project root with the following content:

aiogram==3.17.0

Then, the launch command in Fastpanel will be:

/usr/local/bin/uv run --with-requirements requirements.txt script.py

Option B: Expert (Inline Script Metadata — PEP 723). This is the cleanest approach, following the “minimum files” philosophy. You define dependencies directly within the .py file. uv reads them and prepares the environment automatically. This way, your project folder remains clutter-free, containing nothing but the script itself.

# /// script
# dependencies = [
#   "aiogram",
# ]
# ///

import asyncio
import logging
import sys

# Rest of your code...

The launch command:

/usr/local/bin/uv run script.py

Option C: The Simple Method (Dynamic). In this case, the --with flag tells uv to automatically fetch the library into a temporary isolated storage before execution. If you need other libraries (e.g., for database connectivity or the WordPress API), simply add them using the –with library_name flag directly in this string.

/usr/local/bin/uv run --with aiogram --with aiohttp tg-to-wp.py

Bonus: Running Console Utilities On-the-Fly

uv is perfectly suited for running service scripts via CRON, as you don’t need to manually activate virtual environments or worry about library search paths.

Example: SSL Certificate Expiry Check Script This script calculates how many days are left before an SSL certificate expires for any given domain.

Create a file named check_ssl.py:

# /// script
# dependencies = [
#   "cryptography",
# ]
# ///

import socket
import ssl
from datetime import datetime
from cryptography import x509
import sys

def get_ssl_expiry_date(hostname):
    context = ssl.create_default_context()
    with socket.create_connection((hostname, 443)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert_bin = ssock.getpeercert(binary_form=True)
            cert = x509.load_der_x509_certificate(cert_bin)
            return cert.not_valid_after_utc

if len(sys.argv) < 2:
    print("Usage: uv run check_ssl.py domain.com")
    sys.exit(1)

domain = sys.argv[1]
expiry = get_ssl_expiry_date(domain)
days_left = (expiry - datetime.now(expiry.tzinfo)).days

print(f"Domain: {domain}")
print(f"Certificate expires: {expiry}")
print(f"Days remaining: {days_left}")

How to run it? Simply execute it from the console (no system-wide installations required):

/usr/local/bin/uv run check_ssl.py linuxx.info

Output Example:

Domain: linuxx.info
Certificate expires: 2026-03-22 03:16:41+00:00
Days remaining: 40

uv will automatically download the cryptography library into its temporary cache, execute the script, and return the result. Why is this convenient?

  • Zero Setup: You don’t need to run pip install cryptography or apt install python3-cryptography.
  • Clean Output: You receive only the script’s output without environment-related noise.
  • Portability: You can drop this script onto any VPS with the uv binary installed, and it will function identically.

Conclusion

Combining uv with Fastpanel is a pragmatic choice for those who value the stability of the PHP stack but require modern Python tooling. This architecture ensures that every script lives in its own “isolated bubble,” preventing conflicts with system packages and allowing management through a familiar GUI. This approach completely eliminates the risk of control panel failures caused by library updates and reduces administration to a “set-and-forget” principle.

Troubleshooting (FAQ)

The service fails to start with “uv: command not found” in Fastpanel logs.

Systemd units often use a restricted PATH. Ensure you are using the absolute path to the binary: /usr/local/bin/uv. Also, verify that the binary has execution permissions: chmod +x /usr/local/bin/uv.

Where does uv store its cache, and will it fill up my disk?

By default, uv stores its cache in the user’s home directory (e.g., /root/.cache/uv). You can clear it using uv cache clean. However, uv is highly efficient and reuses identical wheels across different projects, saving space compared to multiple standard virtualenvs.

Can I use a specific Python version if the system version is outdated?

Yes. uv can manage Python versions independently of the OS. Simply add the version flag to your launch command: uv run –python 3.12 script.py. The first time you run this, uv will download a standalone Python 3.12 binary that doesn’t interfere with /usr/bin/python3.

The bot starts but cannot write logs to the project folder.

This is likely a permissions issue. Fastpanel runs systemd services under a specific user. Ensure the project directory is owned by that user: chown -R user:group /var/www/user/data/www/domain.com/.

Rork

Linux hobbyist into networking and digital privacy. I use this hub to translate and store technical notes on sysadmin tasks and anonymity tools. Tech should work for people, not the other way around.

Rate author
Add a comment