Manual Django Deployment on Hetzner VPS (From Zero to Production)
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
- Hetzner VPS (Ubuntu 22.04 or newer).
- Domain Name (e.g., from GoDaddy).
- Cloudflare Account (Free tier).
- GitHub Repository (Private or Public).
- 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.
3. Setup Basic Firewall (UFW)
Secure the server immediately by blocking all ports except SSH and Web traffic.
Press y to confirm.
--------------------------------------------------------------------------------
Phase 2: Database Setup (PostgreSQL)
1. Create Database and User
Log in to the Postgres interactive shell:
Run the following SQL commands:
🛑 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:
--------------------------------------------------------------------------------
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.
🛑 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:
Then, on the server, run git pull.
--------------------------------------------------------------------------------
Phase 4: Django Environment Setup
1. Virtual Environment & Requirements
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:
• Update DATABASES with the PostgreSQL info created in Phase 2.
3. Finalize Django Setup
🛑 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
2. Paste Configuration
How to Save in nano
1️⃣ Press:
(O = Output / Write file)
You will see at bottom:
2️⃣ Press:
3️⃣ Then exit nano:
Done ✅
3. Start the Service
🛑 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
Paste the following:
2. Enable the Site
3. Remove Default Site (Crucial)
4. 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:
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.