Python virtual environments (venv) give each project its own interpreter directory and its own set of packages, so one application's dependencies never break another's or the operating system's. In this tutorial you will prepare Python 3.12 on Ubuntu 24.04, create a virtual environment, install and pin packages with pip, rebuild the environment from a requirements file, and run a small Flask application with Gunicorn as a systemd service that uses the venv directly.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges. This guide uses your_user in paths; replace it with your username.

Step 1 - Installing Python tooling

Ubuntu 24.04 ships Python 3.12 as python3, and the system depends on it. What is missing on a minimal server is the venv module, pip and the headers needed to compile packages with C extensions. Install them:

sudo apt update
sudo apt install python3 python3-venv python3-pip python3-dev build-essential

Check the interpreter version:

python3 --version
Python 3.12.3

Step 2 - Creating a virtual environment

Create a project directory. Keeping the environment inside the project, in a directory named .venv, is a common convention that editors and tools recognize:

mkdir ~/myproject
cd ~/myproject
python3 -m venv .venv

This creates .venv/ with its own python, its own pip and an empty site-packages directory. Activate it:

source .venv/bin/activate

Your prompt now starts with (.venv). Confirm that python and pip point inside the project:

which python pip
python --version
/home/your_user/myproject/.venv/bin/python
/home/your_user/myproject/.venv/bin/pip
Python 3.12.3

Inside an active environment, python and pip refer to the venv versions, so you do not need python3 or sudo. To leave the environment, run deactivate.

Step 3 - Installing and pinning packages

With the environment active, upgrade pip and install the packages for the sample app, Flask and the Gunicorn WSGI server:

python -m pip install --upgrade pip
pip install flask gunicorn

Check that the installed packages have compatible dependencies:

pip check
No broken requirements found.

Record the exact versions of everything installed, including indirect dependencies, in requirements.txt:

pip freeze > requirements.txt
cat requirements.txt
blinker==1.9.0
click==8.3.0
Flask==3.1.2
gunicorn==23.0.0
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
packaging==25.0
Werkzeug==3.1.3

Commit requirements.txt to version control, and exclude the environment itself, since it contains absolute paths and cannot be copied between machines:

echo ".venv/" >> .gitignore

Step 4 - Rebuilding the environment from requirements.txt

A virtual environment is disposable. On a new server, or after a Python upgrade, you recreate it from requirements.txt instead of copying it. Simulate that now:

deactivate
rm -rf .venv
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Calling .venv/bin/pip directly uses the environment without activating it, which is also how scripts, cron jobs and services should use it. Confirm that Flask is back:

.venv/bin/python -c "import flask; print(flask.__version__)"
3.1.2

Step 5 - Creating a sample Flask app

Create a minimal application to run as a service:

nano ~/myproject/app.py
from flask import Flask

app = Flask(__name__)


@app.get("/")
def index():
    return {"status": "ok"}

Test it with Gunicorn from the venv, bound to localhost:

cd ~/myproject
.venv/bin/gunicorn --bind 127.0.0.1:8000 app:app

In a second SSH session, send a request:

curl http://127.0.0.1:8000
{"status":"ok"}

Stop Gunicorn with CTRL+C.

Step 6 - Running the app as a systemd service

A systemd unit does not need to activate the virtual environment. Pointing ExecStart at the Gunicorn binary inside .venv/bin is enough, because that binary already uses the venv's interpreter and packages.

Create the unit file:

sudo nano /etc/systemd/system/myproject.service
[Unit]
Description=Gunicorn for myproject
After=network.target

[Service]
User=your_user
Group=your_user
WorkingDirectory=/home/your_user/myproject
ExecStart=/home/your_user/myproject/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure

[Install]
WantedBy=multi-user.target

A common starting point for --workers is two per CPU core plus one. Reload systemd and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now myproject

Check its status and test it again:

systemctl status myproject
curl http://127.0.0.1:8000
● myproject.service - Gunicorn for myproject
     Loaded: loaded (/etc/systemd/system/myproject.service; enabled; preset: enabled)
     Active: active (running) since ...
{"status":"ok"}

Gunicorn writes its log to the journal. Follow it with:

sudo journalctl -u myproject -f

When you deploy new code or update requirements.txt, run .venv/bin/pip install -r requirements.txt and then sudo systemctl restart myproject.

Using a different Python version

Some projects need a newer or older Python than 3.12. The deadsnakes PPA provides other versions for Ubuntu LTS releases, installed alongside the system Python without replacing it:

sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.13 python3.13-venv

Create the environment with that interpreter; everything inside it then uses Python 3.13:

python3.13 -m venv .venv
.venv/bin/python --version
Python 3.13.9

Troubleshooting

The virtual environment was not created successfully because ensurepip is not available. The python3-venv package is missing. Install it with sudo apt install python3-venv (or python3.13-venv for a deadsnakes version), delete the half-created .venv and run python3 -m venv .venv again.

error: externally-managed-environment when running pip. You are running the system pip, not the venv's. Activate the environment with source .venv/bin/activate or call .venv/bin/pip directly.

Packages fail to build with Python.h: No such file or directory. The development headers are missing. Install python3-dev and build-essential, plus any library the package names in its error, such as libpq-dev for psycopg built from source.

The service fails with status=203/EXEC. systemd cannot run the path in ExecStart. Check that it exists with ls -l /home/your_user/myproject/.venv/bin/gunicorn and that the venv was not moved after it was created. If it was, recreate it as in Step 4.

Conclusion

You installed the Python tooling on Ubuntu 24.04, created a project virtual environment, pinned its dependencies in requirements.txt, rebuilt it from scratch and ran a Flask app with Gunicorn as a systemd service that uses the venv directly.

As next steps you can:

  • Put Nginx in front of Gunicorn as a reverse proxy and add HTTPS with Let's Encrypt.
  • Separate development tools such as pytest into a requirements-dev.txt file.
  • Run the service under a dedicated system user instead of your login user.