App Deployment·7 min read·

Python 3 venv and pip on Ubuntu VPS the Boring Way

Install Python 3, create an isolated venv, and pin pip packages on Ubuntu so your VPS app stays reproducible without polluting the system interpreter.

NB

Netbay Engineering

Netbay Engineering

On this page

The fastest way to wreck a Python deploy on Ubuntu is to pip-install into the system interpreter. Debian and Ubuntu ship Python for apt, so mixing project packages with distro packages produces version fights, broken apt tools, and a server you cannot rebuild six months later. The boring path is still the right one: install Python from Ubuntu, make a dedicated user, create a virtual environment, pin every requirement, and never run sudo pip again.

This walkthrough uses Ubuntu 24.04 on a single VPS in Lucknow. It is the same layout you want before Gunicorn, Uvicorn, systemd, or any deploy script. Nothing here is clever. That is the point. Clever Python installs are how you inherit a snowflake box that only one person can rebuild.

Install the Distro Python, Then Stop Touching It

Ubuntu 24.04 ships Python 3.12. Install the venv and pip helpers from apt so you do not bootstrap pip with a curl-to-shell script that ignores apt security updates.

bash
sudo apt update
sudo apt install -y python3 python3-venv python3-pip python3-dev build-essential
python3 --version
python3 -c "import sys; print(sys.executable)"

python3-dev and build-essential exist for wheels that compile C extensions: psycopg, Pillow, ujson, cryptography. Skip them and the first pip install of a real app fails with a missing gcc error you will debug at 2 a.m. That is the most common it-worked-on-my-laptop failure on a fresh VPS.

Do not replace this interpreter with a third-party PPA unless you have a hard version pin that apt cannot meet. The distro Python is patched for the life of the Ubuntu release. A custom CPython build means you own security updates, ABI compatibility, and the next OpenSSL bump. For a typical web app that is unpaid work.

The version check must print /usr/bin/python3, not a leftover pyenv shim from a previous experiment. If sys.executable points anywhere else, fix PATH before you create a venv.

Dedicated User, Dedicated Directory

Run the app as a non-root user. Root plus a web framework is how a missed injection or a template bug becomes a full-box compromise. A system user with a home directory is enough. You do not need a human login, but /bin/bash makes debugging with sudo -u app easier than nologin.

bash
sudo useradd --system --create-home --shell /bin/bash app
sudo mkdir -p /srv/app
sudo chown app:app /srv/app
sudo -u app -H bash -lc "cd /srv/app; python3 -m venv .venv"
sudo -u app -H /srv/app/.venv/bin/pip install --upgrade pip wheel

The venv lives next to the project, not in /usr and not in a mystery /opt/venvs name. That keeps upgrades boring: stop the service, delete .venv, recreate it, reinstall from a pin file, start the service. The system Python never learns your project's Django version, and apt never tries to help by upgrading a dist-package you depended on.

Ownership matters. If root creates the venv, the app user cannot install plugins later, and you will be tempted to chmod 777. Fix the owner once. High-Speed SSD local disk is where the venv belongs; do not put bytecode caches on a network mount.

Pin What You Install

A floating requirements.txt that lists Flask without a version is a time bomb. Pin every package, including transitive ones. The day PyPI ships a broken urllib3, you want yesterday's wheel, not a surprise.

bash
sudo -u app -H /srv/app/.venv/bin/pip install "flask==3.0.3" "gunicorn==22.0.0"
sudo -u app -H /srv/app/.venv/bin/pip freeze > /srv/app/requirements.lock
sudo -u app -H /srv/app/.venv/bin/pip install -r /srv/app/requirements.lock

Commit the lock file. On the next machine, including the next Netbay VPS you clone, install from that freeze. Do not run pip install flask on production and hope PyPI still serves the same wheel it served last month.

pip-tools or uv can generate the lock from a smaller requirements.in. Either is fine. The invariant is production installs from a full freeze, not from a wish list. When you add a package, add it on a staging clone, freeze again, and review the diff. Transitive upgrades hide in that diff: a one-line Flask bump can drag six other libraries with it.

Hash pinning with pip install --require-hashes is available if you need extra supply-chain caution. Most small teams start with freeze and add hashes when they start mirroring packages.

PATH, Shebang, and sudo

Always call the venv binary by absolute path. Do not activate the venv in systemd units. Activation is a shell convenience that exports VIRTUAL_ENV and prepends PATH. systemd is not an interactive shell and will not source your bashrc.

  • /srv/app/.venv/bin/python is the interpreter.
  • /srv/app/.venv/bin/pip is the installer.
  • /srv/app/.venv/bin/gunicorn is the process you will supervise later.

If a cron job or a sudo invocation uses /usr/bin/python3, you silently ran the distro interpreter. That is the bug you will spend an hour naming because the import error looks like a missing package rather than a wrong binary. Put the absolute path in every unit file, every cron line, and every deploy script.

A quick check is /srv/app/.venv/bin/python -c "import sys; print(sys.prefix)" — it must print /srv/app/.venv, never /usr.

What Not To Do

Avoid these shortcuts even when a tutorial from 2018 recommends them.

  • sudo pip install anything. It writes into dist-packages and can break apt python tools.
  • --break-system-packages as a habit. Debian marked the system Python as externally managed. That warning is the feature, not an obstacle.
  • Installing the app as root and then chmod -R 777 /srv/app. Fix ownership instead.
  • Mixing poetry, pipenv, and raw pip on the same box without documenting which one is canonical.

A 2 GB Ubuntu VPS with Intel Xeon Platinum is plenty for this layout. You are not compiling CPython from source. You are making a directory, a user, and a lock file. L3/L4 DDoS filtering on the VPS does not change any of this; application process hygiene is still yours.

Boring Python layout on one Ubuntu VPS apt python3 /usr/bin, never pip venv /srv/app/.venv pip freeze requirements.lock app user process absolute venv paths only systemd later no source activate

Takeaway

A production Python install on Ubuntu is three directories and a lock file: the distro interpreter you never pollute, a venv owned by a service user, and a freeze you can reinstall blindly. Everything else in this series assumes that layout. If a later step fails, check which python binary ran before you change application code.

You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and follow along — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance