UnderHost
Knowledgebase Docs

Python Deployment Best Practices: Flask, Django

Deploy Python web apps to production. Virtual environments, Gunicorn, uWSGI, Nginx reverse proxy, systemd services, security, monitoring.

On this page

Python web applications require special setup for production deployment. Development servers (Flask's built-in server, Django's runserver) are slow and insecure. Production deployments use WSGI application servers (Gunicorn, uWSGI), reverse proxies (Nginx), systemd services for process management, and proper security configuration. This guide covers production-grade Python deployment.

Deployment Overview

Production deployment architecture:**

Internet → Nginx (port 80/443) → Gunicorn (port 8000)
           ↓                      ↓
         (reverse proxy)    (Python app server)
         (SSL/TLS)          (WSGI interface)
         (static files)
         (compression)

Why this architecture:**

  • Nginx: Fast, handles SSL, serves static files
  • Gunicorn: Multiple workers handle concurrent requests
  • Python app: Runs in isolated virtual environment
  • Systemd: Manages process lifecycle (restart on crash)

Virtual Environments

Always use virtual environments to isolate dependencies:**

# Create virtual environment
python3 -m venv /var/www/myapp/venv

# Activate it
source /var/www/myapp/venv/bin/activate

# Install dependencies
pip install flask gunicorn

# Create requirements.txt (for reproducibility)
pip freeze > requirements.txt

Requirements.txt example:**

Flask==2.3.2
Gunicorn==21.2.0
Flask-SQLAlchemy==3.0.5
python-dotenv==1.0.0
requests==2.31.0

Deploy with requirements.txt:**

# On production server
source /var/www/myapp/venv/bin/activate
pip install -r requirements.txt

WSGI Application Servers

ServerSpeedSetupBest For
GunicornVery FastSimpleMost apps (recommended)
uWSGIVery FastComplexComplex setups
WaitressFastSimpleWindows, simple apps
mod_wsgiGoodMediumApache integration

Gunicorn Setup

Install Gunicorn in virtual environment:**

source /var/www/myapp/venv/bin/activate
pip install gunicorn

Create gunicorn_config.py:**

import multiprocessing

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
max_requests = 1000
timeout = 30
keepalive = 2

Start Gunicorn:**

source /var/www/myapp/venv/bin/activate
gunicorn -c gunicorn_config.py wsgi:app

# For Django:
gunicorn -c gunicorn_config.py config.wsgi:application

Nginx Reverse Proxy

Configure /etc/nginx/sites-available/myapp:**

upstream gunicorn {
    server 127.0.0.1:8000;
}

server {
    listen 80;
    server_name yourdomain.com;

    client_max_body_size 5M;

    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
    }

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

    location / {
        proxy_pass http://gunicorn;
        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;
    }
}

# Enable site
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Systemd Service

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

[Unit]
Description=My Python App (Gunicorn)
After=network.target

[Service]
Type=notify
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/var/www/myapp/venv/bin/gunicorn -c gunicorn_config.py wsgi:app

# Environment variables
Environment="PATH=/var/www/myapp/venv/bin"
Environment="DATABASE_URL=mysql://user:pass@localhost/db"
EnvironmentFile=/var/www/myapp/.env

Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Start service:**

systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
systemctl status myapp

Security Hardening

Environment variables (.env file):**

# .env (never commit this)
SECRET_KEY=your-secret-key-here
DEBUG=False
DATABASE_URL=mysql://user:pass@localhost/db
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

Load in app.py/settings.py:**

import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')

Enable HTTPS with Let's Encrypt:**

apt install certbot python3-certbot-nginx
certbot certonly --nginx -d yourdomain.com

# Nginx auto-configures HTTPS after this

Troubleshooting

502 Bad Gateway error:**

  • Check Gunicorn is running: systemctl status myapp
  • Check logs: journalctl -u myapp -n 50
  • Verify port 8000 binding: netstat -tulpn | grep 8000
  • Test Gunicorn directly: curl http://127.0.0.1:8000

Permission denied when reading files:**

  • Check file ownership: ls -la /var/www/myapp
  • Fix ownership: chown -R www-data:www-data /var/www/myapp
  • Check directory permissions: chmod 755 /var/www/myapp

Database connection errors:**

  • Verify DATABASE_URL in .env is correct
  • Test connection: mysql -u user -p -h localhost -e "SELECT 1"
  • Check MySQL is running: systemctl status mysql
Python in production = virtual env + Gunicorn + Nginx + systemd

This stack is proven, scalable, and secure. Gunicorn handles Python app; Nginx handles HTTP/HTTPS and static files; systemd manages process lifecycle.

Related: Python fundamentals | WSGI servers | Reverse proxy setup | Process management

Was this article helpful?

Need a developer-friendly server?

Use an UnderHost Cloud VPS for SSH, Git, Node.js, Python, Laravel, Docker, cron, and custom development workflows.

Related articles

Back to Developer Tools