How to Package a Self-Contained Python Project as a Deb File

How to Package a Self-Contained Python Project as a Deb File EduTechBuddy.com © edutechbuddy.com
How to Package a Self-Contained Python Project as a Deb File © edutechbuddy.com
Learn how to create a fully self-contained Python runtime and bundle your project into a .deb package for easy installation on Ubuntu, using a workflow adapted from AppImage packaging.

Linux developers often run into dependency headaches and system conflicts when distributing Python applications. For those aiming to ship a Python desktop app that launches without extra setup, building a self-contained runtime and turning it into a .deb file offers a straightforward path. This guide details how to assemble a portable Python environment, include all dependencies, and produce a Debian package ready for Ubuntu installation.

Why Build a Self-Contained Python Runtime?

Python projects tied to system interpreters and libraries can fail if users have mismatched package versions or missing dependencies. By packaging your own Python interpreter and all needed packages—including PyQt6 and WebEngine—you keep your app running the same way across target systems. This method works well for desktop apps with complex dependencies or graphical interfaces. The Python Packaging User Guide notes that a distributable Python project should declare its metadata and dependencies in a standard config file, usually pyproject.toml. This difference between project packaging and just creating an environment matters when turning an app into a system-installable package.

While a self-contained runtime cuts down on reliance on the target machine’s Python packages, it still requires compatibility with native libraries, CPU architecture, filesystem paths, and the Linux distribution. Think of this method as application bundling layered on top of Python packaging, not a substitute for Debian’s native dependency system.

Required Tools and Dependencies

Before you begin, check that your Ubuntu system has the right build tools and libraries. Install dpkg-deb for packaging, uv for Python environment management, and the system libraries needed for Qt and WebEngine. Set up your environment with this command:

sudo apt update
sudo apt install -y dpkg-dev fakeroot curl patchelf file libgl1 libglib2.0-0 libfontconfig1 libx11-6 libxkbcommon0 libdbus-1-3

If uv is missing, install it:

curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env

uv is a fast Python package and project manager. It handles installing packages from PyPI, creating virtual environments, running scripts and command-line tools, and installing Python interpreters. The usual workflow: create a project, add dependencies, resolve them, and install them into a project environment. The uv sync command aligns the environment with the project’s lock file, giving collaborators or build systems a reproducible dependency state. This model is common in institutional computing, as shown by the Rosen Center for Advanced Computing at Purdue University.

Building the Self-Contained Runtime

Start by making a dedicated directory for your runtime. This process follows the logic of AppImage packaging but targets a Debian package layout. Set up your project variables and directories:

PKG="uniclare-client"
VER="3.0.0"
ARCH="amd64"
RUNTIME="runtime"
rm -rf "$RUNTIME"
mkdir -p "$RUNTIME/usr/lib/unidesk"

Copy the Python 3.12 interpreter managed by uv into your runtime directory. The uv venv command lets you create an environment with a chosen Python version, for example, uv venv myenv --python 3.12. This records the interpreter version as part of environment creation, which matters for reproducibility and compatibility.

PY_ROOT=$(dirname $(dirname $(readlink -f $(uv python find 3.12 --no-project))))
echo "PY_ROOT=$PY_ROOT"
test -x "$PY_ROOT/bin/python3" && echo "OK: interpreter exists"
cp -a "$PY_ROOT"/. "$RUNTIME/usr/lib/unidesk/python/"

Build your project wheel and any dependency wheels. If your project uses pyproject.toml, run:

uv build --wheel
mkdir -p build/wheels
uv export --frozen --no-dev --no-emit-project --extra desktop --no-hashes -o build/requirements.txt

Install all dependencies into the runtime using the copied Python interpreter:

uv pip install --python "$RUNTIME/usr/lib/unidesk/python/bin/python3" \
  --target "$RUNTIME/usr/lib/unidesk/site-packages" \
  --no-deps --no-build \
  --find-links build/wheels \
  -r build/requirements.txt dist/uniclare_client-3.0.0-py3-none-any.whl

Purdue University’s workflow starts with uv init, then uv add for dependency declaration and uv run for execution. uv add records the dependency, resolves it, and installs it into the project's .venv. uv sync restores the environment to the lock-file state. This lets the build be recreated from a concrete dependency resolution, not from an unconstrained install command.

Pruning the Runtime for Size

Bundling a full Python environment and all dependencies can make the package large. To shrink the final size, use a pruning script adapted from your AppImage workflow. This script removes extra files, test data, and unused Qt components. Adjust paths as needed for your runtime directory. For a detailed example, see the earlier breakdown of batch processing scripts.

Creating the AppRun Launcher

The AppRun script serves as the entry point for your application. Place it in the runtime directory and set it up to launch your main Python module with the right environment variables. Make it executable:

cat > "$RUNTIME/AppRun" <<'EOF'
#!/bin/sh
HERE="$(dirname \"$(readlink -f \"$0\")\")"
U="$HERE/usr/lib/unidesk"
unset PYTHONHOME PYTHONSTARTUP
export PYTHONPATH="$U/site-packages"
export PYTHONNOUSERSITE=1 PYTHONDONTWRITEBYTECODE=1 PYTHONSAFEPATH=1
export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
export PYWEBVIEW_GUI=qt QT_API=pyqt6 QTWEBENGINE_DISABLE_SANDBOX=1
export QTWEBENGINEPROCESS_PATH="$U/site-packages/PyQt6/Qt6/libexec/QtWebEngineProcess"
export QTWEBENGINE_RESOURCES_PATH="$U/site-packages/PyQt6/Qt6/resources"
exec "$U/python/bin/python3" -m uniclare_client.interfaces.desktop "$@"
EOF
chmod +x "$RUNTIME/AppRun"

Test the runtime by running ./runtime/AppRun before moving forward.

Building the Debian Package Structure

Set up the standard Debian package directory layout. Move your entire runtime into /opt/uniclare-client/ inside the package structure. Add desktop integration files like the .desktop launcher, SVG icon, and metainfo XML. Update the Exec line in the desktop file to point to /usr/bin/uniclare-client.

Create a simple shell script at /usr/bin/uniclare-client that launches /opt/uniclare-client/AppRun. Make sure all scripts and directories have the right permissions.

Writing the Control File and Building the Package

The DEBIAN/control file lists package metadata and dependencies. Since your Python runtime and PyQt6 are bundled, only include the essential system libraries needed for Qt and WebEngine. Set permissions and build the package using dpkg-deb:

dpkg-deb --build --root-owner-group deb-build/$PKG "uniclare-client_${VER}_${ARCH}.deb"

When assembling a bundled application, record the exact Python version, dependency versions, target architecture, and operating-system baseline. These details affect wheel selection and native-extension compatibility. The common model in institutional environments is project metadata plus a lock file plus a managed .venv, not an assumption that all dependencies are portable without system support. For desktop apps using Qt or WebEngine, native components may need graphics, font, DBus, X11, and other host libraries. A .deb with a private Python runtime can improve reproducibility, but still requires explicit runtime-library testing on each supported Ubuntu release.

Installing and Testing the Package

Install your new package with sudo dpkg -i uniclare-client_3.0.0_amd64.deb. If any dependencies are missing, fix them with sudo apt install -f. You can now launch your app from the system menu or by running uniclare-client in the terminal. To remove the package, use sudo dpkg -r uniclare-client.

Key Differences from AppImage Packaging

The runtime construction process closely follows AppImage workflows, but the main differences are the directory structure and packaging tool. The runtime goes in /opt/uniclare-client/ and launches via a shell script in /usr/bin/, with dpkg-deb handling packaging instead of appimagetool. If your AppRun script works in the runtime directory, the resulting .deb package will run as expected on Ubuntu systems.

For more technical details on Python project packaging and distribution, see the Python Packaging User Guide and the Purdue University RCAC documentation on using uv for environment and dependency management.

Camila Ferreira Editor EduTechBuddy.com
Co-Founder

Camila Ferreira

Camila Ferreira is the Co-Founder of EduTechBuddy, where her editorial focus is programming education and practical software development skills. Her primary subjects include Python programming, Git version control, and the fundamental concepts beginners need to understand when working with code.