Deploy Python web applications in production using Gunicorn WSGI server with systemd service management and Nginx reverse proxy. Includes performance tuning, security hardening, and monitoring setup.
Prerequisites
- Root or sudo access
- Basic understanding of Python web applications
- Domain name pointing to your server
What this solves
Gunicorn (Green Unicorn) is a Python WSGI HTTP server that serves Python web applications like Django and Flask in production environments. This tutorial shows you how to install Gunicorn, configure it with optimal worker settings, set up automatic service management with systemd, and secure it behind an Nginx reverse proxy.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of all dependencies.
sudo apt update && sudo apt upgrade -y
Install Python and development tools
Install Python 3, pip, and virtual environment tools needed for application deployment.
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential
Create application user
Create a dedicated system user for running your Python application securely without shell access.
sudo useradd --system --group --no-create-home --shell /bin/false myapp
sudo mkdir -p /var/www/myapp
sudo chown myapp:myapp /var/www/myapp
Create Python virtual environment
Set up an isolated Python environment for your application dependencies.
cd /var/www/myapp
sudo -u myapp python3 -m venv venv
sudo -u myapp /var/www/myapp/venv/bin/pip install --upgrade pip
Install Gunicorn and your application
Install Gunicorn in the virtual environment along with your web framework.
sudo -u myapp /var/www/myapp/venv/bin/pip install gunicorn
For Django applications:
sudo -u myapp /var/www/myapp/venv/bin/pip install django
For Flask applications:
sudo -u myapp /var/www/myapp/venv/bin/pip install flask
Create sample application
Create a basic WSGI application for testing. Replace this with your actual application code.
#!/usr/bin/env python3
def application(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/html; charset=utf-8')]
start_response(status, headers)
return [b'Hello from Gunicorn!
']
if __name__ == '__main__':
from wsgiref.simple_server import make_server
server = make_server('127.0.0.1', 8000, application)
server.serve_forever()
sudo chown myapp:myapp /var/www/myapp/wsgi.py
sudo chmod 644 /var/www/myapp/wsgi.py
Configure Gunicorn settings
Create a Gunicorn configuration file with optimized worker settings for production use.
# Gunicorn configuration file
Server socket
bind = "127.0.0.1:8000"
backlog = 2048
Worker processes
workers = 3
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 50
Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
Logging
accesslog = "/var/log/myapp/access.log"
errorlog = "/var/log/myapp/error.log"
loglevel = "info"
access_log_format = '%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"'
Process naming
proc_name = 'myapp'
Server mechanics
daemon = False
pidfile = '/var/run/myapp/gunicorn.pid'
user = 'myapp'
group = 'myapp'
tmp_upload_dir = None
SSL (uncomment if using HTTPS directly)
keyfile = '/path/to/private.key'
certfile = '/path/to/certificate.crt'
sudo chown myapp:myapp /var/www/myapp/gunicorn.conf.py
sudo chmod 644 /var/www/myapp/gunicorn.conf.py
Create log directories
Set up logging directories with proper permissions for the application user.
sudo mkdir -p /var/log/myapp /var/run/myapp
sudo chown myapp:myapp /var/log/myapp /var/run/myapp
sudo chmod 755 /var/log/myapp /var/run/myapp
Create systemd service file
Configure systemd to manage Gunicorn as a service with automatic restart and proper resource limits.
[Unit]
Description=Gunicorn instance to serve myapp
After=network.target
[Service]
Type=notify
User=myapp
Group=myapp
RuntimeDirectory=myapp
WorkingDirectory=/var/www/myapp
ExecStart=/var/www/myapp/venv/bin/gunicorn --config gunicorn.conf.py wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=always
RestartSec=5
StartLimitInterval=60
StartLimitBurst=3
KillMode=mixed
TimeoutStopSec=5
Security settings
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=/var/log/myapp /var/run/myapp
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
Resource limits
LimitNOFILE=65536
LimitNPROC=4096
[Install]
WantedBy=multi-user.target
Enable and start Gunicorn service
Enable the service to start automatically on boot and start it immediately.
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
Install and configure Nginx
Set up Nginx as a reverse proxy to handle static files and SSL termination.
sudo apt install -y nginx
Configure Nginx virtual host
Create an Nginx configuration with security headers and proper proxy settings.
upstream myapp_backend {
server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80;
server_name example.com www.example.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
# Hide server information
server_tokens off;
# Client settings
client_max_body_size 10M;
client_body_timeout 60s;
client_header_timeout 60s;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Static files
location /static/ {
alias /var/www/myapp/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
alias /var/www/myapp/media/;
expires 1y;
add_header Cache-Control "public";
}
# Favicon
location = /favicon.ico {
alias /var/www/myapp/static/favicon.ico;
log_not_found off;
access_log off;
}
# Application proxy
location / {
proxy_set_header Host $http_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_redirect off;
proxy_buffering on;
proxy_pass http://myapp_backend;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health/ {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Enable Nginx site and test configuration
Activate the site configuration and verify Nginx syntax before restarting.
# On Ubuntu/Debian systems
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
Test configuration
sudo nginx -t
Restart services
sudo systemctl restart nginx
sudo systemctl enable nginx
Configure firewall
Open the necessary ports for HTTP and HTTPS traffic while keeping the system secure.
sudo ufw allow 'Nginx Full'
sudo ufw --force enable
sudo ufw status
Set up log rotation
Configure logrotate to manage application logs and prevent disk space issues.
/var/log/myapp/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
copytruncate
create 644 myapp myapp
postrotate
/bin/kill -USR1 $(cat /var/run/myapp/gunicorn.pid 2>/dev/null) 2>/dev/null || true
endscript
}
Performance tuning
Calculate optimal worker count
Determine the ideal number of Gunicorn workers based on your server's CPU cores.
# Check CPU cores
nproc
Formula: (2 x CPU cores) + 1
For 4 cores: workers = 9
Edit /var/www/myapp/gunicorn.conf.py and update workers value
sudo systemctl restart myapp
Configure system limits
Increase system limits for file descriptors and processes to handle more concurrent connections.
myapp soft nofile 65536
myapp hard nofile 65536
myapp soft nproc 4096
myapp hard nproc 4096
Optimize Nginx worker processes
Configure Nginx to use optimal worker settings for your hardware.
# Add or modify these settings in the main context
worker_processes auto;
worker_connections 1024;
worker_rlimit_nofile 65536;
In http context
keepalive_timeout 65;
keepalive_requests 1000;
sudo nginx -t
sudo systemctl restart nginx
Verify your setup
Test that all components are working correctly and communicating properly.
# Check service status
sudo systemctl status myapp nginx
Test Gunicorn directly
curl -I http://127.0.0.1:8000/
Test through Nginx
curl -I http://localhost/
Check logs
sudo tail -f /var/log/myapp/error.log
sudo tail -f /var/log/nginx/error.log
Monitor worker processes
ps aux | grep gunicorn
Security hardening
Configure SSL certificates
Set up SSL/TLS certificates for secure HTTPS connections. You can get free certificates from Let's Encrypt.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Set up monitoring with systemd
Configure systemd to restart failed services automatically and log service events.
# Enable detailed logging
sudo systemctl edit myapp
[Service]
Environment="PYTHONPATH=/var/www/myapp"
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
sudo systemctl daemon-reload
sudo systemctl restart myapp
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Service fails to start | Permission denied on log directory | sudo chown -R myapp:myapp /var/log/myapp |
| 502 Bad Gateway error | Gunicorn not listening on correct port | Check bind setting in gunicorn.conf.py matches Nginx upstream |
| Workers dying frequently | Memory exhaustion or timeout issues | Increase worker memory limits or reduce max_requests |
| Static files not loading | Incorrect Nginx static file paths | Verify alias paths in Nginx config match actual file locations |
| High CPU usage | Too many workers for available cores | Reduce worker count to (2 x cores) + 1 |
| Connection refused | Firewall blocking access | Check firewall rules with sudo ufw status or sudo firewall-cmd --list-services |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
APP_NAME="${1:-myapp}"
BIND_IP="${2:-127.0.0.1}"
BIND_PORT="${3:-8000}"
WORKERS="${4:-3}"
usage() {
echo "Usage: $0 [app_name] [bind_ip] [bind_port] [workers]"
echo "Example: $0 myapp 127.0.0.1 8000 3"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
exit 1
}
cleanup() {
warn "Installation failed. Cleaning up..."
systemctl stop "$APP_NAME" 2>/dev/null || true
systemctl disable "$APP_NAME" 2>/dev/null || true
rm -f "/etc/systemd/system/$APP_NAME.service"
userdel "$APP_NAME" 2>/dev/null || true
rm -rf "/var/www/$APP_NAME" "/var/log/$APP_NAME" "/var/run/$APP_NAME"
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
fi
# Detect distribution
if [[ ! -f /etc/os-release ]]; then
error "Cannot detect distribution. /etc/os-release not found"
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
PYTHON_PKGS="python3 python3-pip python3-venv python3-dev build-essential"
NGINX_SITES_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
PYTHON_PKGS="python3 python3-pip python3-venv python3-devel gcc"
NGINX_SITES_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
PYTHON_PKGS="python3 python3-pip python3-venv python3-devel gcc"
NGINX_SITES_DIR="/etc/nginx/conf.d"
NGINX_ENABLED_DIR="/etc/nginx/conf.d"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
log "[1/10] Updating system packages..."
$PKG_UPDATE
log "[2/10] Installing Python and development tools..."
$PKG_INSTALL $PYTHON_PKGS
log "[3/10] Installing Nginx..."
$PKG_INSTALL nginx
log "[4/10] Creating application user..."
useradd --system --group --no-create-home --shell /bin/false "$APP_NAME" || true
mkdir -p "/var/www/$APP_NAME"
chown "$APP_NAME:$APP_NAME" "/var/www/$APP_NAME"
log "[5/10] Creating Python virtual environment..."
cd "/var/www/$APP_NAME"
sudo -u "$APP_NAME" python3 -m venv venv
sudo -u "$APP_NAME" "/var/www/$APP_NAME/venv/bin/pip" install --upgrade pip
sudo -u "$APP_NAME" "/var/www/$APP_NAME/venv/bin/pip" install gunicorn
log "[6/10] Creating sample WSGI application..."
cat > "/var/www/$APP_NAME/wsgi.py" << 'EOF'
#!/usr/bin/env python3
def application(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/html; charset=utf-8')]
start_response(status, headers)
return [b'Hello from Gunicorn!']
if __name__ == '__main__':
from wsgiref.simple_server import make_server
server = make_server('127.0.0.1', 8000, application)
server.serve_forever()
EOF
chown "$APP_NAME:$APP_NAME" "/var/www/$APP_NAME/wsgi.py"
chmod 644 "/var/www/$APP_NAME/wsgi.py"
log "[7/10] Creating Gunicorn configuration..."
cat > "/var/www/$APP_NAME/gunicorn.conf.py" << EOF
# Gunicorn configuration file
bind = "$BIND_IP:$BIND_PORT"
backlog = 2048
workers = $WORKERS
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 50
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
accesslog = "/var/log/$APP_NAME/access.log"
errorlog = "/var/log/$APP_NAME/error.log"
loglevel = "info"
access_log_format = '%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"'
proc_name = '$APP_NAME'
daemon = False
pidfile = '/var/run/$APP_NAME/gunicorn.pid'
user = '$APP_NAME'
group = '$APP_NAME'
EOF
chown "$APP_NAME:$APP_NAME" "/var/www/$APP_NAME/gunicorn.conf.py"
chmod 644 "/var/www/$APP_NAME/gunicorn.conf.py"
log "[8/10] Creating log and run directories..."
mkdir -p "/var/log/$APP_NAME" "/var/run/$APP_NAME"
chown "$APP_NAME:$APP_NAME" "/var/log/$APP_NAME" "/var/run/$APP_NAME"
chmod 755 "/var/log/$APP_NAME" "/var/run/$APP_NAME"
log "[9/10] Creating systemd service..."
cat > "/etc/systemd/system/$APP_NAME.service" << EOF
[Unit]
Description=Gunicorn instance to serve $APP_NAME
After=network.target
[Service]
Type=notify
User=$APP_NAME
Group=$APP_NAME
RuntimeDirectory=$APP_NAME
WorkingDirectory=/var/www/$APP_NAME
ExecStart=/var/www/$APP_NAME/venv/bin/gunicorn --config /var/www/$APP_NAME/gunicorn.conf.py wsgi:application
ExecReload=/bin/kill -s HUP \$MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$APP_NAME"
systemctl start "$APP_NAME"
log "[10/10] Configuring Nginx reverse proxy..."
if [[ "$ID" =~ ^(ubuntu|debian)$ ]]; then
NGINX_CONF="/etc/nginx/sites-available/$APP_NAME"
cat > "$NGINX_CONF" << EOF
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://$BIND_IP:$BIND_PORT;
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;
}
}
EOF
ln -sf "$NGINX_CONF" "/etc/nginx/sites-enabled/$APP_NAME"
rm -f /etc/nginx/sites-enabled/default
else
NGINX_CONF="/etc/nginx/conf.d/$APP_NAME.conf"
cat > "$NGINX_CONF" << EOF
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://$BIND_IP:$BIND_PORT;
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;
}
}
EOF
fi
nginx -t
systemctl enable nginx
systemctl restart nginx
# Configure SELinux if present
if command -v setsebool >/dev/null 2>&1; then
setsebool -P httpd_can_network_connect 1 2>/dev/null || true
fi
# Configure firewall if present
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then
ufw allow 'Nginx Full' || true
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http 2>/dev/null || true
firewall-cmd --reload 2>/dev/null || true
fi
log "Verifying installation..."
sleep 2
if systemctl is-active --quiet "$APP_NAME"; then
log "✓ Gunicorn service is running"
else
error "✗ Gunicorn service is not running"
fi
if systemctl is-active --quiet nginx; then
log "✓ Nginx is running"
else
error "✗ Nginx is not running"
fi
if curl -s http://localhost | grep -q "Hello from Gunicorn"; then
log "✓ Application is accessible via Nginx"
else
warn "✗ Application may not be accessible via Nginx"
fi
log ""
log "Installation completed successfully!"
log "Application: $APP_NAME"
log "Service status: systemctl status $APP_NAME"
log "Logs: journalctl -u $APP_NAME -f"
log "Test URL: http://localhost"
log ""
log "To deploy your own application:"
log "1. Replace /var/www/$APP_NAME/wsgi.py with your WSGI application"
log "2. Install dependencies: sudo -u $APP_NAME /var/www/$APP_NAME/venv/bin/pip install -r requirements.txt"
log "3. Restart service: systemctl restart $APP_NAME"
Review the script before running. Execute with: bash install.sh