Django Hetzner Hosting

Manual Django Deployment on Hetzner VPS (From Zero to Production)

Posted: February 07, 2026 | Updated: February 12, 2026
Share: Twitter LinkedIn

This guide covers the transition from shared hosting (like PythonAnywhere) to a professional Virtual Private Server (VPS) setup using Ubuntu, Nginx, Gunicorn, PostgreSQL, and Cloudflare.

📋 Prerequisites

  1. Hetzner VPS (Ubuntu 22.04 or newer).
  2. Domain Name (e.g., from GoDaddy).
  3. Cloudflare Account (Free tier).
  4. GitHub Repository (Private or Public).
  5. VS Code (Optional but recommended for editing).


--------------------------------------------------------------------------------

Phase 1: Server Initialization & Security

1. SSH into your Server

From your local computer (PowerShell or Terminal), connect to the server using the IP provided by Hetzner.

If asked to continue connecting, type yes.

2. Update and Install the Stack

Update the OS and install Python, PostgreSQL, Nginx, Redis, and Git.

apt update && apt upgrade -y
apt install python3-pip python3-venv nginx postgresql postgresql-contrib redis-server git ufw fail2ban -y

3. Setup Basic Firewall (UFW)

Secure the server immediately by blocking all ports except SSH and Web traffic.

ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable

Press y to confirm.


--------------------------------------------------------------------------------

Phase 2: Database Setup (PostgreSQL)

1. Create Database and User

Log in to the Postgres interactive shell:

sudo -u postgres psql

Run the following SQL commands:

CREATE DATABASE myproject;
CREATE USER myuser WITH PASSWORD 'StrongPassword123';
ALTER ROLE myuser SET client_encoding TO 'utf8';
ALTER ROLE myuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE myuser SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myproject TO myuser;

🛑 Common Error: Permission Denied for Schema

Error: During Django migration, you may see permission denied for schema public. The Fix: Modern PostgreSQL requires explicit schema permissions. Run these additional commands inside the psql shell:

\c myproject
GRANT ALL ON SCHEMA public TO myuser;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO myuser;
ALTER SCHEMA public OWNER TO myuser;
\q


--------------------------------------------------------------------------------

Phase 3: Code Deployment via Git

1. Setup SSH Keys for GitHub

Your server needs permission to clone your private repo.

• On the Server: Run ssh-keygen -t ed25519 -C "hetzner-server".

• Action: Copy the output of cat ~/.ssh/id_ed25519.pub.

• On GitHub: Go to Repo Settings > Deploy Keys > Add Key. Paste the key and enable "Allow write access".

2. Clone the Repository

We use /var/www/ as the standard directory.

mkdir -p /var/www
cd /var/www
git clone [email protected]:yourname/myproject.git
cd myproject

🛑 Common Error: Wrong Branch / Old Code

Problem: The server clones main, but your latest code is on a local branch (e.g., dev or v2-3). The Fix (Force Push): From your local computer, force your current branch to become the main branch on GitHub:

git push origin your-current-branch:main --force

Then, on the server, run git pull.


--------------------------------------------------------------------------------

Phase 4: Django Environment Setup

1. Virtual Environment & Requirements

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install gunicorn

2. Configure settings.py

You can edit files directly on the server using nano or use VS Code Remote-SSH (Recommended).

• VS Code Method: Install "Remote - SSH" extension → Press F1 → "Connect to Host" → Select "Linux".

Update settings.py:

• DEBUG = False
• ALLOWED_HOSTS = ['example.com', 'www.example.com', '123.45.67.89']

• Update DATABASES with the PostgreSQL info created in Phase 2.

3. Finalize Django Setup

python manage.py migrate
python manage.py collectstatic

🛑 Testing Tip: The Port 8000 Trap

If you try to test with python manage.py runserver 0.0.0.0:8000, it will fail because the firewall blocks port 8000. The Fix: Temporarily allow the port: ufw allow 8000. Remember to delete this rule later for production security.


--------------------------------------------------------------------------------

Phase 5: Gunicorn (Application Server)

Do not run Gunicorn manually. Create a system service so it restarts automatically.

1. Create Service File

sudo nano /etc/systemd/system/gunicorn.service

2. Paste Configuration

[Unit]
Description=Gunicorn daemon for myproject
After=network.target

[Service]
User=root
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:application
Restart=always

[Install]
WantedBy=multi-user.target

How to Save in nano

1️⃣ Press:

CTRL + O

(O = Output / Write file)

You will see at bottom:

File Name to Write: /etc/systemd/system/mysite2.service

2️⃣ Press:

ENTER

3️⃣ Then exit nano:

CTRL + X

Done ✅


3. Start the Service

systemctl daemon-reload
systemctl start gunicorn
systemctl enable gunicorn

🛑 Common Error: "Unit gunicorn.service not found"

Cause: You forgot to reload systemd after creating the file. The Fix: Run systemctl daemon-reload.


--------------------------------------------------------------------------------

Phase 6: Nginx (Web Server)

1. Create Nginx Config

sudo nano /etc/nginx/sites-available/myproject

Paste the following:




server {
listen 80;
server_name example.com www.example.com;

location /static/ {
alias /var/www/myproject/static/;
}

location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

2. Enable the Site

ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled/

3. Remove Default Site (Crucial)

rm /etc/nginx/sites-enabled/default

4. Restart Nginx

nginx -t
systemctl restart nginx

🛑 Common Error: "Connection Refused"

Cause: Nginx config uses server_name 123.45.67.89 (IP) but you are accessing via Domain, OR the default site is still active. The Fix: Ensure server_name in the Nginx config matches your domain exactly, and remove the default file from sites-enabled.


--------------------------------------------------------------------------------

Phase 7: Domain & SSL (Cloudflare)

1. DNS Setup

• In GoDaddy: Change Nameservers to the ones provided by Cloudflare.

• In Cloudflare DNS:

◦ Add A record: @ points to 123.45.67.89 (Your Server IP).
◦ Add CNAME or A record: www points to 123.45.67.89.
◦ Proxy Status: Set to Proxied (Orange Cloud).

2. SSL Setup

• Go to Cloudflare SSL/TLS menu.

• Set mode to Flexible (if you didn't install Certbot on the server) OR Full (if using Certbot).

• Recommendation: Start with Flexible for immediate HTTPS without server config.

🛑 Common Error: Cloudflare Error 521

Cause: Cloudflare cannot talk to your server. Usually, because the firewall blocks port 80/443 or Nginx isn't listening. The Fix:

1. Check Firewall: ufw allow 80 and ufw allow 443.

2. Check Nginx: Ensure Nginx is running (systemctl status nginx).

3. Check Nginx Config: Ensure server_name matches the domain request coming from Cloudflare.


--------------------------------------------------------------------------------

🎯 Summary Checklist

1. [x] Server: Ubuntu updated & secured.

2. [x] DB: Postgres setup with schema permissions granted.

3. [x] App: Django running via Gunicorn systemd service (Port 8000 internal).

4. [x] Web: Nginx proxying Port 80 → Port 8000.

5. [x] DNS: Cloudflare pointing to Server IP (Proxied).

6. [x] SSL: Cloudflare handling HTTPS.