How to Set Up Nginx for a Python Application

How to Set Up Nginx for a Python Application

How to Set Up Nginx for a Python Application

Unlike PHP, Python doesn’t have a native “hand the request off and let it run” model built into Nginx — there’s no fastcgi_pass-equivalent shortcut that just works out of the box. Instead, the standard, production-proven pattern is to run your Python application behind an application server (Gunicorn or uWSGI for WSGI apps like Django and Flask, or Uvicorn/Hypercorn for ASGI apps like FastAPI), and have Nginx sit in front of that as a reverse proxy. Nginx handles TLS, static files, and connection management; the application server handles actually running your Python code.

This guide covers both the WSGI path (Django, Flask, most traditional Python web frameworks) and the ASGI path (FastAPI, Django with async support, other async frameworks), since the two have real configuration differences worth understanding rather than just following one and hoping it applies to the other.

Understanding WSGI vs ASGI (and Why It Matters Here)

WSGI (Web Server Gateway Interface) is the traditional, synchronous interface Python web apps have used for years — Flask, and Django until relatively recently, are built on it. Gunicorn is the most common WSGI server used in production.

ASGI (Asynchronous Server Gateway Interface) is the newer standard supporting async/await, WebSockets, and long-lived connections — FastAPI is built on it natively, and Django added ASGI support for async views and channels. Uvicorn is the standard ASGI server.

The Nginx-facing configuration is nearly identical either way — Nginx proxies HTTP requests to a local port or Unix socket where your application server is listening. The real difference is on the Python side (which server you run, and whether you need WebSocket proxy support in Nginx, which matters much more for ASGI apps handling real-time features).

Requirements

Step 1: Set Up Your Python Application and Virtual Environment

sudo mkdir -p /var/www/myapp
cd /var/www/myapp
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

For a Django project:

pip install gunicorn

For a FastAPI project:

pip install uvicorn[standard] fastapi

Step 2: Test the Application Server Directly First

Before touching Nginx, confirm the app runs correctly on its own.

Django/Flask with Gunicorn:

gunicorn --bind 127.0.0.1:8000 myproject.wsgi:application

FastAPI with Uvicorn:

uvicorn main:app --host 127.0.0.1 --port 8000

Visit http://127.0.0.1:8000 from the server itself (curl http://127.0.0.1:8000) to confirm it responds before adding Nginx into the mix — isolating problems to either “the app” or “the proxy” is much easier this way than debugging both layers at once.

Step 3: Run the Application Server as a Persistent Service

Don’t rely on a manually run foreground process in production — create a systemd service so it survives reboots and crashes.

For Gunicorn, create /etc/systemd/system/myapp.service:

[Unit]
Description=Gunicorn instance for myapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
ExecStart=/var/www/myapp/venv/bin/gunicorn \
    --workers 4 \
    --bind unix:/var/www/myapp/myapp.sock \
    myproject.wsgi:application

Restart=always

[Install]
WantedBy=multi-user.target

For Uvicorn (FastAPI), the equivalent uses uvicorn directly, or better, gunicorn with the Uvicorn worker class for production process management:

[Unit]
Description=Uvicorn instance for myapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myapp
Environment="PATH=/var/www/myapp/venv/bin"
ExecStart=/var/www/myapp/venv/bin/gunicorn main:app \
    --workers 4 \
    --worker-class uvicorn.workers.UvicornWorker \
    --bind unix:/var/www/myapp/myapp.sock

Restart=always

[Install]
WantedBy=multi-user.target

Enable and start:

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

A Unix socket (rather than a TCP port) is used here since Nginx and the app server are on the same machine — it’s marginally faster and avoids taking up a TCP port unnecessarily. Use --bind 127.0.0.1:8000 instead if you’d rather use TCP, which is also perfectly fine and sometimes simpler to debug.

Step 4: Configure Nginx as the Reverse Proxy

Create /etc/nginx/sites-available/myapp:

server {
    listen 80;
    server_name myapp.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name myapp.example.com;

    ssl_certificate     /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;

    client_max_body_size 20m;

    access_log /var/log/nginx/myapp_access.log;
    error_log  /var/log/nginx/myapp_error.log;

    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
    }

    location /media/ {
        alias /var/www/myapp/media/;
        expires 7d;
    }

    location / {
        proxy_pass http://unix:/var/www/myapp/myapp.sock;
        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;
        proxy_read_timeout 60s;
    }
}

Key points:

For Django specifically, run collectstatic to gather static files into the directory Nginx is serving from:

python manage.py collectstatic

Step 5: Add WebSocket Support (Important for ASGI/FastAPI/Django Channels)

If your application uses WebSockets (common in FastAPI apps, Django Channels, or any real-time feature), add upgrade headers:

location /ws/ {
    proxy_pass http://unix:/var/www/myapp/myapp.sock;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 3600s;
}

The long proxy_read_timeout here matters — WebSocket connections are meant to stay open far longer than a typical HTTP request, and Nginx’s default timeout will silently kill idle-but-legitimate connections otherwise.

Step 6: Test and Reload

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

Step 7: Verify Everything Works

curl -I https://myapp.example.com

Check for a 200 response and confirm static assets load correctly by visiting a page with CSS/images in a browser and checking the network tab for any 404s.

If your app has a WebSocket feature, test it directly (browser dev tools’ Network tab shows WebSocket connection status, or test with a tool like wscat):

wscat -c wss://myapp.example.com/ws/

Troubleshooting Common Issues

502 Bad Gateway. The application server isn’t running, crashed, or the socket/port doesn’t match between the systemd service and the Nginx config. Check:

sudo systemctl status myapp
sudo journalctl -u myapp -n 50

“Permission denied” connecting to the socket. The socket file’s permissions don’t allow the Nginx user (www-data) to connect. Confirm the Gunicorn/Uvicorn process’s User/Group in the systemd unit matches, or explicitly set socket permissions in Gunicorn’s bind config if needed.

Static files 404. collectstatic wasn’t run (Django), or the alias path in Nginx doesn’t match where static files actually live. Double check with ls /var/www/myapp/static/.

App works via curl 127.0.0.1:8000 directly but not through Nginx. Usually a proxy_pass syntax issue (missing http:// prefix before the socket path is a classic one) or a firewall/SELinux context issue if you’re on a RHEL-based system — check sudo journalctl -u nginx and SELinux audit logs (sudo ausearch -m avc -ts recent) if applicable.

WebSocket connections fail or disconnect immediately. Missing Upgrade/Connection: upgrade headers, or proxy_read_timeout too short.

App restarts don’t pick up code changes. Restart the systemd service after deploys: sudo systemctl restart myapp — a common thing to forget in a deploy script.

Security Considerations

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

location /login/ {
    limit_req zone=login burst=3 nodelay;
    proxy_pass http://unix:/var/www/myapp/myapp.sock;
}

Performance Tips

gzip on;
gzip_types application/json text/css application/javascript;
gzip_min_length 256;

Real-World Use Cases

Best Practices

Wrapping Up

The Nginx + Python pattern always comes down to the same core idea: let Nginx handle what it’s excellent at (TLS, static files, connection management, load balancing) and let a dedicated application server (Gunicorn or Uvicorn) handle running your actual Python code, with Nginx proxying requests between the two. Whether you’re running a traditional synchronous Django app or an async FastAPI service, the Nginx-side configuration barely changes — what matters is picking the right application server and worker model for your workload, wiring up static file serving correctly, and adding WebSocket support only where your application actually needs it. Get the systemd service and socket/port wiring right, verify with a direct curl to the app server before adding Nginx, and this setup scales comfortably from a small side project to a production-grade deployment.

Exit mobile version