Reference Guide Detailed deployment notes with production context and concrete examples.

Deploy Flask on Ubuntu VPS (Step-by-Step)

If you're trying to deploy a Flask app on an Ubuntu VPS, this guide shows you how to set up a production-ready stack step-by-step. The outcome is a running Flask application behind Gunicorn and Nginx, managed by systemd and reachable through your server domain or IP.

Quick Setup

Use this on a fresh Ubuntu VPS to validate the stack end-to-end:

bash
sudo apt update && sudo apt install -y python3 python3-venv python3-pip nginx
sudo mkdir -p /var/www/flaskapp && cd /var/www/flaskapp
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn

cat > app.py <<'EOF'
from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Flask app is running on Ubuntu VPS'
EOF

cat > wsgi.py <<'EOF'
from app import app
EOF

sudo tee /etc/systemd/system/flaskapp.service > /dev/null <<'EOF'
[Unit]
Description=Gunicorn instance for flaskapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/flaskapp
Environment="PATH=/var/www/flaskapp/venv/bin"
ExecStart=/var/www/flaskapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now flaskapp

sudo tee /etc/nginx/sites-available/flaskapp > /dev/null <<'EOF'
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp
sudo nginx -t && sudo systemctl reload nginx

curl -I http://127.0.0.1:8000
curl -I http://YOUR_SERVER_IP

Replace the sample app, server_name, paths, and service settings before production launch.

What’s Happening

A standard Flask deployment on Ubuntu uses:

  • Gunicorn to run the Flask app as the WSGI server
  • systemd to start, stop, and restart Gunicorn reliably
  • Nginx to accept public traffic and proxy requests to Gunicorn

Most failures come from bad paths, incorrect import targets, missing dependencies, or an Nginx upstream that does not match the Gunicorn bind address.

Step-by-Step Guide

1. Update Ubuntu and install required packages

bash
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3 python3-venv python3-pip nginx git

2. Create the application directory

bash
sudo mkdir -p /var/www/myflaskapp
sudo chown -R $USER:$USER /var/www/myflaskapp
cd /var/www/myflaskapp

3. Copy or clone your Flask project

bash
git clone YOUR_REPO_URL .

If the app already exists locally, copy it into /var/www/myflaskapp.

4. Create a virtual environment and install dependencies

bash
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
pip install gunicorn

If you do not have a requirements.txt yet, install your app dependencies manually.

5. Create or verify the WSGI entry point

Create wsgi.py in the project root:

python
from yourapp import app

If your Flask app object is in another module or package, adjust it accordingly. Examples:

python
from app import app

or:

python
from project import create_app
app = create_app()

6. Test Gunicorn directly before configuring systemd

bash
/var/www/myflaskapp/venv/bin/gunicorn --bind 127.0.0.1:8000 wsgi:app

If it starts without import errors, leave it running temporarily.

7. Verify the app responds on the local Gunicorn port

In another shell:

bash
curl -I http://127.0.0.1:8000

Expected result: an HTTP response such as 200 OK or your app’s normal redirect/status.

Stop Gunicorn after the test with Ctrl+C.

8. Create the systemd service

Create /etc/systemd/system/myflaskapp.service:

ini
[Unit]
Description=Gunicorn instance for myflaskapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myflaskapp
Environment="PATH=/var/www/myflaskapp/venv/bin"
ExecStart=/var/www/myflaskapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app

[Install]
WantedBy=multi-user.target

If your app needs environment variables, add them here:

ini
Environment="FLASK_ENV=production"
Environment="SECRET_KEY=replace-me"
Environment="DATABASE_URL=postgresql://user:pass@host/dbname"

Or use an environment file:

ini
EnvironmentFile=/etc/myflaskapp.env

9. Reload systemd and start the service

bash
sudo systemctl daemon-reload
sudo systemctl enable --now myflaskapp

10. Check service status

bash
sudo systemctl status myflaskapp --no-pager

If the service does not stay active, go to the Debugging section below.

11. Create the Nginx server block

Create /etc/nginx/sites-available/myflaskapp:

nginx
server {
    listen 80;
    server_name YOUR_DOMAIN_OR_IP;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

If you are deploying with IP only for the first test, use:

nginx
server_name _;

12. Enable the site

bash
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/myflaskapp

13. Disable the default Nginx site if needed

bash
sudo rm -f /etc/nginx/sites-enabled/default

14. Validate and reload Nginx

bash
sudo nginx -t
sudo systemctl reload nginx

15. Open firewall rules if UFW is enabled

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

If your provider has a cloud firewall, allow ports 80 and 443 there as well.

16. Test the public endpoint

bash
curl -I http://YOUR_SERVER_IP

If DNS is already configured:

bash
curl -I http://YOUR_DOMAIN

17. Add static file handling if needed

Do not serve production static files through Flask unless required. Add an Nginx location block:

nginx
location /static/ {
    alias /var/www/myflaskapp/static/;
}

For uploads or media:

nginx
location /uploads/ {
    alias /var/www/myflaskapp/uploads/;
}

If static files are not loading, see Flask Static Files Not Loading in Production.

18. Add a domain and HTTPS after HTTP works

Once the application works by IP or plain HTTP, configure your domain and DNS, then add TLS.

See:

Common Causes

  • Wrong WorkingDirectory in systemd → Gunicorn cannot import the app → point WorkingDirectory to the folder containing your code and wsgi.py.
  • Wrong WSGI module pathModuleNotFoundError or service exits immediately → use the correct target such as wsgi:app or package_name.wsgi:app.
  • Virtual environment not used → dependencies appear missing in production → set Environment="PATH=/path/to/venv/bin" and use the venv Gunicorn binary.
  • Nginx upstream mismatch502 Bad Gateway or connection refused → make proxy_pass match the Gunicorn bind address or socket.
  • Port blocked by firewall → app works locally but not externally → allow HTTP and HTTPS in UFW or provider firewall rules.
  • Permissions issue on app files → Gunicorn cannot read code or write runtime files → ensure the service user can access the application directory.
  • Missing environment variables → app fails during import or startup → define Environment= lines or EnvironmentFile= in systemd and restart.
  • Default Nginx site still active → wrong site responds or config conflicts → remove or disable the default site.

Debugging

Check these in order.

systemd service status and logs

bash
sudo systemctl status myflaskapp --no-pager -l
sudo journalctl -u myflaskapp -n 100 --no-pager
sudo journalctl -u myflaskapp -f

Look for:

  • import errors
  • missing packages
  • bad paths
  • permission denied
  • environment variable errors

Nginx configuration and logs

bash
sudo nginx -t
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/access.log

Look for:

  • upstream connection refused
  • bad proxy_pass
  • syntax errors
  • wrong server_name

Test Gunicorn directly

bash
curl -I http://127.0.0.1:8000
sudo ss -ltnp | grep 8000

If nothing is listening on 127.0.0.1:8000, Gunicorn is not running or is bound somewhere else.

Verify the app imports from the deployment directory

bash
cd /var/www/myflaskapp
source venv/bin/activate
python -c "from wsgi import app; print(app)"

If this fails, fix the Python import path before troubleshooting Nginx.

Check installed packages in the virtual environment

bash
/var/www/myflaskapp/venv/bin/pip freeze

If using a Unix socket instead of a port

bash
ls -lah /run/ | grep gunicorn

Check:

  • socket file exists
  • Nginx points to the same socket
  • socket permissions allow Nginx access

If Nginx returns a 502, see Fix Flask 502 Bad Gateway (Step-by-Step Guide).

Checklist

  • Ubuntu packages are installed and updated
  • Project files are present in the deployment directory
  • Virtual environment exists and all dependencies are installed
  • Gunicorn starts successfully with the correct WSGI target
  • systemd service is enabled and running
  • Nginx config passes sudo nginx -t
  • Nginx proxies to the correct Gunicorn port or socket
  • Firewall allows HTTP or HTTPS traffic
  • curl to localhost and the public endpoint returns the expected response
  • App environment variables are loaded in production
  • Static and media handling is configured separately if needed

For final hardening and launch checks, use Flask Production Checklist (Everything You Must Do).

FAQ

What is the minimum production stack for Flask on Ubuntu?

Gunicorn to run the app, systemd to manage the service, and Nginx to reverse proxy public traffic.

Should I use a port or a Unix socket for Gunicorn?

Either works. A local TCP port is simpler to debug first. A Unix socket is common after the app is stable.

Why am I getting a 502 after deployment?

Usually Nginx cannot reach Gunicorn because the service is down, the bind target is wrong, or permissions prevent access.

Where do I put environment variables?

In the systemd service with Environment= entries or an EnvironmentFile= referenced by the service.

Do I need HTTPS before testing deployment?

No. Validate the app over HTTP first, then add a domain and HTTPS once the stack is working.

Final Takeaway

A reliable Flask deployment on an Ubuntu VPS depends on four aligned pieces: application path, Python environment, Gunicorn service definition, and Nginx upstream configuration. If the app runs correctly under Gunicorn and systemd manages it cleanly, Nginx becomes straightforward to validate and expose publicly.