# Django on a Hostinger VPS with Gunicorn, Nginx and PostgreSQL

URL: https://www.savemores.store/vps/deploy-django
Author: Mr. Ahmed · Published 2026-10-17 · Updated 2026-10-17 · VPS & deployment

## Short answer

- To 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.
- Behind 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.
- A common Gunicorn starting point is two workers per CPU core plus one: 3 on KVM 1, 5 on KVM 2.
- `python manage.py check --deploy` lists the production security settings you have missed; run it before going live.
- Django 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](https://www.savemores.store/vps/secure-new-vps)) and an A record already pointing at the server ([DNS guide](https://www.savemores.store/vps/point-domain-dns)). A [KVM 1](https://www.savemores.store/hostinger-deals/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
```

> From the bench: `manage.py check --deploy` is the most underused command in Django. It reads your settings and lists every production security gap. Run it before launch and treat its output as a to-do list, not a suggestion.

## 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](https://www.savemores.store/vps/github-actions-deploy) 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)"
```

> From the bench: With DEBUG off, errors go quiet. Setting up ADMINS with email, or an error tracker such as Sentry, before launch saves you from reconstructing a failure from access logs after a user reports it.

## FAQ

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