KVM VPS from Rs 149/mo. Mumbai, Noida and Jaipur nodes.

24×7 infrastructure operations Sales +91 98297 14343
vpswala.in
VPSWala 3 min read

Flask on a VPS: Gunicorn Workers, Nginx Reverse Proxy & systemd Service

From flask run to production: a Gunicorn command with sensible workers, an Nginx proxy, a systemd unit and a short list of errors to check first.

To run Flask in production on a VPS, serve it with Gunicorn, keep Gunicorn on a local port, put Nginx in front, and let systemd start it at boot. Never use the Flask development server for real traffic, and never run Gunicorn as root.

Prerequisites

  • A Linux VPSWala KVM VPS with root access, from which you create a non-root user for the app.
  • Python 3 with venv, Nginx installed, and your project in a Git repository.
  • A domain pointing to the server's IP address.

Step 1: Install the app and Gunicorn in a virtualenv

Flask's own deployment guide describes Gunicorn as a pure Python WSGI server with simple configuration and multiple worker types. It does not support Windows natively, which is no issue on a Linux VPS. Install it into the same virtual environment as your application:

sudo adduser --disabled-password flaskapp
sudo -iu flaskapp
cd ~ && git clone https://example.com/you/hello-app.git
cd hello-app
python3 -m venv .venv
. .venv/bin/activate
pip install .
pip install gunicorn

Step 2: Run Gunicorn and choose a worker count

The only required argument tells Gunicorn how to load the app, in the form module:app_variable. For the application factory pattern, call the function:

gunicorn -w 4 'hello:app'
gunicorn -w 4 'hello:create_app()'

Flask's guide points out that the default is a single worker, which is probably not what you want, and suggests that a starting value could be CPU count times two. Treat that as a starting point and adjust after watching CPU and memory under load: each worker is a full copy of your application, so memory is usually the limit on a small VPS. Request logs are not printed by default, only worker info and errors; add --access-logfile=- to see them. The default worker type is the sync worker, which the guide calls appropriate for most use cases. For numerous long-running concurrent connections it offers a gevent worker, started with -k gevent.

Step 3: Bind locally and use Nginx as the public face

The guide says Gunicorn should not run as root, because that would make your application code run as root, which means it also cannot bind to ports 80 or 443. The answer is a reverse proxy such as Nginx. It also warns not to bind to 0.0.0.0 in a proxy setup, because that makes it possible to bypass the proxy. Bind to localhost:

gunicorn -w 4 -b 127.0.0.1:8000 'hello:create_app()'

Flask's Nginx page gives a minimal server block using proxy_pass and the forwarded headers:

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000/;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Prefix /;
    }
}

The same page tells you to configure Flask to trust those headers, using its proxy-fix setup, so that generated URLs and client addresses are correct:

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)

Step 4: Keep it running with systemd

[Unit]
Description=Flask app (Gunicorn)
After=network.target

[Service]
User=flaskapp
WorkingDirectory=/home/flaskapp/hello-app
Environment="PATH=/home/flaskapp/hello-app/.venv/bin"
ExecStart=/home/flaskapp/hello-app/.venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 'hello:create_app()'
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now flaskapp
sudo nginx -t && sudo systemctl reload nginx

Deploying updates

A repeatable update routine prevents most production surprises. Pull the new code as the application user, install dependencies into the virtualenv, run your tests or at least an import check, then restart the service and watch the journal for errors. Keep the previous release available so you can switch back quickly, and add a lightweight health endpoint to the app that returns a plain 200 so you can test it with curl after each restart. If the application writes uploads or a local database file, keep them outside the code directory so a deploy cannot overwrite them.

cd ~/hello-app && git pull
. .venv/bin/activate && pip install .
sudo systemctl restart flaskapp
curl -fsS http://127.0.0.1:8000/health

Verify

systemctl status flaskapp
curl -I http://127.0.0.1:8000/
curl -I http://app.example.com/
journalctl -u flaskapp -n 50 --no-pager

Reboot once and confirm everything returns without you touching it. Then add TLS with an ACME client of your choice.

Common errors

  • 502 Bad Gateway: Gunicorn is not running or is on a different port. Check systemctl status and the journal.
  • ModuleNotFoundError under systemd: the unit is not using the virtualenv's Python or Gunicorn path.
  • Wrong URLs or HTTP links behind HTTPS: ProxyFix is missing or not told how many proxies to trust.
  • Memory exhaustion: too many workers for the RAM; lower -w or choose a larger plan.
  • Database connections exhausted: each worker opens its own pool; size pools to workers.

All VPSWala plans include root access and are unmanaged unless agreed in writing, so updates, firewall rules and backups are your responsibility. If you need faster per-core speed for CPU-heavy views, look at the 9950X VPS plans.


Sources

Not sure which size?

Send the stack, get a size.

Tell us the operating system, application stack, current traffic, database size and where it hurts today. You get a sizing recommendation, the matching plan and a price.

Related

More on this.

Get sizing help See VPS plans