Skip to content
savemores

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 · 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_ORIGINS and 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.
  • 04python manage.py check --deploy lists 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.

bash
sudo apt update
sudo apt install -y python3-venv python3-dev build-essential libpq-dev \
  postgresql nginx git
A database plus a user that owns it:
bash
sudo -u postgres psql <<'SQL'
CREATE USER myproject WITH PASSWORD 'use-a-long-random-password';
CREATE DATABASE myproject OWNER myproject;
SQL

Step 2: the code and a virtualenv

Keep the virtualenv inside the project as .venv. Gunicorn and the PostgreSQL driver go in alongside your requirements.

bash
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.

settings.py (the production-relevant parts)
python
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 = True
/var/www/myproject/.env (chmod 600)
bash
DJANGO_SECRET_KEY=generate-a-long-random-string
DB_PASSWORD=use-a-long-random-password
Load the env, migrate, collect static files, run the deploy check:
bash
set -a; source .env; set +a
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py createsuperuser
python manage.py check --deploy

Step 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.

/etc/systemd/system/gunicorn.service
ini
[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.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn
sudo systemctl status gunicorn
journalctl -u gunicorn -f   # live logs

Step 5: Nginx and HTTPS

Static and media requests never reach Python. Nginx serves them directly and proxies the rest to the socket.

/etc/nginx/sites-available/myproject
nginx
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;
    }
}
bash
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.com

Verify 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+x on /var/www and the project folder usually fixes it.
  • CSRF verification failed on forms: check CSRF_TRUSTED_ORIGINS uses the https:// scheme and that Nginx sends X-Forwarded-Proto.
bash
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.log

Redeploying

The same pattern as the other guides: a script that GitHub Actions can call on every push.

/var/www/myproject/deploy.sh
bash
#!/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.