TürkiyeVPS

Running a Python App on a VPS: venv, systemd and an Nginx Reverse Proxy

A step-by-step guide to running your Python web app or bot permanently on a VPS: a virtual environment (venv), Gunicorn, a systemd service, an Nginx reverse proxy and a free SSL certificate.

8 min read

An app that runs fine on your computer with python app.py raises new questions once you move it to a server: Why does the app stop when I close the terminal? What happens if the server reboots? How do people reach the app with a domain name over HTTPS? In this guide we'll deploy a web app written in Python 3.12 on a VPS running Ubuntu 24.04, in a way that's fit for production.

Here's the setup we'll build:

  1. The app runs in its own virtual environment (venv).
  2. Gunicorn runs the app on a port that's only reachable from inside the server.
  3. systemd manages Gunicorn as a service: it starts it at boot and restarts it if it crashes.
  4. Nginx receives incoming requests from the outside and forwards them to the app (a reverse proxy).

We use Flask in the example. The steps are almost the same for Django or FastAPI; we'll point out the differences where relevant.

1. Prepare the server

Connect via SSH, update packages and install the required tools:

sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-venv python3-pip nginx
python3 --version

On Ubuntu 24.04, python3 --version should print Python 3.12.x. Running the app as its own user with limited privileges, rather than as root, is important for security:

sudo adduser --system --group --home /srv/myapp myapp

2. Put the app in place and create a virtual environment

Put your code in the /srv/myapp/app folder. If you use Git:

sudo -u myapp git clone https://github.com/kullanici/proje.git /srv/myapp/app

For testing, you can also write a tiny Flask app. /srv/myapp/app/app.py:

from flask import Flask

app = Flask(__name__)

@app.get("/")
def index():
    return "Merhaba, uygulama VPS'te çalışıyor!"

Now create the virtual environment and install the dependencies:

cd /srv/myapp
sudo -u myapp python3 -m venv venv
sudo -u myapp venv/bin/pip install --upgrade pip
sudo -u myapp venv/bin/pip install flask gunicorn

If your project has a requirements.txt, use sudo -u myapp venv/bin/pip install -r app/requirements.txt gunicorn instead of the last line.

Why venv? Ubuntu 24.04 prevents you from installing packages into the system Python with pip (the externally-managed-environment error). This is a deliberate safeguard: it stops you from breaking packages that system tools depend on. A separate venv for each project also eliminates dependency conflicts.

3. Try it with Gunicorn

Flask's development server isn't suitable for production. We'll use Gunicorn instead:

cd /srv/myapp/app
sudo -u myapp ../venv/bin/gunicorn --bind 127.0.0.1:8000 app:app

When you run curl http://127.0.0.1:8000 from another terminal, you should see the greeting text. app:app means "the app object in the app.py file". In Django this is usually proje.wsgi:application. For ASGI apps like FastAPI, you add the -k uvicorn.workers.UvicornWorker parameter to Gunicorn (for this you also need to install the uvicorn package).

After testing, stop it with Ctrl+C. Note that the app binds to 127.0.0.1: it can't be reached directly from the outside, and it will receive its traffic through Nginx.

4. Create a systemd service

So that the app doesn't stop when the terminal closes and starts automatically when the server reboots, we write a systemd service:

sudo nano /etc/systemd/system/myapp.service

Its contents:

[Unit]
Description=myapp Gunicorn servisi
After=network.target

[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/app
Environment="PATH=/srv/myapp/venv/bin"
EnvironmentFile=-/srv/myapp/.env
ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

A few notes:

  • EnvironmentFile lets you keep secrets such as database passwords or API keys separate from your code. The leading - means the service still starts if the file doesn't exist.
  • --workers 3 is a reasonable starting point for a small server. The common recommendation is (2 × number of cores) + 1, but take your RAM into account too.
  • Restart=on-failure restarts the app five seconds after it exits with an error.

Enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp

You should see active (running) in the status line. If there's an error, check the logs:

journalctl -u myapp -n 50 --no-pager

5. Configure Nginx as a reverse proxy

First point your domain's DNS A record to the VPS's IP address. Then create an Nginx site file:

sudo nano /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name ornek.com www.ornek.com;

    client_max_body_size 20M;

    location /static/ {
        alias /srv/myapp/app/static/;
    }

    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;
    }
}

The /static/ block lets Nginx serve CSS, JS and images directly without burdening Python. If your app doesn't have such a folder, you can delete this block. Enable the site:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

The nginx -t command checks the configuration for errors; make a habit of running it after every change.

6. Firewall and HTTPS

If you use UFW, allow SSH and web traffic:

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

To get a free SSL certificate from Let's Encrypt:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ornek.com -d www.ornek.com

Certbot adds the HTTPS block to your Nginx configuration itself and sets up automatic certificate renewal. You can test that renewal works with sudo certbot renew --dry-run.

Updating the app

When you change the code, here's what you need to do:

cd /srv/myapp/app
sudo -u myapp git pull
sudo -u myapp ../venv/bin/pip install -r requirements.txt
sudo systemctl restart myapp

Keep secrets out of your code

Writing values like a database password, an API key or SECRET_KEY into your code can cause them to leak when the code is pushed to a Git repository. Use the /srv/myapp/.env file we referenced in the systemd service for this:

sudo nano /srv/myapp/.env
DATABASE_URL=postgresql://myapp:[email protected]:5432/myapp
SECRET_KEY=uzun-ve-rastgele-bir-deger

Restrict the file's permissions so only the app user can read it, and restart the service:

sudo chown myapp:myapp /srv/myapp/.env
sudo chmod 600 /srv/myapp/.env
sudo systemctl restart myapp

In Python you read these values with os.environ["DATABASE_URL"]. To generate a strong random key, you can use the command python3 -c "import secrets; print(secrets.token_hex(32))".

Common mistakes

  • Running the app as root: If a vulnerability is found in the app, an attacker takes over the whole server. Always use a separate user with limited privileges.
  • Exposing the development server: flask run --host 0.0.0.0 or Django's runserver command isn't designed for production. Use Gunicorn and Nginx.
  • Leaving debug mode on: debug=True in Flask or DEBUG = True in Django can expose your code and settings on error pages.
  • Forgetting ALLOWED_HOSTS in Django: If you don't add your domain to this list, requests coming through Nginx get a "Bad Request (400)" error. Don't forget to run collectstatic for static files, too.
  • Folder permissions: For Nginx to read static files, the /srv/myapp/app/static folder and its parent folders must be readable by the www-data user. If you get a 403 error, this is the first place to look.

If you're running a bot or a background job

For a Telegram/Discord bot or a script without a web interface, you don't need Nginx. In the same systemd file, just change the ExecStart line to /srv/myapp/venv/bin/python bot.py. systemd keeps the bot running and restarts it if it crashes.

Which server is enough?

For a small Flask/FastAPI app or a bot, a VPS with 1–2 vCPUs and 1–2 GB of RAM is usually enough. If you have a machine learning model, heavy background tasks or constantly high CPU usage, a server whose resources aren't shared with others gives more consistent results. TürkiyeVPS VPS and VDS plans come with root access, so you can apply every step in this guide on your own server.

Frequently Asked Questions

Isn't running it with screen or tmux enough?

It's fine for testing, but the app won't come back up on its own when the server reboots or the app crashes. Since systemd provides both automatic startup and logging, it should be preferred for permanent apps.

I get an "externally-managed-environment" error. What should I do?

This error appears when you try to pip install directly into the system Python. Create a virtual environment for your project with python3 -m venv venv and install packages with venv/bin/pip.

The app returns a 502 Bad Gateway error. Why?

It means Nginx can't reach the app behind it. Check that the service is running with systemctl status myapp, whether there are errors with journalctl -u myapp, and that the proxy_pass port in Nginx matches Gunicorn's port.

Can I run more than one Python app on the same VPS?

Yes. Just create a separate venv, a different port (such as 8000, 8001), a separate systemd service and a separate Nginx server block for each app.