
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
- Ubuntu 20.04, 22.04, or 24.04 LTS VPS
- At least 512 MB RAM (1 GB recommended for Flask + Nginx)
- sudo or root access
- A domain with its A record pointing to your VPS IP (needed for SSL)
- Basic SSH knowledge — see SSH VPS Guide
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 -yVerify the installation:
python3 --version
pip3 --versionStep 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/activateInstall Flask and Gunicorn inside the virtualenv:
pip install flask gunicornCreate 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)
EOFCreate wsgi.py as the Gunicorn entry point:
cat > wsgi.py << 'EOF'
from app import app
if __name__ == '__main__':
app.run()
EOFTest that Gunicorn works:
gunicorn --bind 0.0.0.0:8000 wsgi:appOpen 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.servicePaste 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.targetHow 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 myflaskappYou should see Active: active (running).
Step 4: Install and Configure Nginx
Install Nginx:
sudo apt install nginx -yCreate an Nginx config for the Flask app:
sudo nano /etc/nginx/sites-available/myflaskappAdd 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 nginxStep 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 -yObtain an SSL certificate (domain must already point to this VPS):
sudo certbot --nginx -d your-domain.com -d www.your-domain.comCertbot automatically updates your Nginx config with HTTPS redirect and SSL settings. Test auto-renewal:
sudo certbot renew --dry-runTest Auto-start After Reboot
sudo rebootAfter reconnecting via SSH:
sudo systemctl status myflaskapp
sudo systemctl status nginxBoth 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 nginxImproving 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 geventUpdate 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 APIs | 1 request per worker at a time |
| gevent | I/O-heavy apps, external API calls | 1000+ concurrent requests per worker |
| gthread | Thread-safe code | Uses threads instead of greenlets |
Reload systemd and restart the service after editing the unit file:
sudo systemctl daemon-reload
sudo systemctl restart myflaskappNginx 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/envContents 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=productionRestrict access to root only:
sudo chmod 600 /etc/myflaskapp/env
sudo chown root:root /etc/myflaskapp/envRead 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_USERNAMEFlask 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 →