VPS & deployment
Django on a Hostinger VPS with Gunicorn, Nginx and PostgreSQL
Deploy Django on a Hostinger VPS: a virtualenv, PostgreSQL, Gunicorn under systemd, Nginx for static files, the proxy settings Django needs, and free HTTPS.
By Mr. Ahmed · 20+ years in tech
Published · 10 min read
The short answer
- 01To deploy Django on a Hostinger VPS, run Gunicorn under systemd on a Unix socket, let Nginx serve static and media files and proxy the rest, and keep PostgreSQL on the same server.
- 02Behind an HTTPS proxy, set
SECURE_PROXY_SSL_HEADER,CSRF_TRUSTED_ORIGINSand secure cookies, or Django treats every request as plain HTTP and CSRF checks fail. - 03A common Gunicorn starting point is two workers per CPU core plus one: 3 on KVM 1, 5 on KVM 2.
- 04
python manage.py check --deploylists the production security settings you have missed; run it before going live. - 05Django will not run properly on Hostinger shared hosting, because those plans cannot keep a Python process like Gunicorn running.
How the pieces fit
A request hits Nginx first. Static and media files are served straight from disk; everything else goes over a Unix socket to Gunicorn, which runs your Django app. systemd keeps Gunicorn alive. PostgreSQL stores the data on the same machine.
I am assuming Ubuntu 24.04 with a secured sudo user (checklist here) and an A record already pointing at the server (DNS guide). A KVM 1 is enough for a typical app.
Step 1: system packages and PostgreSQL
Ubuntu 24.04's Python 3.12 is supported by current Django releases.
sudo apt update
sudo apt install -y python3-venv python3-dev build-essential libpq-dev \
postgresql nginx gitsudo -u postgres psql <<'SQL'
CREATE USER myproject WITH PASSWORD 'use-a-long-random-password';
CREATE DATABASE myproject OWNER myproject;
SQLStep 2: the code and a virtualenv
Keep the virtualenv inside the project as .venv. Gunicorn and the PostgreSQL driver go in alongside your requirements.
sudo mkdir -p /var/www && sudo chown $USER:$USER /var/www
cd /var/www
git clone https://github.com/you/myproject.git
cd myproject
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt gunicorn "psycopg[binary]"Step 3: production settings
Secrets come from the environment, never from settings.py in Git. The proxy settings are not optional: without them Django believes every request arrived over plain HTTP, and both secure cookies and CSRF validation break.
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "myproject",
"USER": "myproject",
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": "127.0.0.1",
"PORT": "5432",
}
}
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
# Behind Nginx terminating HTTPS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = TrueDJANGO_SECRET_KEY=generate-a-long-random-string
DB_PASSWORD=use-a-long-random-passwordset -a; source .env; set +a
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser
python manage.py check --deployStep 4: Gunicorn under systemd
A common starting point is two workers per CPU core plus one: 3 on KVM 1, 5 on KVM 2. RuntimeDirectory tells systemd to create /run/gunicorn for the socket on every start, so it survives reboots. Swap deploy for your username.
[Unit]
Description=Gunicorn for myproject
After=network.target postgresql.service
[Service]
User=deploy
Group=www-data
WorkingDirectory=/var/www/myproject
EnvironmentFile=/var/www/myproject/.env
RuntimeDirectory=gunicorn
ExecStart=/var/www/myproject/.venv/bin/gunicorn \
--workers 3 \
--bind unix:/run/gunicorn/myproject.sock \
--access-logfile - \
myproject.wsgi:application
Restart=always
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now gunicorn
sudo systemctl status gunicorn
journalctl -u gunicorn -f # live logsStep 5: Nginx and HTTPS
Static and media requests never reach Python. Nginx serves them directly and proxies the rest to the socket.
server {
listen 80;
server_name example.com www.example.com;
client_max_body_size 20M;
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 30d;
}
location /media/ {
alias /var/www/myproject/media/;
}
location / {
proxy_pass http://unix:/run/gunicorn/myproject.sock;
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;
}
}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 reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comVerify it worked, and troubleshooting
Talk to Gunicorn's socket directly first. If that answers, any remaining problem is in Nginx; if not, it is in Django or systemd.
- Bad Request (400): the hostname is missing from
ALLOWED_HOSTS. Add every host you serve, including www, and restart Gunicorn. - 502 Bad Gateway: Gunicorn is not running or the socket path differs between the unit file and Nginx. Read
journalctl -u gunicorn. - 403 on static files: Nginx runs as www-data and needs execute permission on every directory in the path.
chmod o+xon /var/www and the project folder usually fixes it. - CSRF verification failed on forms: check
CSRF_TRUSTED_ORIGINSuses thehttps://scheme and that Nginx sendsX-Forwarded-Proto.
sudo systemctl status gunicorn --no-pager
# Ask Gunicorn directly, bypassing Nginx (Host must be in ALLOWED_HOSTS):
curl -s -o /dev/null -w "%{http_code}\n" \
--unix-socket /run/gunicorn/myproject.sock \
-H "Host: example.com" http://localhost/
curl -I https://example.com
sudo tail -n 50 /var/log/nginx/error.logRedeploying
The same pattern as the other guides: a script that GitHub Actions can call on every push.
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/myproject
set -a; source .env; set +a
git pull origin main
.venv/bin/pip install -r requirements.txt
.venv/bin/python manage.py migrate --noinput
.venv/bin/python manage.py collectstatic --noinput
sudo systemctl restart gunicorn
echo "Deployed at $(date -Iseconds)"Questions people ask
Can I run Django on Hostinger shared hosting?
Not well. Shared plans are built for PHP and do not let you keep a long-running Python process such as Gunicorn alive. Use a VPS for Django.
Which Hostinger VPS plan do I need for Django?
KVM 1 handles a typical Django app with PostgreSQL. Choose KVM 2 if you add Celery workers and Redis or expect steady traffic.
Why does Django return Bad Request (400)?
The requested hostname is not in ALLOWED_HOSTS. Add every domain you serve, including the www version, then restart Gunicorn.
Where do I find errors when DEBUG is off?
Run journalctl -u gunicorn for application errors and read /var/log/nginx/error.log for proxy errors. ADMINS email or an error tracker like Sentry makes this much easier.
Independent site, not Hostinger. Buy through my links and Hostinger pays me a referral fee; your price is the same or lower. How that works.