Deploy Python Flask on VPS with Gunicorn and Nginx

Flask is a lightweight Python web framework ideal for building REST APIs, web applications, and microservices. However, Flask's built-in development server is not suitable for production — it handles limited concurrency and lacks stability. The correct approach is to serve Flask through Gunicorn (a WSGI server) and place Nginx in front as a reverse proxy. This guide walks you through a complete production-ready setup on Ubuntu VPS.

What you'll have at the end: Flask App → Gunicorn → Nginx → Domain with HTTPS, auto-starting on every VPS reboot.

Prerequisites

Step 1: Update Server and Install Python 3

Ubuntu ships with Python 3, but update packages first:

sudo apt update && sudo apt upgrade -y sudo apt install python3 python3-pip python3-venv -y

Verify the installation:

python3 --version pip3 --version

Step 2: Create Flask App and Virtual Environment

Using a virtual environment isolates each project's dependencies and prevents package conflicts:

mkdir -p ~/myflaskapp && cd ~/myflaskapp python3 -m venv venv source venv/bin/activate

Install Flask and Gunicorn inside the virtualenv:

pip install flask gunicorn

Create app.py:

cat > app.py << 'EOF' from flask import Flask, jsonify app = Flask(__name__) @app.route('/') def index(): return jsonify({"message": "Hello from AsiaGB Flask App!", "status": "ok"}) @app.route('/health') def health(): return jsonify({"status": "healthy"}) if __name__ == '__main__': app.run(debug=False) EOF

Create wsgi.py as the Gunicorn entry point:

cat > wsgi.py << 'EOF' from app import app if __name__ == '__main__': app.run() EOF

Test that Gunicorn works:

gunicorn --bind 0.0.0.0:8000 wsgi:app

Open http://YOUR_VPS_IP:8000 in a browser. You should see the JSON response. Press Ctrl+C to stop.

Step 3: Create a systemd Service for Gunicorn

To auto-start the Flask app on boot, create a systemd unit file:

sudo nano /etc/systemd/system/myflaskapp.service

Paste the following (replace YOUR_USERNAME):

[Unit] Description=Gunicorn instance to serve Flask App After=network.target [Service] User=YOUR_USERNAME Group=www-data WorkingDirectory=/home/YOUR_USERNAME/myflaskapp Environment="PATH=/home/YOUR_USERNAME/myflaskapp/venv/bin" ExecStart=/home/YOUR_USERNAME/myflaskapp/venv/bin/gunicorn \ --workers 3 \ --bind unix:myflaskapp.sock \ -m 007 \ wsgi:app [Install] WantedBy=multi-user.target

How many workers? The recommended formula is 2 × CPU cores + 1. A 1-core VPS should use 3 workers; a 2-core VPS should use 5 workers.

Start and enable the service:

sudo systemctl start myflaskapp sudo systemctl enable myflaskapp sudo systemctl status myflaskapp

You should see Active: active (running).

Step 4: Install and Configure Nginx

Install Nginx:

sudo apt install nginx -y

Create an Nginx config for the Flask app:

sudo nano /etc/nginx/sites-available/myflaskapp

Add the following (replace domain and path):

server { listen 80; server_name your-domain.com www.your-domain.com; location / { include proxy_params; proxy_pass http://unix:/home/YOUR_USERNAME/myflaskapp/myflaskapp.sock; } location /static { alias /home/YOUR_USERNAME/myflaskapp/static; expires 30d; add_header Cache-Control "public, no-transform"; } }

Enable the config and test:

sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx

Step 5: Configure Firewall

sudo ufw allow 'Nginx Full' sudo ufw allow OpenSSH sudo ufw enable sudo ufw status

⚠️ Important: Always allow OpenSSH before enabling ufw. If you forget, you will be locked out of the VPS immediately.

Step 6: Install Free SSL with Certbot

Install Certbot for Nginx:

sudo apt install certbot python3-certbot-nginx -y

Obtain an SSL certificate (domain must already point to this VPS):

sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Certbot automatically updates your Nginx config with HTTPS redirect and SSL settings. Test auto-renewal:

sudo certbot renew --dry-run

Test Auto-start After Reboot

sudo reboot

After reconnecting via SSH:

sudo systemctl status myflaskapp sudo systemctl status nginx

Both should show active (running).

Common Management Commands

# Restart after code changes sudo systemctl restart myflaskapp # Stream Gunicorn logs sudo journalctl -u myflaskapp -f # Stream Nginx access/error logs sudo tail -f /var/log/nginx/access.log sudo tail -f /var/log/nginx/error.log # Test Nginx config after editing sudo nginx -t && sudo systemctl reload nginx

Improving Gunicorn Performance — Async Workers and Connection Tuning

Gunicorn's default sync worker handles one request at a time per worker. If your Flask app is I/O-heavy — calling external APIs, running database queries, or waiting on network responses — switching to async workers lets each worker handle thousands of concurrent connections without adding more CPU:

pip install gevent

Update ExecStart in your systemd unit file:

ExecStart=/home/YOUR_USERNAME/myflaskapp/venv/bin/gunicorn \ --workers 3 \ --worker-class gevent \ --worker-connections 1000 \ --timeout 60 \ --bind unix:myflaskapp.sock \ -m 007 \ wsgi:app
Worker Class Best For Notes
sync (default)CPU-bound apps, simple APIs1 request per worker at a time
geventI/O-heavy apps, external API calls1000+ concurrent requests per worker
gthreadThread-safe codeUses threads instead of greenlets

Reload systemd and restart the service after editing the unit file:

sudo systemctl daemon-reload sudo systemctl restart myflaskapp

Nginx Caching and Rate Limiting for Flask

Nginx can cache responses from Gunicorn to reduce application load, and apply rate limits to prevent abuse. Add the following to your Nginx configuration:

# Outside the server block — define cache zone and rate limit zone proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=flask_cache:10m max_size=100m inactive=60m; limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m; server { listen 443 ssl; server_name your-domain.com; # Cache API responses for 5 minutes location /api/ { limit_req zone=api_limit burst=10 nodelay; proxy_cache flask_cache; proxy_cache_valid 200 5m; proxy_cache_use_stale error timeout; add_header X-Cache-Status $upstream_cache_status; include proxy_params; proxy_pass http://unix:/home/YOUR_USERNAME/myflaskapp/myflaskapp.sock; } # Skip cache for dynamic endpoints location / { proxy_no_cache 1; include proxy_params; proxy_pass http://unix:/home/YOUR_USERNAME/myflaskapp/myflaskapp.sock; } }

Tip: Check the X-Cache-Status: HIT / MISS response header in your browser's DevTools → Network tab to confirm caching is working. A HIT means Nginx served the response directly without forwarding to Gunicorn.

Secure Configuration with Environment Variables

Hard-coding secret keys, database URIs, or API tokens directly in your code is a serious security risk. The correct approach is to inject secrets at runtime using environment variables via the systemd unit file:

[Service] ... EnvironmentFile=/etc/myflaskapp/env ExecStart=...

Create the env file and lock down permissions:

sudo mkdir -p /etc/myflaskapp sudo nano /etc/myflaskapp/env

Contents of the env file:

FLASK_SECRET_KEY=your-very-long-random-secret-key-here DATABASE_URL=mysql+pymysql://user:password@localhost/dbname REDIS_URL=redis://127.0.0.1:6379/0 FLASK_ENV=production

Restrict access to root only:

sudo chmod 600 /etc/myflaskapp/env sudo chown root:root /etc/myflaskapp/env

Read secrets in app.py via os.environ:

import os from flask import Flask app = Flask(__name__) app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'fallback-dev-key') DATABASE_URL = os.environ.get('DATABASE_URL')

⚠️ Never commit secrets to Git. Add /etc/myflaskapp/env and any .env files to your .gitignore. Leaked credentials in public repositories are among the most common causes of production security incidents.

Troubleshooting

502 Bad Gateway

Usually means Gunicorn is not running. Check with sudo systemctl status myflaskapp and review logs with journalctl -u myflaskapp -e.

Permission Denied on Socket

The Nginx user (www-data) needs read access to the socket in your home directory. Add www-data to your user's group:

sudo usermod -aG YOUR_USERNAME www-data sudo chmod 710 /home/YOUR_USERNAME

Flask Debug Mode in Production

Make sure debug=True is not set in app.py and that the environment variable FLASK_DEBUG=1 is not set. Debug mode enables Werkzeug's interactive debugger, which is a serious security vulnerability in production.

Summary: Python 3 + virtualenv → Flask + Gunicorn → systemd Service → Nginx Reverse Proxy → UFW Firewall → Let's Encrypt SSL = production-ready Flask on VPS.

Need a VPS for Python Flask?

AsiaGB offers Linux Ubuntu VPS starting at ฿500/month · Full Root Access · SSD · 1 Dedicated IP

View VPS Plans →