Goals:
Create a Debian package that contains both the virtual environment and the Python interpreter inside.
Why is this project useful? Because you may need to deploy a Debian package and you would like control over the Python interpreter and over your dependencies. Control equals autonomy. Autonomy is needed to getting things done.
Alternatives:
The packaging of Python applications is a world in itself. For a summary read here:
There are of course similar and established projects that tackle the problem that I am trying to solve.
Probably the closest project to this one is dh_virtualenv .
dh_virtualenv allows you to create Python debian packages with the virtual environment inside.
It was a project created by Spotify, and for a long time has been among my favorite tools.
What I like is that it works well with the setup.py, and the executables that setup.py creates –they point to the correct python interpreter (the one inside the virtual environment).
I would like to continue using dh_virtualenv, however I would also like to add the Python interpreter inside the package, and I would like native support for UV. For example, I would like a tool that uses directly the uv.lock instead of me having to compile the uv.lock to a requirements.txt, so that it could be understood by dh_virtualenv.
Also, there is pyinstaller, which actually bundles your code, virtual environment and python interpreter into a single executable. A workflow based on pyinstaller would be to create the virtual environment with uv (including the library pyinstaller), then execute pyinstaller to create the bundled binary (actually a zip file), and finally create the debian package with the bundled binary inside.
Pyinstaller supports several versions of python upto 3.13. Other similar projects are Nuitka an PyOxidizer. These two project produce a single binary from the python project. Nuitka supports Python 3 (3.4 — 3.13) and Python 2 (2.6, 2.7), while PyOxydizer only supports Python 3.8, 3.9, and 3.10.
The downside of Pyinstaller, Nuitka and Pyoxidizer is that despite being interesting projects, in the case of a web development app they are not very useful: There are no clear benefits of “compiling” the code (I am not exactly sure that the code is actually compiled) because the webservers (e.g uwsgi, gunicorn, etc.) work with text files. Hence, despite that there is some merit on distributing your whole app in a single binary the risk of adding an unnecessary project to your pipeline may not be worth it.
The solution
I splitted this project in two parts. In the first, I had to learn how to insert a Python interpreter (installed via uv) inside a Debian package.
You can read that project here:
You can clonee the first project here:
The second part builds upon the first project, but in this case its not only the python interpreter, but the virtual environment and the project code.
You can clone the project here:
Basically we have a small fastapi app that we want to package in a debian format, with the virtual environment and the python interpret inside:
import sys
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
print(sys.version)
return {"message": f"Hello World using python {sys.version}"}
The project dependencies are given in pyproject.toml
[project]
name = "debian-uv-python"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"fastapi[standard]>=0.115.12",
]
We also pinned the python version to be 3.9
cat ./.python-version
3.9
And here is the makefile that provides the solution:
PREFIX = /opt/venvs/debianuvpython/
SHELL = /bin/bash
.ONESHELL:
default: help
install:
set -eu
curl -LsSf https://astral.sh/uv/0.7.5/install.sh | sh
source $$HOME/.local/bin/env
# Declare venv --relocatable so that the executables in the venv do not have the python interpreter hardcoded
uv venv --relocatable
uv sync
# Get the path of the interpreter used/created by UV
python_interpreter_dir_to_copy=$$(uv python list --only-installed | awk '/debian/ {gsub(/bin\/python3.9/, ""); print $$2}')
name_of_python_interpreter_to_copy=$$(uv python list --only-installed | awk '/debian/ {gsub(/bin\/python3.9/, ""); print $$1}')
mkdir -p $(DESTDIR)$(PREFIX)
cp -R $${python_interpreter_dir_to_copy} $(DESTDIR)$(PREFIX)
# Fix the dynamic libraries of the python interpreter
patchelf --replace-needed "\$$ORIGIN/../lib/libpython3.9.so.1.0" "$(PREFIX)/$${name_of_python_interpreter_to_copy}/lib/libpython3.9.so.1.0" $(DESTDIR)$(PREFIX)/$${name_of_python_interpreter_to_copy}/bin/python3
patchelf --replace-needed "\$$ORIGIN/../lib/libpython3.9.so.1.0" "$(PREFIX)/$${name_of_python_interpreter_to_copy}/lib/libpython3.9.so.1.0" $(DESTDIR)$(PREFIX)/$${name_of_python_interpreter_to_copy}/lib/libpython3.so
cp -R ./.venv/* $(DESTDIR)$(PREFIX)
# Fix the symbolic links
rm -f $(DESTDIR)$(PREFIX)/bin/python && ln -s ../$${name_of_python_interpreter_to_copy}/bin/python $(DESTDIR)$(PREFIX)/bin/python
# Copy the actual code of the application
cp -R ./example_fastapi $(DESTDIR)$(PREFIX)
clean:
rm -rf ./.venv
rm -rf ./debian/debianuvpython
rm -rf ./debian/.debhelper
DOCKER_RUN := docker run -v$$PWD:/home/debian-only-uv-python-1.0.0 debian:bullseye
build:
$(DOCKER_RUN) bash -c "apt-get update && apt-get install -y devscripts debmake build-essential patchelf && cd /home/debian-only-uv-python-1.0.0 && debmake -y --native; cat debian/rules | awk '/override_dh_shlibdeps:/ {exit 0} END {exit 1}' || echo -e 'override_dh_shlibdeps:\n\tdh_shlibdeps -Xcpython-3.9.22-linux-x86_64-gnu' >> debian/rules; debuild && cp ../*deb ./"
help:
echo "This is debian loves uv"
What this Makefile does is to leverage the dh_autoinstall debhelper. When dpkg sees a makefile, dh_autoinstall will execute make install as part of building the package. This gives your the space to put any low level logic there.
In my case I use for:
- Install uv
- Create the virtual environment and install the python interpreter
- Patch the ELF executable of the Python interpreter
- Copy the application code and fix some symlinks
Perhaps, what is key of this makefile (not mentioning the low level details discussed in the previous project https://camilomatajira.wordpress.com/2025/05/19/create-debian-package-out-of-a-python-installed-via-uv/) is the need to use uv venv –relocatable
so that the executables of the virtual environment point to the correct python interpreter.
How to build
I provide the following utility to help you build/try the project.
make build
Results
Start by executing:
make build
(...)
W: debian-uv-python: unusual-interpreter opt/venvs/debianuvpython/lib/python3.9/site-packages/markdown_it/cli/parse.py #!python
W: debian-uv-python: wrong-bug-number-in-closes #nnnn in the installed changelog (line 3)
Finished running lintian.
Then, let’s install it in a new container and execute uvicorn!
docker run -it -v$PWD:/home -p 8000:8000 --rm debian:bookworm /bin/bash
root@a047cf292d66:/# cd home/
root@a047cf292d66:/home# dpkg -i debian-uv-python_1.0.0_amd64.deb
Selecting previously unselected package debian-uv-python.
(Reading database ... 6091 files and directories currently installed.)
Preparing to unpack debian-uv-python_1.0.0_amd64.deb ...
Unpacking debian-uv-python (1.0.0) ...
Setting up debian-uv-python (1.0.0) ...
Processing triggers for libc-bin (2.36-9+deb12u10) ...
root@a047cf292d66:/home# cd /opt/venvs/debianuvpython/example_fastapi/
root@a047cf292d66:/opt/venvs/debianuvpython/example_fastapi# /opt/venvs/debianuvpython/bin/uvicorn --host 0.0.0.0 main:app
INFO: Started server process [38]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Now in you local:
curl http://localhost:8000
{"message":"Hello World using python 3.9.22 (main, Apr 9 2025, 04:03:41) \n[Clang 20.1.0 ]"}%
You see, we get the response 3.9.22!
Conclusion
We were able to create a Debian package, with the code of a simple fastapi app, with a specific python interpreter and its virtual environment.
Uvicorn, which we used as webserver, was installed using uv!
However, this project is still a little bit raw. For example I hardcoded several mentions of python3.9, there is still no support for the setup.py executables. Let’s wait for the next time!