Flask VPS Hosting Production Setup: Gunicorn, Nginx and systemd
Flask's built-in server is for development. How to run it properly on a VPS.
Flask's built-in development server is not meant for production, and the Flask documentation says so. In production, run the app under a WSGI server such as Gunicorn, keep it alive with systemd, and put Nginx in front for TLS, static files and slow clients. This guide sets that up on a KVM VPS.
Prerequisites
- A KVM VPS with root access. VPS 4 (2 vCPU, 4 GB, ₹596) suits a small API. VPS 8 (4 vCPU, 8 GB, ₹1,438) is a common production start.
- A supported Python 3 version and your app in a Git repository.
Step 1: app user and virtualenv
adduser --system --group app
sudo -u app python3 -m venv /srv/app/venv
sudo -u app /srv/app/venv/bin/pip install -r /srv/app/requirements.txt gunicornStep 2: Gunicorn
Gunicorn runs several worker processes, each handling requests independently. A common starting point for synchronous workers is two to four per CPU core. Test with your own traffic, because memory per worker often sets the real limit.
/srv/app/venv/bin/gunicorn --workers 4 --bind 127.0.0.1:8000 wsgi:appStep 3: systemd service
[Unit]
Description=Flask app
After=network.target
[Service]
User=app
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/srv/app/venv/bin/gunicorn --workers 4 --bind 127.0.0.1:8000 wsgi:app
Restart=on-failure
[Install]
WantedBy=multi-user.targetEnable it with systemctl enable --now app. Keep secrets in the environment file with permissions restricted to the app user.
Step 4: Nginx in front
location /static/ { alias /srv/app/static/; }
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Tell Flask it is behind a proxy with Werkzeug's ProxyFix middleware so generated URLs use the correct scheme and host.
Step 5: verify
- Confirm port 8000 is not reachable from outside.
- Kill a Gunicorn worker and confirm the master replaces it.
- Reboot and confirm the service starts on its own.
- Confirm debug mode is off.
Sizing
| Plan | vCPU | Memory | RAID NVMe | Transfer | Monthly |
|---|---|---|---|---|---|
| VPS 2 | 1 | 2 GB | 20 GB | 100 GB | ₹298 |
| VPS 4 | 2 | 4 GB | 40 GB | 200 GB | ₹596 |
| VPS 8 | 4 | 8 GB | 80 GB | 400 GB | ₹1,438 |
| VPS 16 | 6 | 16 GB | 160 GB | 800 GB | ₹2,399 |
| VPS 32 | 12 | 32 GB | 320 GB | 1,600 GB | ₹4,799 |
Monthly INR, excluding GST. All ten sizes, from 1 GB (₹149) to 128 GB (₹19,099), are on the VPSWala cloud VPS plans page. Every plan is KVM with RAID NVMe storage and full root access.
Responsibilities
VPSWala plans are unmanaged unless you agree otherwise in writing: the host platform, network and provisioning are VPSWala's job, and everything inside the VM is yours. Per the terms of service, off-server backups, control-panel licences, Windows licensing, managed administration and extra IPv4 addresses are quoted separately, and any availability commitment applies only where it is written into your quotation or agreement.
Logging and monitoring
Send Gunicorn access and error logs to journald through systemd, and read them with journalctl -u app. Add a lightweight health endpoint that checks the database connection, and point an external uptime check at it. Watch memory per worker over a few days: steady growth usually means a leak in a library or a cache that never expires.
Security checklist
- Set a strong
SECRET_KEYfrom the environment and never commit it. - Turn on secure, HTTP-only session cookies once TLS is in place.
- Keep the virtualenv and system packages patched.
- Firewall everything except SSH, HTTP and HTTPS, and bind Gunicorn to localhost only.
Configuration by environment
Load configuration from environment variables rather than editing code between environments. Keep a .env.example in the repository that lists every variable with a harmless placeholder, and let the real file live only on the server. This makes a rebuild of the VPS, or a move to a larger plan, a matter of copying one file and running the same deploy steps.
Pin every dependency version in requirements.txt, rebuild the virtualenv from it on deploy, and keep the previous release directory until the new one has run cleanly for a day, so rolling back is a symlink change rather than a rebuild under pressure.
Frequently asked questions
Gunicorn or uWSGI?
Both work. Gunicorn is simpler to configure and is the option the Flask deployment docs list first.
How do I run background jobs?
Use a task queue such as Celery or RQ with Redis, run as its own systemd service, rather than threads inside the web workers.
Async workers?
Use async workers only if your code and libraries are written for them. For typical database-backed apps, sync workers are the safer default.
How do I deploy without downtime?
Send Gunicorn a HUP signal to reload workers gracefully after you update the code and dependencies.
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
Deploying Next.js SSR on a Linux VPS: Standalone Build, PM2 & Nginx Proxy
Learn how to deploy a server-rendered Next.js application on a Linux KVM VPS using standalone build artifacts, PM2 cluster management, and Nginx.
VPS Firewall Setup with UFW and iptables: Port Hardening & Lockout Prevention
A hands-on sysadmin guide to securing a Linux cloud VPS with UFW and iptables, implementing strict ingress filtering while preventing accidental connection loss.
VPS Hosting for Bhopal: Which VPSWala Node to Pick and How to Test It
A practical routing and workload sizing guide for developers and businesses in Bhopal to evaluate VPSWala cloud nodes and verify network path stability.