Linux VPS

Deploy a Python app with Gunicorn, systemd and Nginx

Set up a virtual environment, run your Flask or Django app with Gunicorn as a systemd service, and serve it through Nginx with HTTPS.

IntermediateLinux server2 min read

For Flask, Django and other WSGI apps, a reliable single-server setup is: Gunicorn runs the Python code, systemd keeps Gunicorn running, and Nginx handles public traffic and HTTPS. We use /srv/myapp and the user alex as examples.

1. Install Python tools

Bash
sudo apt install -y python3-venv python3-pip     # Ubuntu / Debian
sudo dnf install -y python3 python3-pip           # AlmaLinux / Rocky Linux

2. Create the app folder and virtual environment

Bash
sudo mkdir -p /srv/myapp
sudo chown alex:alex /srv/myapp
cd /srv/myapp
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install gunicorn flask

Copy or clone your code into /srv/myapp and install its requirements with pip install -r requirements.txt.

3. Test Gunicorn by hand

For a Flask app in app.py with app = Flask(__name__):

Bash
gunicorn --bind 127.0.0.1:8000 app:app

For Django, point at the WSGI module instead, for example myproject.wsgi:application. Press Ctrl + C once it works.

4. Create a systemd service

Create /etc/systemd/system/myapp.service:

INI
[Unit]
Description=myapp (Gunicorn)
After=network.target

[Service]
User=alex
WorkingDirectory=/srv/myapp
EnvironmentFile=-/srv/myapp/.env
ExecStart=/srv/myapp/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 vCPU plus one. Keep secrets such as database passwords in /srv/myapp/.env (one KEY=value per line) with chmod 600, not in the unit file.

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp --no-pager

5. Put Nginx in front

Nginx
server {
    listen 80;
    server_name app.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;
    }
}

Test and reload with sudo nginx -t && sudo systemctl reload nginx, then add HTTPS with Let's Encrypt. For Django, serve /static/ directly from Nginx with an alias to your STATIC_ROOT after running collectstatic.

Note: On AlmaLinux and Rocky Linux, allow Nginx to reach the app port: sudo setsebool -P httpd_can_network_connect 1.

Deploy an update

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

Troubleshooting

  • Service fails to start: read the real error with sudo journalctl -u myapp -n 50 --no-pager. Usual causes are a wrong module name (app:app) or a missing package in the venv.
  • 502 Bad Gateway: Gunicorn is not running on 127.0.0.1:8000. Check sudo ss -tlnp | grep 8000.
  • Requests time out on slow pages: raise Gunicorn's --timeout (default 30 seconds) or move long work to a background job.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.