DeployWise
HomeGuidesDeploy Flask to VPS
FlaskPythonVPSGunicornNginxSSL

Deploy Flask to a VPS — Gunicorn + Nginx Guide 2026

Step-by-step tutorial: deploy a production-ready Flask app to any Ubuntu VPS. Covers Gunicorn WSGI setup, systemd process management, Nginx reverse proxy, environment variables, static files, and free SSL with DeployWise.

14 min read
Updated 2026

What you need

A VPS running Ubuntu 20.04 or later (DigitalOcean, Hetzner, Vultr, etc.)
SSH access — either password or private key
A GitHub account with your Flask repo
A domain name (optional, but recommended for SSL)
Python 3.8+ installed locally for development
Basic familiarity with command line and pip

Why deploy Flask to a VPS?

Flask is one of the most popular Python web frameworks, powering APIs and full-stack apps across thousands of companies. Managed platforms like Heroku or Render charge $25-50+/month per app and restrict your runtime environment. A $5/month VPS from Hetzner or DigitalOcean gives you full root access, no cold starts, and can host multiple Flask apps simultaneously.

The challenge is configuring Gunicorn, Nginx, systemd, and SSL correctly — but that's exactly what DeployWise solves. One click and your Flask app is live with production-grade infrastructure.

1VPS setup — install Python, pip, and venv

SSH into your server and install the required system packages:

bash
# Update system packages
sudo apt update && sudo apt upgrade -y

# Install Python 3, pip, venv, and build essentials
sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential

# Verify installation
python3 --version   # Python 3.10+ on Ubuntu 22.04
pip3 --version

# Install Nginx (we'll configure it later)
sudo apt install -y nginx

# Allow HTTP and HTTPS through the firewall
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable

Create a dedicated user for your application (recommended for security):

bash
# Create a deploy user
sudo adduser --disabled-password --gecos "" deploy

# Add to www-data group (for Nginx socket access)
sudo usermod -aG www-data deploy

# Switch to deploy user
sudo su - deploy

2Clone project and create virtual environment

Clone your Flask repository and set up an isolated Python environment:

bash
# Clone your Flask project
cd /home/deploy
git clone https://github.com/yourusername/your-flask-app.git
cd your-flask-app

# Create a virtual environment
python3 -m venv venv

# Activate the virtual environment
source venv/bin/activate

# Upgrade pip inside the venv
pip install --upgrade pip

# Install project dependencies
pip install -r requirements.txt

# Install Gunicorn (if not in requirements.txt)
pip install gunicorn
Always use virtual environments

Never install Python packages globally on your server. Virtual environments isolate dependencies per project, prevent version conflicts, and make it easy to reproduce your environment.

3Test that Flask runs

Before setting up Gunicorn, verify your Flask app starts correctly:

bash
# Make sure you're in the project directory with venv activated
cd /home/deploy/your-flask-app
source venv/bin/activate

# Option A: Run with Flask's dev server
flask run --host=0.0.0.0 --port=5000

# Option B: Run directly
python app.py

# Test from another terminal (or locally with curl)
curl http://your-server-ip:5000

If your app returns a response, you're ready to configure Gunicorn. If it fails, check that all dependencies are installed and environment variables are set.

Never use Flask's dev server in production

Flask's built-in server is single-threaded and not designed for production traffic. Always use a WSGI server like Gunicorn for production deployments.

4Set up Gunicorn WSGI server

First, create a WSGI entry point file so Gunicorn can import your Flask app:

Create wsgi.py

python
# wsgi.py (in your project root)
from app import app

if __name__ == "__main__":
    app.run()

If your Flask app factory uses create_app(), adjust accordingly:

python
# wsgi.py (for app factory pattern)
from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run()

Test Gunicorn

Run Gunicorn manually first to verify everything works:

bash
# Activate venv and test Gunicorn
cd /home/deploy/your-flask-app
source venv/bin/activate

# Test with TCP binding (for quick verification)
gunicorn --workers 3 --bind 0.0.0.0:5000 wsgi:app

# Test with Unix socket (production setup)
gunicorn --workers 3 --bind unix:myapp.sock wsgi:app

Key Gunicorn settings explained:

--workers 3Number of worker processes (recommended: 2 x CPU cores + 1)
--bind unix:myapp.sockBind to a Unix socket instead of TCP — faster and more secure for local Nginx proxying
wsgi:appModule:variable — import 'app' from wsgi.py
--timeout 120Kill workers that are silent for 120 seconds (default: 30)
--access-logfile -Log access requests to stdout (captured by systemd journal)

5Create a systemd service for Gunicorn

A systemd service ensures Gunicorn starts on boot and restarts automatically if it crashes. Create the service file:

ini
# /etc/systemd/system/myflaskapp.service

[Unit]
Description=Gunicorn instance to serve Flask application
After=network.target

[Service]
User=deploy
Group=www-data
WorkingDirectory=/home/deploy/your-flask-app
Environment="PATH=/home/deploy/your-flask-app/venv/bin"
ExecStart=/home/deploy/your-flask-app/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:myapp.sock \
    --access-logfile /var/log/gunicorn/access.log \
    --error-logfile /var/log/gunicorn/error.log \
    --timeout 120 \
    wsgi:app
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Enable and start the service:

bash
# Create log directory
sudo mkdir -p /var/log/gunicorn
sudo chown deploy:www-data /var/log/gunicorn

# Reload systemd to pick up the new service
sudo systemctl daemon-reload

# Start Gunicorn
sudo systemctl start myflaskapp

# Enable on boot
sudo systemctl enable myflaskapp

# Check status
sudo systemctl status myflaskapp

# View logs if something is wrong
sudo journalctl -u myflaskapp -f
Verify the socket was created

After starting the service, run ls -la /home/deploy/your-flask-app/myapp.sock to confirm the Unix socket exists. If it's missing, check the journal logs for errors.

6Nginx reverse proxy configuration

Nginx sits in front of Gunicorn, handling SSL termination, static file serving, and buffering slow clients:

nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    client_max_body_size 10M;

    # Serve static files directly (bypass Gunicorn)
    location /static/ {
        alias /home/deploy/your-flask-app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://unix:/home/deploy/your-flask-app/myapp.sock;
        proxy_http_version 1.1;

        # Pass original request info
        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;
    }
}

Enable the site

bash
# Create the config file
sudo nano /etc/nginx/sites-available/myflaskapp

# Enable the site by creating a symlink
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/

# Remove the default site (optional)
sudo rm /etc/nginx/sites-enabled/default

# Test Nginx configuration for syntax errors
sudo nginx -t

# Reload Nginx to apply changes
sudo systemctl reload nginx

Visit http://yourdomain.com — you should see your Flask app served through Nginx.

7SSL with Let's Encrypt (Certbot)

Get a free SSL certificate and automatic HTTPS redirect:

bash
# Install Certbot with Nginx plugin
sudo apt install -y certbot python3-certbot-nginx

# Obtain SSL certificate (auto-configures Nginx)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Verify auto-renewal works
sudo certbot renew --dry-run

# Certbot sets up a systemd timer for auto-renewal
sudo systemctl list-timers | grep certbot

Certbot automatically modifies your Nginx config to listen on port 443, redirect HTTP to HTTPS, and renew certificates before they expire.

8Environment variables with python-dotenv

Never hardcode secrets. Use environment variables loaded from a .env file:

Install python-dotenv

bash
pip install python-dotenv

Create .env on the server

bash
# /home/deploy/your-flask-app/.env
FLASK_ENV=production
SECRET_KEY=your-production-secret-key-here
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
REDIS_URL=redis://localhost:6379/0
MAIL_SERVER=smtp.yourdomain.com
MAIL_USERNAME=noreply@yourdomain.com
MAIL_PASSWORD=your-mail-password

Load env in your Flask app

python
# app.py or config.py
import os
from dotenv import load_dotenv

load_dotenv()  # Load .env file

from flask import Flask

app = Flask(__name__)

app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'fallback-dev-key')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
app.config['DEBUG'] = False  # Never True in production
bash
# Lock down .env permissions (only owner can read)
chmod 600 /home/deploy/your-flask-app/.env

# Add .env to .gitignore
echo ".env" >> .gitignore
Never expose secrets

Always add .env to .gitignore. Never commit secrets to version control. Use different SECRET_KEY values for development and production.

Common Flask deployment pitfalls

ModuleNotFoundError when Gunicorn starts

Gunicorn is running outside the virtual environment. Ensure ExecStart in your systemd service uses the full path to the venv's Gunicorn binary: /home/deploy/your-flask-app/venv/bin/gunicorn. Also verify all dependencies are installed in the venv with pip freeze.

Socket permission denied (502 Bad Gateway)

Nginx can't access the Unix socket. Ensure the Gunicorn process runs as a user in the www-data group (Group=www-data in the systemd service). The socket file should have permissions srw-rw----. Run 'sudo usermod -aG www-data deploy' and restart both services.

Static files return 404 in production

Flask's built-in static file serving is disabled or slow in production. Configure Nginx to serve /static/ directly with an alias directive pointing to your Flask app's static folder. This bypasses Gunicorn entirely for static assets.

App crashes with 'Address already in use'

Another process is using the same port or socket. Find it with 'sudo lsof -i :5000' or check if an old Gunicorn process is still running. Kill it with 'sudo kill -9 PID' or remove the stale .sock file.

Database connection errors after deployment

Check that DATABASE_URL is set in your .env file and that python-dotenv is installed. Also verify the database server is running and accepts connections from localhost. If using PostgreSQL, ensure psycopg2-binary is in requirements.txt.

Gunicorn workers keep dying (timeout)

Increase the --timeout value in your systemd service (default is 30 seconds). Long-running requests or slow database queries will kill workers. Set --timeout 120 or higher. For background tasks, use Celery instead of blocking Gunicorn workers.

Production tips

Structured Logging

Configure Python's logging module for production-grade log output:

python
import logging
from logging.handlers import RotatingFileHandler

if not app.debug:
    file_handler = RotatingFileHandler(
        'logs/flask_app.log',
        maxBytes=10240000,  # 10 MB
        backupCount=10
    )
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)
    app.logger.setLevel(logging.INFO)
    app.logger.info('Flask app startup')

Error Handling

Register custom error handlers so users never see raw tracebacks:

python
@app.errorhandler(404)
def not_found_error(error):
    return {'error': 'Not found'}, 404

@app.errorhandler(500)
def internal_error(error):
    app.logger.error(f'Server Error: {error}')
    return {'error': 'Internal server error'}, 500

@app.errorhandler(Exception)
def unhandled_exception(error):
    app.logger.error(f'Unhandled Exception: {error}')
    return {'error': 'Something went wrong'}, 500

Rate Limiting

Protect your API from abuse with Flask-Limiter:

python
pip install Flask-Limiter

# app.py
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379/0"
)

@app.route('/api/data')
@limiter.limit("10 per minute")
def get_data():
    return {'data': 'your response'}

Frequently asked questions

How many requests can Flask handle with Gunicorn on a VPS?

A single $5 VPS (1 vCPU, 1 GB RAM) running Gunicorn with 3 workers can handle roughly 500-1,000 requests per second for simple endpoints. Adding more workers or upgrading to a 2-vCPU server doubles throughput. For CPU-heavy workloads, consider using Gunicorn with gevent or uvicorn workers.

Should I use Gunicorn or uWSGI for Flask in production?

Gunicorn is recommended for most Flask deployments. It is simpler to configure, well-documented, and performs on par with uWSGI for typical web applications. uWSGI offers more advanced features but adds complexity. Start with Gunicorn unless you have a specific need for uWSGI.

Why do I need Nginx in front of Gunicorn?

Nginx handles SSL termination, serves static files efficiently, buffers slow client connections (protecting Gunicorn workers), provides rate limiting, and acts as a load balancer. Gunicorn is not designed to face the internet directly.

How do I update my Flask app after the initial deployment?

SSH into your server, navigate to the project directory, run git pull, activate the virtual environment, install new dependencies with pip install -r requirements.txt, then restart with sudo systemctl restart myflaskapp. DeployWise automates this entire process.

Can I deploy multiple Flask apps on the same VPS?

Yes. Create separate virtual environments, systemd services, and Nginx server blocks for each app. Each app gets its own Unix socket. A $5 VPS can comfortably run 3-5 small Flask apps.

How do I serve Flask static files in production?

Configure Nginx to serve static files directly with a location /static/ block pointing to your Flask app's static directory. This bypasses Gunicorn and is significantly faster than Flask's built-in static file serving.

How DeployWise automates Flask deployment

Every step above — Python setup, Gunicorn config, systemd service, Nginx, SSL — is executed automatically by DeployWise when you click Deploy. You never touch the terminal:

1.Install Python 3, pip, and venv (if not present)
2.Clone your Flask repo from GitHub
3.Create virtual environment and install requirements
4.Create wsgi.py entry point
5.Configure Gunicorn with optimal worker count
6.Create systemd service for auto-restart and boot startup
7.Set environment variables securely from your dashboard
8.Configure Nginx reverse proxy with static file serving
9.Issue Let's Encrypt SSL certificate (if domain configured)
10.Set up log rotation
11.Verify health check passes

The entire process takes 30-60 seconds. You can watch every step live in the deployment log panel.

Subsequent Deployments

On future pushes to your branch, DeployWise runs a zero-downtime update:

1.Pull latest code from GitHub
2.Install new/changed dependencies in the venv
3.Run database migrations (if configured)
4.Restart Gunicorn via systemd (graceful reload)
5.Verify health check passes

What happens after deployment

Your Flask app is now:

Running under Gunicorn with multiple workers — handles concurrent requests efficiently
Managed by systemd — auto-restarts on crash, starts on server boot
Proxied through Nginx — handles HTTPS, serves static files, buffers slow clients
Protected with SSL — free Let's Encrypt certificate, auto-renewing
Accessible at your domain or via server IP
Logged and monitored — check status with systemctl, logs with journalctl

Future deploys are one click away via GitHub push (if auto-deploy enabled). If something breaks, roll back to any previous commit from the deployment history.

Getting started with DeployWise

Three simple steps to deploy your Flask app to any VPS:

1. Sign in with GitHub

Open DeployWise dashboard and authenticate with your GitHub account

2. Add your VPS

Enter your server's IP/hostname, SSH port, username and credentials. DeployWise tests the connection.

3. Create & deploy

Select your Flask repo, configure environment variables, choose a domain, then click Deploy. Done in 30-60 seconds.

Ready to deploy Flask?

Sign in with GitHub, add your VPS, and have your Flask app live with Gunicorn, Nginx and SSL — all in minutes, for free.

Open DeployWise Dashboard

Related guides