How to Deploy a Flask App with Gunicorn and Nginx
Take a Flask app to production on Ubuntu 24.04: a virtualenv, Gunicorn as the WSGI server, a systemd service, and an Nginx reverse proxy.

Flask's built-in server is for development only — it warns you every time you start it. For production you run Flask behind a WSGI server (Gunicorn) managed by systemd, with Nginx handling the public HTTP and TLS. This guide wires all of that together on Ubuntu 24.04.
The production shape
Requests flow: browser → Nginx (ports 80/443, TLS) → Gunicorn (local socket) → your Flask app. Gunicorn runs multiple worker processes; systemd keeps Gunicorn alive across crashes and reboots.
Set up the project and virtualenv
Install Python's venv and pip, then create an isolated environment:
sudo apt update
sudo apt install python3-venv python3-pip -y
cd ~/myflaskapp
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn
A minimal app.py for testing:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Flask is live on Nxeon"
Test with Gunicorn
Gunicorn needs the module and the app object — here app:app means "the app object in app.py":
gunicorn --workers 3 --bind 127.0.0.1:8000 app:app
In another session, curl http://127.0.0.1:8000 should return the message. Stop it with Ctrl+C. A good worker count is roughly 2 × CPU cores + 1.

Create a systemd service
Running Gunicorn by hand stops when you log out. A systemd unit keeps it running. Create /etc/systemd/system/myflaskapp.service:
[Unit]
Description=Gunicorn instance for myflaskapp
After=network.target
[Service]
User=youruser
Group=www-data
WorkingDirectory=/home/youruser/myflaskapp
Environment="PATH=/home/youruser/myflaskapp/venv/bin"
ExecStart=/home/youruser/myflaskapp/venv/bin/gunicorn --workers 3 --bind unix:myflaskapp.sock -m 007 app:app
[Install]
WantedBy=multi-user.target
This binds Gunicorn to a Unix socket (myflaskapp.sock) rather than a TCP port, which is slightly faster and cleaner for local-only traffic. Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now myflaskapp
sudo systemctl status myflaskapp
Our dedicated guide to creating a systemd service for your app explains each directive in depth.
Configure Nginx
Create /etc/nginx/sites-available/myflaskapp to proxy to the socket:
server {
listen 80;
server_name flask.example.com;
location / {
include proxy_params;
proxy_pass http://unix:/home/youruser/myflaskapp/myflaskapp.sock;
}
}
The proxy_params snippet ships with Ubuntu's Nginx and sets the forwarded headers for you. Enable and reload:
sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Make sure your user's home directory is traversable by Nginx (chmod 755 /home/youruser) so it can reach the socket. For the general pattern, see our Nginx reverse proxy guide.
Add HTTPS
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d flask.example.com
Details in the Certbot guide.
Deploying updates
cd ~/myflaskapp && git pull
source venv/bin/activate && pip install -r requirements.txt
sudo systemctl restart myflaskapp
FAQ
Why do I need Gunicorn — can't Flask serve itself?
Flask's development server is single-threaded, insecure for public use, and explicitly not for production. Gunicorn is a real WSGI server with multiple workers, proper signal handling, and robustness under load.
Should Gunicorn bind to a socket or a port?
A Unix socket is marginally faster and avoids exposing a TCP port when Nginx and Gunicorn share a host. A TCP port (127.0.0.1:8000) is easier to debug and required if they are on different machines. Either works.
How many Gunicorn workers should I run?
Start with 2 × CPU cores + 1. For I/O-bound apps, async workers (gevent, --worker-class gevent) can handle far more concurrency. Measure before over-provisioning.
The socket permission is denied — what is wrong?
Nginx (running as www-data) cannot reach the socket. Make sure the service's Group=www-data and the -m 007 umask are set, and that every directory in the path to the socket is traversable.
Nxeon VPS hosting for developers gives you full root and NVMe speed to run Python apps in production the proper way — with free migration help to move an existing Flask app over. Compare with deploying Django or FastAPI.
Manage configuration and secrets
Never hard-code secrets in a Flask app. Load them from the environment, which your systemd unit can supply via an EnvironmentFile:
import os
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///app.db")
Keep a .env file on the server, locked down with chmod 600, and reference it from the service unit. This keeps credentials out of your repository and lets the same code run in staging and production with different values.
Add logging you can actually read
Gunicorn can write access and error logs; point them somewhere durable in the systemd ExecStart:
--access-logfile /home/youruser/logs/access.log \
--error-logfile /home/youruser/logs/error.log
Or let them flow to journald and read with journalctl -u myflaskapp -f. Within the app, configure Python's logging module rather than using print — real log levels and timestamps make production debugging far easier when something breaks at 3am.