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:
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
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3 python3-venv python3-pip nginx git
2. Create the application directory
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
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
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:
from yourapp import app
If your Flask app object is in another module or package, adjust it accordingly. Examples:
from app import app
or:
from project import create_app
app = create_app()
6. Test Gunicorn directly before configuring systemd
/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:
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:
[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:
Environment="FLASK_ENV=production"
Environment="SECRET_KEY=replace-me"
Environment="DATABASE_URL=postgresql://user:pass@host/dbname"
Or use an environment file:
EnvironmentFile=/etc/myflaskapp.env
9. Reload systemd and start the service
sudo systemctl daemon-reload
sudo systemctl enable --now myflaskapp
10. Check service status
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:
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:
server_name _;
12. Enable the site
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/myflaskapp
13. Disable the default Nginx site if needed
sudo rm -f /etc/nginx/sites-enabled/default
14. Validate and reload Nginx
sudo nginx -t
sudo systemctl reload nginx
15. Open firewall rules if UFW is enabled
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
curl -I http://YOUR_SERVER_IP
If DNS is already configured:
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:
location /static/ {
alias /var/www/myflaskapp/static/;
}
For uploads or media:
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
WorkingDirectoryin systemd → Gunicorn cannot import the app → pointWorkingDirectoryto the folder containing your code andwsgi.py. - Wrong WSGI module path →
ModuleNotFoundErroror service exits immediately → use the correct target such aswsgi:apporpackage_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 mismatch →
502 Bad Gatewayor connection refused → makeproxy_passmatch 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 orEnvironmentFile=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
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
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
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
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
/var/www/myflaskapp/venv/bin/pip freeze
If using a Unix socket instead of a port
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
-
curlto 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).
Related Guides
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask Production Checklist (Everything You Must Do)
- Flask Domain and DNS Setup for Production
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.