Skip to content
Extra 20% off HostingerWEBHOST2026Get my link →
DiscountOffer.Store
Tutorials

Deploy Django on a Hostinger VPS (Gunicorn, Nginx, PostgreSQL)

10 min read

A production Django setup on Ubuntu: a virtualenv, PostgreSQL, Gunicorn under systemd, Nginx serving static files, the security settings Django needs behind a proxy, and free HTTPS.

What you'll end up with

Nginx takes the request, serves static and media files straight from disk, and passes everything else over a Unix socket to Gunicorn, which runs your Django app. systemd keeps Gunicorn alive. PostgreSQL holds the data on the same machine.

This assumes Ubuntu 24.04 with a secured sudo user and an A record already pointing at the server.

Step 1 — System packages and the database

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
Create a database and 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 — Code and virtualenv

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

Read secrets from the environment, not from `settings.py`. The proxy settings matter: without them Django thinks every request arrived over plain HTTP, and secure cookies and CSRF checks 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
Migrate and collect static files:
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 for workers is two per CPU core plus one, so 3 on KVM 1 and 5 on KVM 2. `RuntimeDirectory` makes systemd create `/run/gunicorn` for the socket on every start.

/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

Static and media files never touch Python; Nginx serves them directly.

/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

Redeploying

The same pattern as every other guide here: a script that GitHub Actions can call.

/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)"
Extra 20% off with WEBHOST2026Claim 20% off →

Frequently asked questions

Can I run Django on Hostinger shared hosting?

Not well. Shared plans are built for PHP and don't let you run a long-lived Python process like Gunicorn. Use a VPS for Django.

Which 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 say 'Bad Request (400)'?

The domain isn't in ALLOWED_HOSTS. Add every hostname you serve, including www, and restart Gunicorn.

Where do I see errors when DEBUG is off?

Run `journalctl -u gunicorn` for app errors and check /var/log/nginx/error.log for proxy errors. Setting up ADMINS with email, or an error tracker such as Sentry, saves a lot of digging.

Ready to save an extra 20% with coupon WEBHOST2026?

Get my discount link →

Disclosure: links to Hostinger on this page are referral links. If you buy through them we may earn a commission, at no extra cost to you. It doesn't change what we recommend. About us

Ready to claim your Hostinger discount?

Generate your personalised cart link with coupon WEBHOST2026 attached — it takes 20 seconds.