
How to Serve Flask Applications with Gunicorn and Nginx on Ubuntu 24
Setting up Flask applications in production requires more than just running `flask run` – you need a robust application server and reverse proxy to handle real-world traffic. Gunicorn serves as the WSGI server that manages your Flask processes, while Nginx acts as the reverse proxy handling static files, SSL termination, and load balancing. This guide walks you through deploying Flask applications on Ubuntu 24 using this battle-tested stack, covering everything from basic setup to performance optimization and troubleshooting common deployment headaches.
Understanding the Flask + Gunicorn + Nginx Stack
Flask’s built-in development server is single-threaded and designed for development, not production. When you deploy a Flask app to handle real users, you need:
- WSGI Server (Gunicorn): Spawns multiple worker processes to handle concurrent requests
- Reverse Proxy (Nginx): Handles static files, SSL, compression, and forwards dynamic requests to Gunicorn
- Process Manager (systemd): Ensures your application starts on boot and restarts on crashes
The request flow looks like this: Client β Nginx β Gunicorn β Flask App. Nginx handles what it can (static files, caching) and passes dynamic requests to Gunicorn, which manages multiple Flask processes.
Component | Primary Role | Performance Benefit |
---|---|---|
Flask Dev Server | Development only | Single-threaded, ~1 request/second |
Gunicorn | WSGI application server | Multi-process, ~1000+ requests/second |
Nginx | Reverse proxy, static files | ~10,000+ static requests/second |
Prerequisites and Initial Setup
Start with a fresh Ubuntu 24.04 system and update packages:
sudo apt update && sudo apt upgrade -y
sudo apt install python3-pip python3-venv nginx -y
Create a dedicated user for your application (security best practice):
sudo adduser --system --group --home /opt/flaskapp flaskapp
sudo mkdir -p /opt/flaskapp
sudo chown flaskapp:flaskapp /opt/flaskapp
Setting Up Your Flask Application
Switch to the application user and create a sample Flask app:
sudo -u flaskapp -i
cd /opt/flaskapp
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn
Create a simple Flask application for testing:
# /opt/flaskapp/app.py
from flask import Flask, jsonify
import os
import socket
app = Flask(__name__)
@app.route('/')
def home():
return jsonify({
'message': 'Hello from Flask!',
'worker_pid': os.getpid(),
'hostname': socket.gethostname()
})
@app.route('/health')
def health():
return jsonify({'status': 'healthy'}), 200
@app.route('/api/users')
def users():
# Simulate some processing
users = [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'}
]
return jsonify(users)
if __name__ == '__main__':
app.run(debug=False)
Create a WSGI entry point:
# /opt/flaskapp/wsgi.py
from app import app
if __name__ == "__main__":
app.run()
Test that your Flask app works with Gunicorn:
gunicorn --bind 0.0.0.0:8000 wsgi:app
Configuring Gunicorn for Production
Create a Gunicorn configuration file for production settings:
# /opt/flaskapp/gunicorn.conf.py
import multiprocessing
# Server socket
bind = "127.0.0.1:8000"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
max_requests = 1000
max_requests_jitter = 50
# Logging
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = 'flaskapp_gunicorn'
# Security
limit_request_line = 4096
limit_request_fields = 100
limit_request_field_size = 8190
# Server mechanics
preload_app = True
daemon = False
pidfile = "/var/run/gunicorn/flaskapp.pid"
user = "flaskapp"
group = "flaskapp"
tmp_upload_dir = None
Create necessary directories and set permissions:
sudo mkdir -p /var/log/gunicorn /var/run/gunicorn
sudo chown flaskapp:flaskapp /var/log/gunicorn /var/run/gunicorn
Creating a Systemd Service
Create a systemd service file to manage your Flask application:
# /etc/systemd/system/flaskapp.service
[Unit]
Description=Gunicorn instance to serve Flask App
After=network.target
[Service]
User=flaskapp
Group=flaskapp
WorkingDirectory=/opt/flaskapp
Environment="PATH=/opt/flaskapp/venv/bin"
ExecStart=/opt/flaskapp/venv/bin/gunicorn --config /opt/flaskapp/gunicorn.conf.py wsgi:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable flaskapp
sudo systemctl start flaskapp
sudo systemctl status flaskapp
Configuring Nginx as Reverse Proxy
Remove the default Nginx configuration and create a new one:
sudo rm /etc/nginx/sites-enabled/default
sudo nano /etc/nginx/sites-available/flaskapp
Add the following Nginx configuration:
# /etc/nginx/sites-available/flaskapp
upstream flaskapp_upstream {
server 127.0.0.1:8000;
keepalive 32;
}
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Static files (if you have any)
location /static {
alias /opt/flaskapp/static;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint (bypass upstream for faster response)
location /health {
access_log off;
proxy_pass http://flaskapp_upstream;
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_connect_timeout 5s;
proxy_send_timeout 5s;
proxy_read_timeout 5s;
}
# Main application
location / {
proxy_pass http://flaskapp_upstream;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# Logging
access_log /var/log/nginx/flaskapp_access.log;
error_log /var/log/nginx/flaskapp_error.log;
}
Enable the site and test the configuration:
sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Performance Optimization and Tuning
Here are key performance optimizations for the stack:
Gunicorn Worker Tuning
Worker Type | Use Case | Pros | Cons |
---|---|---|---|
sync | CPU-bound, simple apps | Simple, reliable | Blocks on I/O |
gevent | I/O-bound apps | High concurrency | Complex debugging |
gthread | Mixed workloads | Good balance | GIL limitations |
For most Flask applications, use the sync worker with this formula:
# Workers = (2 Γ CPU cores) + 1
# For a 4-core server: workers = 9
# Test different configurations:
gunicorn --workers 4 --worker-class sync wsgi:app
gunicorn --workers 4 --worker-class gevent --worker-connections 1000 wsgi:app
Nginx Optimization
Add these optimizations to `/etc/nginx/nginx.conf`:
# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
# Basic optimization
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
# Buffer optimization
client_body_buffer_size 128k;
client_max_body_size 10m;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;
output_buffers 1 32k;
postpone_output 1460;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
}
SSL/TLS Configuration with Let’s Encrypt
Install Certbot and obtain SSL certificates:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
The SSL configuration will be automatically added, but here’s what a complete HTTPS setup looks like:
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL optimization
ssl_session_timeout 1d;
ssl_session_cache shared:MozTLS:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# Rest of your configuration...
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name your-domain.com www.your-domain.com;
return 301 https://$server_name$request_uri;
}
Monitoring and Logging
Set up proper logging and monitoring for production applications:
# Create log rotation for Gunicorn
sudo nano /etc/logrotate.d/gunicorn
/var/log/gunicorn/*.log {
daily
missingok
rotate 52
compress
delaycompress
notifempty
create 644 flaskapp flaskapp
postrotate
systemctl reload flaskapp
endscript
}
Add basic monitoring to your Flask app:
# Add to your Flask app
import time
import logging
from flask import request, g
@app.before_request
def before_request():
g.start_time = time.time()
@app.after_request
def after_request(response):
total_time = time.time() - g.start_time
app.logger.info(f'{request.method} {request.path} - {response.status_code} - {total_time:.3f}s')
return response
Common Issues and Troubleshooting
502 Bad Gateway Errors
The most common issue when setting up this stack. Check these in order:
# 1. Is Gunicorn running?
sudo systemctl status flaskapp
sudo journalctl -u flaskapp -f
# 2. Is Gunicorn binding to the right address?
ss -tlnp | grep :8000
# 3. Check Nginx error logs
sudo tail -f /var/log/nginx/error.log
# 4. Test Gunicorn directly
curl http://127.0.0.1:8000/
Permission Issues
# Fix ownership issues
sudo chown -R flaskapp:flaskapp /opt/flaskapp
sudo chmod +x /opt/flaskapp/venv/bin/gunicorn
# SELinux issues (if enabled)
sudo setsebool -P httpd_can_network_connect 1
Performance Issues
Symptom | Likely Cause | Solution |
---|---|---|
High response times | Too few workers | Increase Gunicorn workers |
Memory usage spikes | Memory leaks or too many workers | Enable max_requests, reduce workers |
Connection refused | Worker timeout | Increase timeout values |
Debugging Commands
# Check all services
sudo systemctl status nginx flaskapp
# Monitor resource usage
htop
iotop
# Test configuration
sudo nginx -t
/opt/flaskapp/venv/bin/gunicorn --check-config gunicorn.conf.py
# Network debugging
netstat -tlnp | grep -E '(80|443|8000)'
curl -I http://localhost/
Real-World Production Considerations
Load Balancing Multiple Instances
For high-traffic applications, run multiple Gunicorn instances:
# /etc/nginx/sites-available/flaskapp
upstream flaskapp_upstream {
server 127.0.0.1:8000 weight=3;
server 127.0.0.1:8001 weight=3;
server 127.0.0.1:8002 backup;
keepalive 32;
keepalive_requests 100;
}
Database Connection Pooling
When using databases, configure connection pools properly:
# For SQLAlchemy
from sqlalchemy import create_engine
engine = create_engine(
'postgresql://user:pass@localhost/db',
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=3600
)
Environment-Specific Configuration
# /opt/flaskapp/.env
FLASK_ENV=production
DATABASE_URL=postgresql://user:pass@localhost/prod_db
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=your-secret-key-here
Load environment variables in your systemd service:
[Service]
EnvironmentFile=/opt/flaskapp/.env
Security Best Practices
- Firewall Configuration: Only expose necessary ports (80, 443)
- Regular Updates: Keep Ubuntu, Python, and all packages updated
- Process Isolation: Run Flask app as dedicated non-root user
- Rate Limiting: Implement rate limiting in Nginx
- Input Validation: Always validate and sanitize user input
- Secrets Management: Use environment variables or dedicated secret management
This setup provides a solid foundation for Flask applications in production. The combination of Gunicorn and Nginx handles thousands of concurrent requests while maintaining security and performance. For additional resources, check the official Gunicorn documentation and Nginx documentation. Consider upgrading to a dedicated server from MangoHost for high-traffic applications that need consistent performance and additional resources.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.