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
sudoprivileges. This guide usesyour_userin 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
NoteOn Ubuntu 24.04, running
pip install <package>outside a virtual environment fails witherror: externally-managed-environment. This is intentional (PEP 668): it stops pip from overwriting packages that apt manages. Install Python libraries inside a venv, install system-wide libraries withapt install python3-<name>, and install standalone command-line tools withpipx(sudo apt install pipx). Do not use--break-system-packageson a server.
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
WarningNever change what
/usr/bin/python3points to, for example withupdate-alternatives. Ubuntu tools such as apt helpers rely on the default Python 3.12 and break if it is replaced.
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
pytestinto arequirements-dev.txtfile. - Run the service under a dedicated system user instead of your login user.
