TutorialsAugust 4, 20264 min read

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.

NBy Nxeon

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.

The Nxeon game-server control panel — live console, player slots, and TPS
The Nxeon game-server control panel — live console, player slots, and TPS

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.

#flask#python#gunicorn#nginx#deployment#seobatch

Deploy your first server in under a minute

Creating an account is free and takes no card details. You pay when you deploy — choose a billing term and pay from your wallet or by card at checkout.