Technology

VPS Hosting for Django and Flask with Gunicorn, Uvicorn, Nginx and SSL

If you are moving a Django or Flask project from local development to production, one of the most practical steps is putting it on a VPS with a proper web stack: an application server (Gunicorn or Uvicorn), Nginx as reverse proxy, and a solid SSL/TLS setup. This combination is battle‑tested, efficient, and flexible enough for everything from small side projects to busy SaaS dashboards. On the dchost.com side, we see the same pattern over and over: teams start with the built‑in development server, then quickly realize they need something more robust, secure, and observable. In this guide, we will walk through the architecture, configuration patterns, and small but critical details that make Python apps stable in production. You will see where Gunicorn vs Uvicorn fits, how Nginx should be configured, and how to handle HTTPS and certificate automation without turning your weekends into maintenance windows.

Why VPS Hosting Fits Django and Flask Projects

Before touching configuration files, it helps to be clear why running Django and Flask on a VPS is such a common choice.

Control and flexibility

With a VPS you control:

  • Linux distribution and versions – Choose Ubuntu, Debian, AlmaLinux, etc. with the Python versions you actually need.
  • System packages – Install system libraries (e.g. Postgres client libs, image processing tools) without fighting shared hosting limitations.
  • Network and firewall – Open only the ports you want, restrict SSH, and harden the box to your security standards.
  • Background services – Run Celery, Redis, cron jobs, WebSocket servers, and other components next to your app.

Performance and predictable resources

Django and Flask are often backed by databases, queues, and caching layers. They benefit from:

  • Dedicated vCPU and RAM – Your workers are not fighting with unrelated websites running heavy plugins.
  • NVMe or SSD storage – Faster disk I/O is critical for database and log performance.
  • Stable process management – Gunicorn/Uvicorn worker counts, memory usage, and timeouts tuned exactly for your workload.

We covered how to read resource signals (CPU steal, IO wait, RAM pressure) in detail for other stacks; the same thinking applies to Python apps. If you want a deeper dive into VPS resource behaviour, our article on monitoring VPS resource usage with htop, iotop, Netdata and Prometheus is a good companion read.

Security and compliance

Many Django/Flask apps handle personal data, internal dashboards or business‑critical workflows. A VPS makes it easier to:

  • Lock down SSH and admin ports.
  • Apply OS‑level hardening (kernel params, firewall, Fail2ban).
  • Implement proper logging and retention for audits.

For a step‑by‑step checklist, see our VPS security hardening checklist with sshd_config and Fail2ban.

Planning Your VPS for Django and Flask

Choosing OS and baseline specs

Most Python teams today choose a recent LTS Linux distribution, such as Ubuntu LTS or Debian stable. Look for:

  • Long support window (5+ years) so you are not forced into frequent OS migrations.
  • Modern OpenSSL and TLS libraries for up‑to‑date HTTPS and HTTP/2/3 support.

As a rough starting point for a single small‑to‑medium Django/Flask app:

  • 1–2 vCPUs – Enough for a few Gunicorn/Uvicorn workers and occasional background jobs.
  • 2–4 GB RAM – Gives space for the app, OS, database client, and caching.
  • 20–40 GB NVMe/SSD – App code, logs, virtualenvs, and basic database usage.

Larger SaaS apps or API backends with significant traffic will often start at 4+ vCPUs and 8+ GB RAM, sometimes with separate database and Redis servers. Our article on how many vCPUs and how much RAM you really need shows how we think about sizing for web apps; the logic translates well to Python frameworks.

Basic server preparation

Once your dchost.com VPS is provisioned and you can log in via SSH, apply a simple baseline:

  1. Update packages:
    sudo apt update && sudo apt upgrade
  2. Create a non‑root user and grant sudo.
  3. Configure SSH keys, disable password login, optionally move SSH to a non‑standard port.
  4. Enable a firewall (e.g. ufw) allowing only SSH, HTTP (80) and HTTPS (443).

We walk through these first‑day steps in more detail in our guide to the first 24 hours on a new VPS.

Python environment and project layout

For Django and Flask deployments, we strongly recommend:

  • Using python3 -m venv for a project‑local virtual environment.
  • Keeping application code under /srv/<project> or /var/www/<project> with clear ownership (dedicated Unix user).
  • Separating configuration via environment variables or a .env file, never hard‑coding secrets in the repo.

If you have not yet formalised your secrets handling, check our piece on managing .env files and secrets on a VPS safely for patterns that work beyond the first deployment.

Gunicorn vs Uvicorn: WSGI vs ASGI in Real Life

Understanding WSGI and ASGI

Historically, Python web frameworks used WSGI (Web Server Gateway Interface) – a synchronous interface. Gunicorn is a popular WSGI application server, perfect for classic Django and Flask apps.

Modern frameworks introduce ASGI (Asynchronous Server Gateway Interface), which supports async views, WebSockets, and long‑lived connections. Uvicorn and Daphne are common ASGI servers. Django now supports ASGI; Flask has community ASGI wrappers and many ecosystems (e.g. FastAPI) are ASGI‑first.

When to choose Gunicorn

Gunicorn is an excellent default when:

  • You run a primarily synchronous Django or Flask app.
  • You do not heavily rely on WebSockets or background streaming in the same process.
  • You want a stable, well‑tested server with simple process management.

Basic Gunicorn command for a Django project:

cd /srv/myproject
source venv/bin/activate
gunicorn myproject.wsgi:application 
  --bind 127.0.0.1:8000 
  --workers 3 
  --timeout 30

When to choose Uvicorn (or Uvicorn workers under Gunicorn)

Uvicorn shines when:

  • You use Django’s async views, Channels, or an ASGI‑first framework.
  • You need WebSockets for live dashboards, chats, or notifications.
  • You expect many long‑lived connections with moderate CPU usage.

Uvicorn can be run directly:

uvicorn myproject.asgi:application 
  --host 127.0.0.1 --port 8000 
  --workers 3

Or you can use Gunicorn as a process manager with Uvicorn workers:

gunicorn myproject.asgi:application 
  -k uvicorn.workers.UvicornWorker 
  --bind 127.0.0.1:8000 
  --workers 3

This hybrid approach gives you Gunicorn’s familiar config style with ASGI performance.

Worker count and timeouts

A simple rule of thumb:

  • CPU‑bound workloads: ~2 x vCPUs workers.
  • IO‑bound/light workloads: sometimes more workers, but monitor RAM and response times.

For many Django/Flask APIs on a 2‑vCPU VPS, starting with 3–4 workers is reasonable. Then watch CPU, load average, and request latency under traffic and adjust.

Systemd Services: Keeping Gunicorn/Uvicorn Running

Manually running your application server from the shell is fine for tests, but in production we want a supervised service that starts on boot and restarts on failure. On most modern distributions, this means systemd.

Example systemd unit for Gunicorn

[Unit]
Description=Gunicorn for myproject
After=network.target

[Service]
User=myproject
Group=myproject
WorkingDirectory=/srv/myproject
Environment="PATH=/srv/myproject/venv/bin"
ExecStart=/srv/myproject/venv/bin/gunicorn myproject.wsgi:application 
  --bind 127.0.0.1:8000 
  --workers 3 
  --timeout 30

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Save this as /etc/systemd/system/myproject-gunicorn.service, then enable and start:

sudo systemctl daemon-reload
sudo systemctl enable myproject-gunicorn
sudo systemctl start myproject-gunicorn
sudo systemctl status myproject-gunicorn

Example systemd unit for Uvicorn

[Unit]
Description=Uvicorn for myproject (ASGI)
After=network.target

[Service]
User=myproject
Group=myproject
WorkingDirectory=/srv/myproject
Environment="PATH=/srv/myproject/venv/bin"
ExecStart=/srv/myproject/venv/bin/uvicorn myproject.asgi:application 
  --host 127.0.0.1 --port 8000 --workers 3

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Using systemd means your app comes back after reboots and crashes, and you get structured logs via journalctl.

Nginx as a Reverse Proxy in Front of Django/Flask

Gunicorn/Uvicorn are great at executing Python code, but not ideal for handling TLS, HTTP/2, slow clients, and static file caching. That is where Nginx sits in front as a reverse proxy.

Installing Nginx

sudo apt install nginx

Once installed, Nginx will likely be listening on port 80. We will create a dedicated server block for your domain, proxying to Gunicorn/Uvicorn on 127.0.0.1:8000.

Basic Nginx server block

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

    # Increase buffer sizes slightly if you have large headers/cookies
    client_max_body_size 20m;

    location /static/ {
        alias /srv/myproject/static/;
    }

    location /media/ {
        alias /srv/myproject/media/;
    }

    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;

        proxy_read_timeout 60s;
        proxy_connect_timeout 5s;
        proxy_redirect off;
    }
}

Enable the config and reload Nginx:

sudo ln -s /etc/nginx/sites-available/myproject.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If you want a broader view of reverse proxy patterns (including load balancing multiple backends), our practical guide on Nginx reverse proxy and simple load balancer setup for small projects is worth exploring.

Static and media files

In Django, you typically run python manage.py collectstatic to collect static assets into a directory like /srv/myproject/static/, which Nginx serves directly via alias. Flask projects often either use a similar pattern or let Nginx serve static files from a static folder within the project.

Serving static and media from Nginx offloads this lightweight work from your Python workers and simplifies caching and compression rules.

Enabling HTTPS: SSL/TLS and Let’s Encrypt

Running Django or Flask in production without HTTPS is no longer acceptable: browsers show warnings, APIs refuse insecure callbacks, and security baselines fail. The good news is that free certificates and automation have made proper TLS straightforward.

Let’s Encrypt and Certbot

The most common approach on a VPS is Let’s Encrypt via Certbot:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot will:

  • Verify domain ownership using an HTTP‑01 challenge.
  • Obtain a certificate and configure Nginx to use it.
  • Optionally set up automatic HTTP→HTTPS redirects.

By default, it also installs a cron/systemd timer to renew the certificate automatically.

Manual Nginx TLS configuration

If you prefer manual control, your HTTPS server block will look like:

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # Optionally add HSTS (after testing!)
    # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    client_max_body_size 20m;

    location /static/ {
        alias /srv/myproject/static/;
    }

    location /media/ {
        alias /srv/myproject/media/;
    }

    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;
    }
}

You can then keep a lightweight HTTP server block that only redirects to HTTPS:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

We explain the SEO and security side of full HTTPS migrations (including HSTS and canonical URLs) in our full HTTP to HTTPS migration guide with HSTS and canonical settings.

Certificate automation and renewals

Certificates have short lifetimes (often 90 days), so automation matters. Certbot timers usually handle renewals automatically, but you should still:

  • Test renewal with sudo certbot renew --dry-run.
  • Monitor certificate expiry with external checks or a simple cron script.
  • Reload Nginx after successful renewals (Certbot typically does this for you).

For more complex setups (wildcards, DNS‑01 challenges, multi‑tenant SaaS), we recommend our piece on SSL certificate automation tools and ACME strategies, which shows how to scale auto‑SSL beyond a single site.

Security and Observability Basics for Python Apps on a VPS

Firewall and surface reduction

Even a small Django or Flask app benefits from basic network hardening:

  • Allow only SSH, HTTP, and HTTPS on the public interface.
  • Bind Gunicorn/Uvicorn to 127.0.0.1 only, never to a public IP.
  • Use Fail2ban to block repeated failed SSH and, optionally, admin login attempts.

HTTP security headers

Django and Flask both support setting HTTP security headers from the app layer or via Nginx. Typical ones include:

  • Strict-Transport-Security (HSTS)
  • Content-Security-Policy (CSP)
  • X-Frame-Options
  • X-Content-Type-Options

You can configure many of them directly in Nginx with add_header directives. If you want a structured walk‑through with examples for different setups, see our HTTP security headers guide for shared hosting and VPS.

Logs and monitoring

At minimum, keep an eye on:

  • Nginx access and error logs – Status codes, response times, spikes in 4xx/5xx.
  • Gunicorn/Uvicorn logs – Worker timeouts, crashes, import errors.
  • System metrics – CPU, RAM, disk space, I/O, and network.

For teams starting to professionalise their operations, our tutorial on VPS monitoring and alerts with Prometheus, Grafana and Uptime Kuma shows how to build a practical, alert‑driven observability stack without over‑engineering.

End-to-End Example: Deploying Django on a dchost.com VPS

Let’s combine everything into a concrete high‑level runbook for a typical Django app.

1. Prepare the server

  • Provision a VPS from dchost.com in the region closest to your main users.
  • Harden SSH, create a non‑root user, and enable a firewall (SSH, HTTP, HTTPS only).
  • Install base packages: python3-venv, python3-pip, git, nginx, database client tools, and any OS libraries your project needs.

2. Deploy the application

  • Create /srv/myproject and a dedicated myproject Unix user.
  • Clone your repository into /srv/myproject.
  • Create a virtualenv: python3 -m venv /srv/myproject/venv.
  • Install dependencies: pip install -r requirements.txt.
  • Configure environment variables for DJANGO_SETTINGS_MODULE, database URL, secret key, etc.
  • Run migrations (manage.py migrate) and collectstatic.

3. Configure Gunicorn (WSGI)

  • Test Gunicorn manually binding to 127.0.0.1:8000.
  • Add a systemd service unit for myproject-gunicorn as shown above.
  • Enable and start the service, confirm it stays up across reboots.

4. Configure Nginx

  • Create an Nginx server block for your domain, with proxy_pass pointing to http://127.0.0.1:8000.
  • Map /static/ and /media/ to the appropriate directories using alias.
  • Enable the site, test Nginx config, and reload.

5. Enable HTTPS

  • Point your domain’s A/AAAA records to the VPS IP from your domain registrar or dchost.com domain panel.
  • Run Certbot with the Nginx plugin to obtain and install a Let’s Encrypt certificate.
  • Verify that HTTP redirects to HTTPS and that the certificate chain is valid in browsers.

6. Final hardening and monitoring

  • Set up Fail2ban and tighten firewall rules if needed.
  • Add basic HTTP security headers via Nginx or Django middleware.
  • Configure resource and uptime monitoring (e.g. Prometheus/Node Exporter and Uptime Kuma).
  • Test backup and restore processes for your database and uploaded files.

This workflow is almost identical for Flask, except for framework‑specific details (e.g. Flask’s application object, configuration patterns). For async‑heavy workloads or WebSockets, you swap Gunicorn’s WSGI entrypoint for Uvicorn/ASGI while keeping Nginx and TLS architecture the same.

Summary: A Solid Production Foundation for Django and Flask on dchost.com

Deploying Django and Flask on a VPS does not need to be dramatic. Once you understand the roles of each component, the stack is surprisingly simple: Nginx terminates TLS and handles clients; Gunicorn or Uvicorn runs your application; systemd keeps everything alive; Let’s Encrypt (or other ACME automation) keeps certificates fresh. From the dchost.com side, this is the pattern we see most often in successful Python projects—whether they start as internal tools, customer portals, or full‑blown SaaS products.

If you are planning your next deployment, start by picking a dchost.com VPS size that matches your expected traffic, then follow the sequence above: secure the server, set up Python and your app server, wire Nginx in front, and finish with HTTPS, security headers, backups, and monitoring. As your traffic grows, you can layer on load balancing, separate database servers, or containers, but the fundamentals you built here will stay the same. With a clean Gunicorn/Uvicorn + Nginx + SSL foundation, your Django and Flask apps have room to grow without forcing you into a complete re‑architecture on day one.

Frequently Asked Questions

Technically you can expose a Django or Flask app using the built-in development server, but it is not designed for production: it lacks robust process management, does not handle slow clients well, and offers no proper TLS support. Using Gunicorn or Uvicorn behind Nginx gives you a clean separation of concerns: Python workers focus on executing application logic, while Nginx handles TLS, HTTP/2, buffering, redirects, and static files. Even for a small internal project, this stack improves stability, security, and the ability to grow later without re-architecting everything.

Choose Uvicorn (as a standalone ASGI server or as Gunicorn workers) when you rely on async features or real-time connections. Examples include WebSocket-based chats, live dashboards, streaming responses, or Django Channels. Uvicorn is built for ASGI and excels at handling many concurrent, long-lived connections efficiently. For classic request/response websites and APIs with primarily synchronous views, Gunicorn as a WSGI server remains a safe and simple default. You can also combine them by using Gunicorn with UvicornWorker to get ASGI support plus a familiar process model.

For a small production Django or Flask app serving a few thousand requests per day, a VPS with 1–2 vCPUs and 2–4 GB RAM is usually a good starting point. This comfortably runs several Gunicorn or Uvicorn workers, Nginx, and basic system services. If you add heavier components such as Celery workers, in-process caching, or run a database on the same server, you may want 4+ GB RAM and more vCPUs. The key is to monitor CPU load, memory usage, response times, and error rates under real traffic, then scale vCPUs and RAM as your needs grow.

Yes. You can run multiple projects on a single VPS by creating separate Unix users, virtual environments, and Gunicorn/Uvicorn systemd services for each app, then defining individual Nginx server blocks for each domain or subdomain. Each Nginx server block proxies to a different internal port (for example 127.0.0.1:8001, 8002, and so on). This keeps configurations isolated and makes it easy to restart or deploy one project without affecting others. Just keep an eye on cumulative CPU, RAM, and disk usage so that one app cannot starve the others.

For Django and Flask on a VPS, think in three layers: code, data, and configuration. Code is usually in Git and can be redeployed; the critical backups are your database (PostgreSQL/MySQL), uploaded media files, and configuration such as .env files and Nginx/systemd configs. Use regular database dumps or snapshot-based backups, sync your media directories to off-site storage, and periodically archive your configuration files. Automate this with cron or a backup tool, and test restores on a staging VPS to confirm that you can rebuild the entire stack if you ever lose the main server.