Running python manage.py runserver is fine for local hacking, but taking Django to production is where things usually get messy. Moving to a fresh Linux box means wrestling with file permissions, missing static assets, Gunicorn socket failures, and CSRF origin rejections behind proxies.
I spun up a production Django 5 stack on an Ubuntu 24.04 Hostinger VPS to document the entire pipeline. Here is how to wire up Gunicorn managed by systemd, drop Nginx in front for SSL and static file offloading, route your Namecheap DNS, and layer on a CDN without breaking POST requests.

1. Server Baseline: SSH and User Hardening
Grab your server’s public IP from your Hostinger dashboard. First rule: don’t deploy or run your application as root.
SSH into your VPS as root to get things provisioned:
ssh root@195.179.228.42Update existing packages, create a dedicated deploy user with sudo privileges, and lock down UFW so you don’t accidentally lock yourself out:
apt update && apt upgrade -y
apt install -y python3-pip python3-venv python3-dev libpq-dev nginx curl git ufw adduser deploy
usermod -aG sudo deploy # Configure UFW
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enableDrop into the deploy shell before touching any code or virtual environments:
su - deploy2. Clone the Django Project and Configure the Virtual Environment
I keep my web apps under /var/www/ because it makes permission boundaries between systemd, Gunicorn, and Nginx dead simple to reason about. Create the directory and hand ownership over to deploy:
sudo mkdir -p /var/www/myproject
sudo chown -R deploy:deploy /var/www/myproject
cd /var/www/myprojectClone your repo (or scaffold a fresh project) inside that directory, then spin up a dedicated virtualenv:
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install django gunicorn psycopg2-binary python-dotenvHere is the project layout you want to see inside /var/www/myproject:
/var/www/myproject/
├── manage.py
├── myproject/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── static/
├── media/
└── venv/Create your .env file in the project root so secrets, database credentials, and production flags stay out of Git:
DEBUG=False
SECRET_KEY=replace-with-a-random-50-character-string
ALLOWED_HOSTS=example.com,www.example.com,195.179.228.42
CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.comMake sure your myproject/settings.py reads these environment variables properly and specifies exact paths for static files:
import os
from pathlib import Path
from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(os.path.join(BASE_DIR, '.env')) SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False') == 'True' ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', '').split(',') STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')Apply your migrations and collect static files into the directory Nginx will serve directly:
python manage.py migrate
python manage.py collectstatic --noinput3. Configure Gunicorn Systemd Service and Socket
Never run Gunicorn in a screen session or tmux window in production. We want it managed by a systemd service listening over a private UNIX socket. If the process crashes or the server reboots, systemd brings it right back.
Create the systemd socket unit first:
sudo nano /etc/systemd/system/gunicorn.socketPaste this socket configuration:
[Unit]
Description=gunicorn socket [Socket]
ListenStream=/run/gunicorn.sock [Install]
WantedBy=sockets.targetNext, create the accompanying service definition that tells systemd how to run Gunicorn:
sudo nano /etc/systemd/system/gunicorn.serviceAdd the service configuration below. For worker count, the general baseline is (2 * CPU cores) + 1:
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target [Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application [Install]
WantedBy=multi-user.targetEnable and start the socket:
sudo systemctl daemon-reload
sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socketTest that the socket is actually responding to requests:
sudo systemctl status gunicorn.socket
curl --unix-socket /run/gunicorn.sock httpIf you get HTML or a valid Django HTTP response back in your terminal, your Gunicorn socket is healthy. Just like in our setup for FastAPI deployments on Hostinger, systemd handles process restarts and keeps things clean.
4. Set Up Nginx as a Reverse Proxy
Nginx sits in front of Gunicorn. It takes incoming HTTP traffic, serves CSS, JS, and user uploads straight from disk without hitting Python, and passes dynamic routes over the UNIX socket.
Create a fresh virtual host configuration:
sudo nano /etc/nginx/sites-available/myprojectAdd the server block:
server { listen 80; server_name example.com www.example.com; client_max_body_size 20M; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { alias /var/www/myproject/staticfiles/; expires 30d; add_header Cache-Control "public, no-transform"; } location /media/ { alias /var/www/myproject/media/; expires 7d; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $http_host; proxy_redirect off; }
}Symlink the config to sites-enabled, run a config test, and reload Nginx:
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginxCheck the official Nginx proxy module documentation if you need to pass extra custom headers or tweak upstream timeout thresholds.
5. Point Namecheap DNS to Your Hostinger VPS
Before Certbot can issue an SSL certificate, your domain’s DNS records must resolve to your VPS IP.
- Log in to your Namecheap dashboard.
- Open your Domain List and hit Manage next to your domain.
- Navigate to the Advanced DNS tab.
- Under Host Records, create these two entries:
- Type:
A Record| Host:@| Value:195.179.228.42(Your VPS IP) | TTL:Automaticor5 min - Type:
CNAME Record| Host:www| Value:example.com.| TTL:Automatic
Verify that your DNS records have propagated using dig or nslookup:
dig +short example.comOnce you see your VPS IP address in the response, you’re ready for SSL.
6. Secure the Domain with Let’s Encrypt SSL
Install Certbot and the Nginx plugin to provision a free Let’s Encrypt certificate:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comCertbot will ask for an email address and whether you want automatic HTTPS redirects. Say yes to the redirect. If the ACME validation fails, check our troubleshooting walkthrough on how to fix Certbot ACME HTTP-01 challenge errors on Nginx.
Double-check that the auto-renewal timer is active:
sudo systemctl status certbot.timer7. Add a CDN and Fine-Tune Cache Headers
Putting an edge CDN like StackPath in front of your Hostinger VPS cuts down latency for global visitors and keeps static asset requests off your server entirely. The catch is making sure your cache headers and origin handshakes are set up right.
Update your Nginx config to set immutable cache headers on hashed static assets while keeping HTML uncached:
location /static/ { alias /var/www/myproject/staticfiles/; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; access_log off;
}If you use hashed filenames with Django’s ManifestStaticFilesStorage, read our deep-dive on immutable cache-control and cache busting in Nginx to avoid serving stale assets after running new deployments.
If your CDN throws origin connection errors during setup, follow our steps to debug 502 Bad Gateway and SSL handshake errors between CDNs and origin servers.
8. Gotcha: Fixing CSRF and DisallowedHost Behind Proxies
The most common surprise when putting Django behind Nginx and a CDN is running into DisallowedHost errors or 403 Forbidden: CSRF verification failed on form submissions.
This happens because Nginx terminates SSL and forwards requests over plain HTTP. Django needs to know it should trust the X-Forwarded-Proto header from your proxy, otherwise it assumes requests are insecure.
Add these proxy settings to your myproject/settings.py, as recommended in the official Django deployment documentation:
# Tell Django to trust the X-Forwarded-Proto header from Nginx
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # Ensure cookies are only sent over HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True # Ensure CSRF trusted origins include the protocol
CSRF_TRUSTED_ORIGINS = [ 'https://example.com', 'https://www.example.com',
]Restart Gunicorn to apply the settings:
sudo systemctl restart gunicornFrequently Asked Questions
How do I deploy code updates without downtime?
Pull down the latest code as the deploy user, run your migrations, collect static files, and tell systemd to restart Gunicorn:
cd /var/www/myproject
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicornWhy is Nginx returning a 502 Bad Gateway?
A 502 almost always means Nginx can’t connect to the Gunicorn socket. Check the socket permissions (it needs to be owned by deploy:www-data with 660) and read the daemon logs with sudo journalctl -u gunicorn.service -e.
Do I need WhiteNoise if I use Nginx?
No. WhiteNoise is great for platforms like Heroku or Fly.io where you don’t control the web server layer. Since Nginx runs on your VPS, let it serve static files directly in C—it’s significantly faster and frees up Python worker processes.
How do I automate Gunicorn restarts when code changes?
You can bundle the update commands into a bash script or trigger them through a GitHub Actions workflow that connects to your Hostinger VPS over SSH on every push to main.
Next Steps
Your Django app is now running on a hardened stack with Gunicorn, Nginx, Let’s Encrypt, and Namecheap DNS. If you need background task processing with Celery and Redis, see our guide on how to manage background daemons and reverse proxies on Hostinger VPS.

