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.
What you need
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:
# 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):
# 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:
# 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
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:
# 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.
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
# 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:
# 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:
# 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:
5Create a systemd service for Gunicorn
A systemd service ensures Gunicorn starts on boot and restarts automatically if it crashes. Create the service file:
# /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.targetEnable and start the service:
# 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
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:
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
# 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:
# 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
pip install python-dotenv
Create .env on the server
# /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
# 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# Lock down .env permissions (only owner can read) chmod 600 /home/deploy/your-flask-app/.env # Add .env to .gitignore echo ".env" >> .gitignore
Always add .env to .gitignore. Never commit secrets to version control. Use different SECRET_KEY values for development and production.
Common Flask deployment pitfalls
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.
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.
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.
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.
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.
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:
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:
@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'}, 500Rate Limiting
Protect your API from abuse with Flask-Limiter:
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
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.
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.
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.
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.
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.
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:
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:
What happens after deployment
Your Flask app is now:
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:
Open DeployWise dashboard and authenticate with your GitHub account
Enter your server's IP/hostname, SSH port, username and credentials. DeployWise tests the connection.
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