Install Python and a virtual environment
Debian 13 ships Python 3.13, but its system installation is protected: global pip install is blocked to avoid breaking system tools. The right approach is one virtual environment per application, with pinned dependencies, then a systemd service that starts the application with that exact Python.
Install Python and venv
sudo apt update
sudo apt install -y python3 python3-venv python3-pip
python3 --versionpython3-venv is a separate package: without it, python3 -m venv fails with ensurepip is not available. Debian enforces PEP 668 and rejects global pip installs, hence the externally-managed-environment error if you try. Do not bypass this protection with --break-system-packages on a production server.
Create the environment
Switch to the deployment user before creating the venv:
sudo mkdir -p /srv/app
sudo chown deploy:www-data /srv/app
sudo -iu deploy
cd /srv/app
python3 -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt
deactivateAfter activation, which python must point to /srv/app/venv/bin/python. Pin versions in requirements.txt (flask==3.1.0) to make deployment reproducible. The venv directory does not go into Git: it is recreated from the requirements file.
systemd service
Create /etc/systemd/system/app.service:
[Unit]
Description=Python application
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app.env
ExecStart=/srv/app/venv/bin/python /srv/app/main.py
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now app
sudo systemctl status appExecStart uses the venv Python, not /usr/bin/python3, otherwise installed packages will not be found. EnvironmentFile loads PORT, DATABASE_URL and other variables; the file must be readable by deploy only (chmod 600).
Listening and reverse proxy
The application reads its port from the environment and listens on 0.0.0.0. For Flask or FastAPI, do not run the development server in production: use Gunicorn or Uvicorn behind Nginx. The application port stays local and closed in the firewall; only 80 and 443 are public.
Verify and troubleshoot
journalctl -u app -n 100 --no-pager shows startup errors. Common causes: a wrong path in ExecStart, a module missing from the venv, an unloaded variable, a port already in use (ss -ltnp). Test with curl http://127.0.0.1:PORT/ before concluding on the Nginx side. After pip install -r requirements.txt, restart the service: a reload does not pick up new packages.
Advertisement
Was this article helpful?
Read next
- Deploy with Git on a VPSDeploy from a Git repository with a read-only deploy key and predictable updates.
- Set up secure SFTP accessProvide SFTP access restricted to one folder with chroot, without a shell or root rights.
- Install and secure RedisInstall Redis, restrict it to local listening, add authentication and back up its data.