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.
İçindekiler
- 1 Why VPS Hosting Fits Django and Flask Projects
- 2 Planning Your VPS for Django and Flask
- 3 Gunicorn vs Uvicorn: WSGI vs ASGI in Real Life
- 4 Systemd Services: Keeping Gunicorn/Uvicorn Running
- 5 Nginx as a Reverse Proxy in Front of Django/Flask
- 6 Enabling HTTPS: SSL/TLS and Let’s Encrypt
- 7 Security and Observability Basics for Python Apps on a VPS
- 8 End-to-End Example: Deploying Django on a dchost.com VPS
- 9 Summary: A Solid Production Foundation for Django and Flask on dchost.com
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:
- Update packages:
sudo apt update && sudo apt upgrade - Create a non‑root user and grant sudo.
- Configure SSH keys, disable password login, optionally move SSH to a non‑standard port.
- 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 venvfor 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
.envfile, 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 vCPUsworkers. - 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.1only, 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-OptionsX-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/myprojectand a dedicatedmyprojectUnix 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-gunicornas 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_passpointing tohttp://127.0.0.1:8000. - Map
/static/and/media/to the appropriate directories usingalias. - 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.
