How to Install Python 3 on Ubuntu 24.04 VPS: venv, uv, pyenv, and Production
Python is the default language for web backends, automation, data pipelines, and AI workloads -- and Ubuntu 24.04 LTS ships with a well-integrated Python 3.12 stack out of the box. But installing python3 alone is only the first step. To run Python cleanly on a production VPS you need to understand virtual environments, the PEP 668 "externally-managed" rules that now block sudo pip install, faster modern tooling like uv and pipx, and how to wrap your app in Gunicorn plus a systemd unit behind Nginx. This guide walks through all of it, from a fresh SSH session to a hardened Python service.
Want a Python-ready VPS in one click? Our CloudCore Starter plan comes with Ubuntu 24.04 LTS, full root access, and enough headroom to run Flask, FastAPI, or Django in production. Deploy in under 60 seconds and start coding.
Table of Contents
What Ships on Ubuntu 24.04
Ubuntu 24.04 LTS ("Noble Numbat") ships with Python 3.12.3 as the default system interpreter, installed as /usr/bin/python3. This is the version that runs apt, cloud-init, netplan, and most of the other system tooling, which is exactly why you must never touch it with sudo pip install -- more on that in Step 4.
For most web and API workloads, Python 3.12 is already an excellent choice: it's roughly 5% faster than 3.11 on typical benchmarks, supports modern syntax like type statement aliases (PEP 695), and has first-class support in every major framework. You only need a different interpreter if you are targeting a specific version for compatibility testing (via pyenv) or shipping a library that must support 3.10 through 3.13.
What Ubuntu 24.04 does not give you out of the box is pip and venv -- those live in separate packages (python3-pip and python3-venv) that you install in Step 3.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server (PuTTY on Windows, or the built-in terminal on macOS/Linux)
- At least 1 GB of RAM (2 GB+ recommended for compiling wheels or running small web apps)
- At least 5 GB of free disk space for the interpreter, venvs, and dependencies
Recommended Plan: CloudCore Starter>
For running one or two Python services -- a Flask API, a FastAPI backend, a cron-driven scraper, or a Django blog -- we recommend the CloudCore Starter plan:>
- 2 vCPU cores
- 4 GB RAM
- 50 GB NVMe SSD
- Unmetered bandwidth
- Ubuntu 24.04 LTS pre-installed>
Plenty of headroom for a venv, Gunicorn workers, and a reverse proxy. Scale up to CloudCore Professional when you add a database, a background worker, or heavy ML dependencies.
Connect to your server via SSH to get started:
ssh root@your-server-ipStep 1: Update System Packages
Start by refreshing your package index and applying any pending security updates. This keeps Python's underlying dependencies (OpenSSL, libffi, zlib) on the latest patch level.
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.If the kernel was upgraded, reboot before continuing:
sudo rebootThen reconnect via SSH after a minute.
Step 2: Verify the Bundled Python
Ubuntu 24.04 already has Python 3.12 installed. Confirm it:
python3 --versionExpected output:
Python 3.12.3Check where it lives:
which python3Expected output:
/usr/bin/python3Note that there is no python command by default -- only python3. If a script or Makefile expects the bare python name, install the compatibility shim:
sudo apt install -y python-is-python3This package creates /usr/bin/python as a symlink to python3. It is safe to install on a fresh server, but avoid it on servers that still run legacy Python 2 code.
Step 3: Install python3-venv, python3-pip, and Build Dependencies
Ubuntu splits Python into several packages. You need three of them:
python3-venv-- lets you create isolated virtual environments withpython3 -m venvpython3-pip-- the package installer itselfpython3-devandbuild-essential-- headers and a C toolchain, required when a dependency ships a C extension and needs to compile a wheel
psycopg2, lxml, cryptography, and Pillow need:sudo apt install -y python3-venv python3-pip python3-dev \
build-essential libssl-dev libffi-dev \
libxml2-dev libxslt1-dev zlib1g-dev \
libjpeg-dev libpq-dev pkg-configExpected output ends with something like:
Setting up python3-pip (24.0+dfsg-1ubuntu1) ...
Setting up python3-venv (3.12.3-0ubuntu1) ...
Processing triggers for man-db (2.12.0-4build2) ...Verify pip is available:
python3 -m pip --versionExpected output:
pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12)You now have everything needed to create virtual environments. But before you do, it's important to understand why you can't just sudo pip install on Ubuntu 24.04.
Step 4: Understand PEP 668 (Why sudo pip install Fails)
If you try to install a package globally on Ubuntu 24.04, you'll hit this error:
sudo pip install requestserror: externally-managed-environment× This environment is externally managed ╰─> To install Python packages system-wide, try apt install python3-xyz, where xyz is the package you are trying to install.
If you wish to install a non-Debian-packaged Python package, create a virtual environment using python3 -m venv path/to/venv.
This is PEP 668 at work. Ubuntu (like Debian, Fedora, and most modern distros) marks its system Python as "externally managed" by dropping a marker file at /usr/lib/python3.12/EXTERNALLY-MANAGED. The purpose is to stop you from clobbering apt-installed packages with pip-installed ones, which can silently break apt, unattended-upgrades, cloud-init, and other critical tools.
You have three safe options for installing Python packages on Ubuntu 24.04:
pipx for standalone CLI tools that should be available globally -- Step 6apt when a Debian package exists (e.g. sudo apt install python3-requests)Do not use the --break-system-packages flag or delete the EXTERNALLY-MANAGED file. Both will eventually corrupt your system Python.
Step 5: Create and Use a Virtual Environment
A virtual environment is a self-contained directory with its own python binary and its own site-packages. Every project should have one. Packages installed inside it never touch the system Python.
Create a project directory and a venv inside it:
mkdir -p ~/projects/myapp
cd ~/projects/myapp
python3 -m venv .venvThis creates a .venv/ folder containing a private copy of Python 3.12, pip, and the activation scripts.
Activate the environment:
source .venv/bin/activateYour shell prompt will change to show the active venv:
(.venv) user@server:~/projects/myapp$Now pip install works without sudo and without touching the system:
pip install --upgrade pip
pip install requests flaskExpected output:
Successfully installed flask-3.0.3 requests-2.32.3 ...Verify what's installed:
pip listExpected output:
Package Version
----------- -------
blinker 1.8.2
click 8.1.7
Flask 3.0.3
requests 2.32.3
...When you are done working on the project, deactivate the venv:
deactivateWhy the .venv naming convention?
Prefixing the directory with . hides it in ls, keeps it out of most editor file trees, and matches what tools like Poetry, Hatch, and uv create by default. .venv/ should always be in your .gitignore -- never commit the environment itself, only the requirements.txt or pyproject.toml that reproduces it.
Step 6: Install pipx for Global CLI Tools
Some Python packages ship a CLI you want available system-wide -- linters (ruff, black), build tools (poetry, hatch), or utilities (httpie, yt-dlp). You don't want to activate a venv every time you run them, but you also can't install them globally because of PEP 668.
The answer is pipx, which installs each CLI tool into its own isolated venv under ~/.local/pipx/ and symlinks the executables into ~/.local/bin/. You get global commands without polluting the system Python.
Install pipx from apt:
sudo apt install -y pipx
pipx ensurepathpipx ensurepath adds ~/.local/bin to your PATH. Reload your shell:
source ~/.bashrcInstall a CLI tool:
pipx install httpie
pipx install ruff
pipx install poetryExpected output:
installed package httpie 3.2.2, installed using Python 3.12.3
These apps are now globally available
- http
- https
- httpie
done! ✨ 🌟 ✨Run them from anywhere:
http GET https://api.github.com
ruff check .List everything pipx manages:
pipx listUpgrade a tool:
pipx upgrade ruffUninstall:
pipx uninstall httpieUse pip/venvs for your app's libraries. Use pipx for standalone tools you invoke from the command line. The distinction matters.
Step 7: Install uv -- the Fast Modern Resolver
uv is a Python package manager written in Rust by Astral (the team behind ruff). It is a drop-in replacement for pip, pip-tools, virtualenv, and pyenv, and it is 10-100x faster at resolving and installing dependencies. On a cold install of a Django project, uv finishes in a few seconds where pip takes a minute.
Install it with the official installer:
curl -LsSf https://astral.sh/uv/install.sh | shExpected output:
installing to /root/.local/bin
uv
uvx
everything's installed!Reload your PATH:
source ~/.bashrcVerify:
uv --versionExpected output:
uv 0.4.27Create a project and venv with uv
uv replaces both python3 -m venv and pip install:
mkdir -p ~/projects/fastapp
cd ~/projects/fastapp
uv venvThis creates a .venv/ in milliseconds. Activate it as usual, then install packages with uv pip:
source .venv/bin/activate
uv pip install fastapi uvicorn[standard]Expected output:
Resolved 14 packages in 42ms
Installed 14 packages in 89ms
+ fastapi==0.115.0
+ uvicorn==0.31.0
...The modern workflow: uv init + uv sync
For new projects, skip requirements.txt entirely and use uv's project workflow:
uv init myapi
cd myapi
uv add fastapi uvicorn
uv run uvicorn main:app --reloadThis creates a pyproject.toml with your dependencies, generates a uv.lock file (fully reproducible cross-platform lockfile), and runs commands inside the project venv automatically -- no manual activation needed.
To reinstall the exact locked versions on another machine:
uv syncuv sync is the equivalent of pip install -r requirements.txt but faster, deterministic, and driven by the lockfile. This is the recommended workflow for any new Python project in 2026.
Step 8: Install Alternate Python Versions with deadsnakes
Ubuntu 24.04 only packages Python 3.12. If you need 3.10, 3.11, or 3.13 (for example, to match a production cluster or test your library across versions) you have two options: the deadsnakes PPA for system-wide installs, or pyenv (Step 9) for per-user, per-project installs.
The deadsnakes PPA is a long-running Ubuntu PPA that backports every supported CPython version.
Add the PPA:
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt updateInstall Python 3.11 alongside the existing 3.12:
sudo apt install -y python3.11 python3.11-venv python3.11-devVerify both versions are available:
python3.11 --version
python3.12 --versionExpected output:
Python 3.11.10
Python 3.12.3Create a venv using the older interpreter:
python3.11 -m venv ~/projects/legacyapp/.venvUse deadsnakes when you need a specific Python version installed system-wide (e.g. for a systemd service) and you're comfortable with a trusted PPA. For local development across many versions, pyenv is usually a better fit.
Step 9: Multi-Version Development with pyenv
pyenv installs CPython versions into your home directory and lets you switch between them on a per-shell or per-directory basis. It's the tool of choice when you're maintaining libraries that need to work across 3.9, 3.10, 3.11, 3.12, and 3.13.
Install the build dependencies (already done in Step 3 if you followed along) plus a few more pyenv needs:
sudo apt install -y make libbz2-dev libreadline-dev libsqlite3-dev \
libncurses-dev liblzma-dev tk-dev xz-utilsInstall pyenv with the official installer:
curl https://pyenv.run | bashAdd pyenv to your shell:
cat >> ~/.bashrc << 'EOF'
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
EOF
source ~/.bashrcVerify:
pyenv --versionExpected output:
pyenv 2.4.17List available Python versions:
pyenv install --list | grep " 3\.13"Install Python 3.13:
pyenv install 3.13.0This compiles CPython from source (takes 2-5 minutes on a modest VPS). When it finishes, the binary is at ~/.pyenv/versions/3.13.0/bin/python3.13.
Set a version globally for your user:
pyenv global 3.13.0
python --versionOr set it just for one project:
cd ~/projects/myapp
pyenv local 3.13.0This creates a .python-version file in the project directory. Any shell that enters the directory automatically uses that Python version. Combine pyenv with uv venv --python 3.13.0 to get fast venvs on arbitrary interpreter versions.
Step 10: Deploy a Production App with Gunicorn + systemd + Nginx
Running python app.py or flask run is fine for development, but it's single-threaded, lacks process supervision, and terminates when your SSH session drops. For production, the standard pattern on Ubuntu is:
Nginx (public TLS + static files) → Gunicorn (WSGI process manager + workers) → your Flask/FastAPI/Django app
For async frameworks like FastAPI, swap Gunicorn's default sync workers for uvicorn workers (gunicorn -k uvicorn.workers.UvicornWorker), or run uvicorn directly under systemd.
Create the app and venv
sudo useradd -r -m -s /bin/bash myapp
sudo -u myapp bash
cd ~
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install flask gunicorn python-dotenvCreate a minimal app.py:
cat > ~/app.py << 'EOF' import os from flask import Flask from dotenv import load_dotenvload_dotenv()
app = Flask(__name__)
@app.route("/") def hello(): env = os.getenv("APP_ENV", "unset") return f"Hello from Flask on Ubuntu 24.04 -- env={env}\n" EOF
Test it locally:
~/.venv/bin/gunicorn --bind 127.0.0.1:8000 app:appIn another SSH session:
curl http://127.0.0.1:8000Expected output:
Hello from Flask on Ubuntu 24.04 -- env=unsetStop with Ctrl+C and exit back to your admin user.
Worker count formula
Gunicorn's documentation recommends (2 × vCPU) + 1 workers for sync workloads. On a 2 vCPU VPS that's 5 workers, on a 4 vCPU VPS it's 9. Each worker holds its own Python interpreter and app state in memory -- budget roughly 40-100 MB per worker depending on your dependencies.
For I/O-bound apps (lots of database or HTTP calls), use gthread workers with multiple threads, or switch to uvicorn async workers.
Create the systemd service
As root, create /etc/systemd/system/myapp.service:
sudo tee /etc/systemd/system/myapp.service > /dev/null << 'EOF' [Unit] Description=MyApp Flask Gunicorn service After=network.target[Service] Type=notify User=myapp Group=myapp WorkingDirectory=/home/myapp EnvironmentFile=/etc/myapp/env ExecStart=/home/myapp/.venv/bin/gunicorn \ --workers 5 \ --bind 127.0.0.1:8000 \ --access-logfile - \ --error-logfile - \ app:app Restart=on-failure RestartSec=5s
Hardening
NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=read-only ReadWritePaths=/home/myapp
[Install] WantedBy=multi-user.target EOF
Store secrets in EnvironmentFile
Never bake secrets into the systemd unit or your repo. Put them in a root-owned file readable only by the service user:
sudo mkdir -p /etc/myapp
sudo tee /etc/myapp/env > /dev/null << 'EOF'
APP_ENV=production
DATABASE_URL=postgresql://user:secret@localhost/mydb
SECRET_KEY=change-this-to-a-long-random-string
EOF
sudo chmod 600 /etc/myapp/env
sudo chown root:myapp /etc/myapp/envpython-dotenv in your app can read the same file during local development, and systemd's EnvironmentFile= directive injects it as real process environment variables in production -- the secrets never appear in ps auxe output from other users.
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappExpected output:
● myapp.service - MyApp Flask Gunicorn service
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-04-16 11:00:00 UTC; 3s ago
Main PID: 2345 (gunicorn)
Tasks: 6 (limit: 4567)
Memory: 124.5MPut Nginx in front
Install Nginx and add a reverse proxy config. For the full walkthrough, see our How to Install Nginx on Ubuntu 24.04 guide. The minimal config for this app:
server { listen 80; server_name myapp.example.com;
location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
Add TLS with Certbot, reload Nginx, and your Python app is now serving HTTPS through a hardened systemd unit. For deeper systemd patterns (socket activation, timers, restart policies), see our systemd service guide.
Dependency Management: requirements.txt vs pyproject.toml
Python has three common ways to declare dependencies. Pick the one that matches your team's maturity.
requirements.txt is the simplest: a flat list of package==version lines, generated with pip freeze > requirements.txt. It works everywhere, but it freezes the entire transitive dependency tree, and it has no separation between "what I actually asked for" and "what was pulled in". Use it for small scripts and legacy projects.
pip-tools splits this into requirements.in (your direct deps) and requirements.txt (the compiled lockfile). Run pip-compile to regenerate the lockfile whenever requirements.in changes. This is a good middle ground.
pyproject.toml + uv.lock (or poetry.lock) is the modern standard. Your direct dependencies live in pyproject.toml under [project] dependencies, and a machine-generated lockfile (uv.lock or poetry.lock) captures every transitive pin with hashes. uv sync or poetry install reproduces the exact environment deterministically. This is what new projects should use.
Minimal pyproject.toml for a Flask app:
[project] name = "myapp" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "flask>=3.0", "gunicorn>=22.0", "python-dotenv>=1.0", ]
[project.optional-dependencies] dev = ["pytest", "ruff", "mypy"]
Generate and commit the lockfile:
uv lock
git add pyproject.toml uv.lockOn the production VPS, reproduce the exact environment:
uv sync --no-devTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
error: externally-managed-environment | PEP 668 blocks system-wide pip installs on Ubuntu 24.04 | Use a virtual environment (python3 -m venv .venv && source .venv/bin/activate) or install the CLI with pipx install <name> -- see Steps 5 and 6 |
ModuleNotFoundError: No module named 'ensurepip' when running python3 -m venv | python3-venv package not installed | Install it: sudo apt install -y python3-venv |
error: command 'gcc' failed when installing a package | Missing python3-dev or build-essential | Install build deps: sudo apt install -y python3-dev build-essential libssl-dev libffi-dev |
psycopg2 build fails with pg_config executable not found | PostgreSQL client headers missing | Install libpq-dev: sudo apt install -y libpq-dev. Or use the pre-built psycopg2-binary wheel instead. |
lxml or cryptography wheel build fails | Missing libxml2-dev, libxslt1-dev, or libffi-dev | sudo apt install -y libxml2-dev libxslt1-dev libffi-dev libssl-dev |
UnicodeDecodeError or weird character output in Python scripts | Server locale not set to UTF-8 | Check locale. Set it: sudo locale-gen en_US.UTF-8 && sudo update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8, then log out and back in. |
pyenv install 3.X.Y fails with ModuleNotFoundError: No module named '_ctypes' | Missing libffi-dev before building | Install libffi-dev, then rebuild: pyenv install 3.X.Y |
| Gunicorn workers OOM-killed on a 1 GB VPS | Too many workers for available RAM | Reduce --workers to 2-3, or upgrade to CloudCore Starter with 4 GB RAM |
ImportError after activating venv | Venv was built against a different Python version that was later removed | Delete and recreate: rm -rf .venv && python3 -m venv .venv && pip install -r requirements.txt |
uv: command not found after install | ~/.local/bin not on PATH | Run source ~/.bashrc, or add export PATH="$HOME/.local/bin:$PATH" to your shell rc file |
Viewing application logs
Once your app runs under systemd, journalctl is your best friend:
sudo journalctl -u myapp -f-f follows the log in real time. Use -n 100 --no-pager to dump the last 100 lines non-interactively.
FAQ
Should I upgrade the system Python to 3.13?
No. Leave /usr/bin/python3 on the 3.12 that Ubuntu ships -- apt, cloud-init, netplan, and other system tools depend on it. If you need 3.13 for your application, install it separately via the deadsnakes PPA (Step 8) or pyenv (Step 9) and point your venv or systemd service at that interpreter. Never replace or overwrite the system interpreter.
Can I use Anaconda or Miniconda on a VPS?
You can, but it's rarely the right call on a production server. Conda shines for data-science workstations where you juggle heavy scientific stacks (NumPy, SciPy, CUDA, R) and need pre-built non-Python binaries. On a web-facing VPS with one or two services, plain venv plus pip (or uv) is lighter, faster, and produces smaller container images. Use Conda where its strengths matter; use venv for everything else.
What's the difference between uv, Poetry, and pip-tools?
All three solve "reproducible dependency management", but at different speeds and scopes. pip-tools is a minimal layer over pip that adds a lockfile (pip-compile). Poetry is a full project manager with its own dependency resolver, packaging commands, and poetry.lock format. uv is the newest entrant: it replaces pip, pip-tools, virtualenv, and most of pyenv in one Rust binary, resolves dependencies 10-100x faster than pip, and uses a cross-platform lockfile. For new projects starting in 2026, uv is the recommended default. Existing Poetry or pip-tools projects are fine to leave alone.
Do I need Gunicorn if I'm running FastAPI?
FastAPI is an ASGI framework, so it wants an ASGI server -- uvicorn is the canonical choice. You have two patterns. For small apps, run uvicorn directly under systemd: ExecStart=/home/myapp/.venv/bin/uvicorn --workers 4 main:app. For multi-worker production with graceful restarts, run Gunicorn with the uvicorn.workers.UvicornWorker class: gunicorn -k uvicorn.workers.UvicornWorker --workers 4 main:app. The Gunicorn-plus-uvicorn-workers pattern gives you Gunicorn's battle-tested process supervisor with uvicorn's async runtime.
How do I handle database migrations in a systemd-deployed app?
Keep migrations out of the runtime unit. Create a separate oneshot systemd service (or a simple shell script invoked during deploy) that activates the venv and runs the migration command (alembic upgrade head, flask db upgrade, python manage.py migrate). Run it before starting/restarting the main service. Committing to this separation keeps deploys reproducible and prevents startup races when multiple Gunicorn workers try to migrate simultaneously.
Next Steps
You now have a clean Python 3.12 install, the right tools for dependency management, and a production deployment pattern. Here's what to do next:
- Add Nginx with TLS -- follow How to Install Nginx on Ubuntu 24.04 to put HTTPS in front of your Gunicorn service using Certbot and Let's Encrypt.
- Install PostgreSQL or MariaDB -- most Python web apps need a database. See How to Install PostgreSQL on Ubuntu 24.04 or How to Install MariaDB on Ubuntu 24.04.
- Set up Redis for caching and queues -- Python apps commonly pair with Redis for sessions, caching, and Celery/RQ job queues. See How to Install Redis on Ubuntu 24.04.
- Monitor with Prometheus and Grafana -- instrument your app with
prometheus_client, scrape it with Prometheus, and dashboard it in Grafana. See How to Build a Monitoring Stack on Ubuntu.
- Containerize with Docker -- when you're ready to ship immutable builds, see How to Install Docker on Ubuntu 24.04. Pair it with
uvinside apython:3.12-slimimage for minimal build times.
- Read the official docs -- python.org, the uv documentation, and the pyenv repository are all worth bookmarking.
Skip the Manual Install -- Get a Python-Ready VPS>
Our CloudCore Starter plan ships Ubuntu 24.04 LTS with full root access, unmetered bandwidth, and enough resources for Flask, FastAPI, or Django in production. Deploy in under 60 seconds and follow this guide to get from fresh server to HTTPS-protected Python service in 20 minutes.>
- 2 vCPU cores, 4 GB RAM, 50 GB NVMe SSD
- Ubuntu 24.04 LTS pre-installed
- Root access, no PEP 668 surprises
- Systemd, Nginx, and Certbot ready out of the box
- Scale up any time as your workload grows>
Launch Your Python VPS Now -- Plans built for developers who ship.