How to Deploy Django on a Budget with Hetzner and DeployHQ

Devops & Infrastructure, Python, and Tutorials

How to Deploy Django on a Budget with Hetzner and DeployHQ

Deploying a Django application doesn't have to mean expensive platform-as-a-service bills or complex container orchestration. With a Hetzner cloud server and DeployHQ, you can run a production Django app for under €5/month — with automated Git-based deployments, zero-downtime releases, and proper production security.

This guide walks through the complete process: provisioning a Hetzner server, configuring it for Django with PostgreSQL, Gunicorn, and Nginx, and setting up DeployHQ for push-to-deploy automation.


What This Setup Costs

One of the biggest advantages of Hetzner is the pricing. Here's what a production Django deployment actually costs:

Component Monthly Cost
Hetzner CX22 (2 vCPU, 4 GB RAM, 40 GB SSD) €4.35
IPv4 address €0.50
DeployHQ Free plan (1 project) €0
Total €4.85/month

For comparison, a comparable setup on DigitalOcean costs $12/month, AWS Lightsail costs $10/month, and managed platforms like Railway or Render start at $7/month per service (and you'll need at least two — web + database).

If you have multiple Django projects, DeployHQ's Pro plan at €19/month covers 10 projects with unlimited deployments and users.


Prerequisites

Before starting, you'll need:

  • A Django application in a Git repository (GitHub, GitLab, or Bitbucket)
  • A requirements.txt file in your repository
  • A Hetzner Cloud account
  • A DeployHQ account (free tier works)
  • An SSH key pair on your local machine

Step 1: Create a Hetzner Cloud Server

Log in to the Hetzner Cloud Console and create a new project.

Click Add Server and configure:

  • Location: Choose the region closest to your users (e.g., Helsinki, Falkenstein, or Ashburn)
  • Image: Ubuntu 24.04
  • Type: Shared vCPU, x86, CX22 (2 vCPU, 4 GB RAM, 40 GB SSD)
  • Networking: Leave both IPv4 and IPv6 enabled
  • SSH Keys: Add your public SSH key
  • Name: django-production

Click Create & Buy Now. Your server will be ready in about 30 seconds.

Test the connection:

ssh root@YOUR_SERVER_IP

Step 2: Secure the Server

Never run your application as root. Create a dedicated deploy user and lock down SSH access.

# Create a deploy user
adduser --disabled-password --gecos "" deploy

# Give deploy user sudo access (for service restarts)
usermod -aG sudo deploy
echo "deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart django-app, /usr/bin/systemctl reload nginx" > /etc/sudoers.d/deploy

# Set up SSH for the deploy user
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

# Disable root SSH login and password authentication
sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

Enable the firewall:

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

From this point forward, connect as the deploy user:

ssh deploy@YOUR_SERVER_IP

Step 3: Install Python, PostgreSQL, and Nginx

sudo apt update && sudo apt upgrade -y

# Python and build dependencies
sudo apt install -y python3 python3-pip python3-venv build-essential libpq-dev

# PostgreSQL
sudo apt install -y postgresql postgresql-contrib

# Nginx
sudo apt install -y nginx

# Certbot for SSL
sudo apt install -y certbot python3-certbot-nginx

Create the PostgreSQL Database

sudo -u postgres psql <<EOF
CREATE DATABASE django_db;
CREATE USER django_user WITH PASSWORD 'GENERATE_A_STRONG_PASSWORD_HERE';
ALTER ROLE django_user SET client_encoding TO 'utf8';
ALTER ROLE django_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE django_user SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE django_db TO django_user;
\q
EOF

Replace GENERATE_A_STRONG_PASSWORD_HERE with a real password. You can generate one with:

python3 -c "import secrets; print(secrets.token_urlsafe(32))"

Step 4: Prepare Your Django Project

Make sure your Django project has these production-ready configurations.

requirements.txt

Your requirements.txt should include:

django>=5.1,<5.2
gunicorn
psycopg[binary]
python-decouple
whitenoise
dj-database-url

settings.py — Production Configuration

Use python-decouple to load sensitive settings from environment variables:

from decouple import config, Csv
import dj_database_url

# Security
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())

# Database
DATABASES = {
    'default': dj_database_url.config(
        default=config('DATABASE_URL')
    )
}

# Static files with WhiteNoise
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # Add after SecurityMiddleware
    # ... rest of your middleware
]

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STORAGES = {
    'staticfiles': {
        'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage',
    },
}

# HTTPS settings (enable once SSL is configured)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=True, cast=bool)
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', cast=Csv(), default='')

.env File (Do NOT Commit This)

Create a .env file for local development:

SECRET_KEY=your-local-dev-secret-key
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=sqlite:///db.sqlite3
SECURE_SSL_REDIRECT=False
CSRF_TRUSTED_ORIGINS=http://localhost:8000

Add .env to your .gitignore:

.env
staticfiles/

Step 5: Set Up the Application Directory

On your server, create the directory structure:

sudo mkdir -p /var/www/django-app
sudo chown deploy:deploy /var/www/django-app

Create the production .env file on the server:

cat > /var/www/django-app/.env << 'EOF'
SECRET_KEY=your-generated-production-secret-key
DEBUG=False
ALLOWED_HOSTS=your-domain.com,YOUR_SERVER_IP
DATABASE_URL=postgres://django_user:YOUR_DB_PASSWORD@localhost:5432/django_db
SECURE_SSL_REDIRECT=False
CSRF_TRUSTED_ORIGINS=https://your-domain.com
EOF

chmod 600 /var/www/django-app/.env

Set SECURE_SSL_REDIRECT=False initially — we'll enable it after configuring SSL.


Step 6: Configure Gunicorn as a Systemd Service

Create a systemd service file so Gunicorn starts automatically and restarts on failure:

sudo tee /etc/systemd/system/django-app.service > /dev/null << 'EOF'
[Unit]
Description=Django Application (Gunicorn)
After=network.target postgresql.service

[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/django-app
ExecStart=/var/www/django-app/venv/bin/gunicorn \
    --workers 3 \
    --bind unix:/var/www/django-app/gunicorn.sock \
    --access-logfile /var/log/django-app/access.log \
    --error-logfile /var/log/django-app/error.log \
    your_project.wsgi:application
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo mkdir -p /var/log/django-app
sudo chown deploy:deploy /var/log/django-app
sudo systemctl daemon-reload
sudo systemctl enable django-app

Replace your_project with your actual Django project name (the directory containing wsgi.py).


Step 7: Configure Nginx

sudo tee /etc/nginx/sites-available/django-app > /dev/null << 'EOF'
server {
    listen 80;
    server_name your-domain.com;

    client_max_body_size 10M;

    location /static/ {
        alias /var/www/django-app/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://unix:/var/www/django-app/gunicorn.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;
    }
}
EOF

sudo ln -sf /etc/nginx/sites-available/django-app /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Add SSL with Let's Encrypt

Once your domain's DNS points to the server:

sudo certbot --nginx -d your-domain.com

Certbot will automatically modify the Nginx configuration to handle HTTPS and set up auto-renewal.

After SSL is working, update the .env on the server:

sed -i 's/SECURE_SSL_REDIRECT=False/SECURE_SSL_REDIRECT=True/' /var/www/django-app/.env

Step 8: Set Up DeployHQ

Now for the automated deployment pipeline. DeployHQ connects your Git repository to your server so every push deploys automatically.

Create a Project

  1. Log in to DeployHQ and click New Project
  2. Connect your GitHub, GitLab, or Bitbucket repository

Add Your Server

Go to ServersNew Server:

  • Name: Production
  • Protocol: SSH/SFTP
  • Hostname: Your Hetzner server IP
  • Port: 22
  • Username: deploy
  • Authentication: SSH Key (add DeployHQ's public key to /home/deploy/.ssh/authorized_keys)
  • Deployment Path: /var/www/django-app
  • Zero-downtime deployments: Enabled

With zero-downtime deployments enabled, DeployHQ deploys to a new release directory and atomically switches a symlink when everything is ready. Your Django app stays live throughout the deploy.

Important: With zero-downtime enabled, your app is served from /var/www/django-app/current. Update the Gunicorn service and Nginx configuration paths accordingly:

# Update Gunicorn WorkingDirectory and ExecStart
sudo sed -i 's|/var/www/django-app|/var/www/django-app/current|g' /etc/systemd/system/django-app.service
sudo sed -i 's|/var/www/django-app/current/current|/var/www/django-app/current|g' /etc/systemd/system/django-app.service

# Update Nginx static path
sudo sed -i 's|/var/www/django-app/staticfiles|/var/www/django-app/current/staticfiles|g' /etc/nginx/sites-available/django-app
sudo sed -i 's|/var/www/django-app/gunicorn|/var/www/django-app/current/gunicorn|g' /etc/nginx/sites-available/django-app

sudo systemctl daemon-reload
sudo nginx -t && sudo systemctl reload nginx

Configure the Build Pipeline

Go to Build PipelineNew Command. DeployHQ's build pipeline runs commands on DeployHQ's servers before transferring files — so your production server doesn't need build tools.

Add a build command:

pip install -r requirements.txt -t vendor/

This installs dependencies into a vendor/ directory that gets deployed alongside your code. Enable Cache for vendor/** to speed up subsequent deployments.

Add SSH Post-Deploy Commands

Go to SSH Commands and add a command that runs after deployment:

cd /var/www/django-app/current
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt --quiet
python3 manage.py migrate --noinput
python3 manage.py collectstatic --noinput
sudo /usr/bin/systemctl restart django-app

Use Config Files for the .env

Instead of manually managing the .env file on the server, use DeployHQ's config files to inject it during deployment. Go to Config FilesNew Config File:

  • Path: .env
  • Content: Your production environment variables

This way, the .env file is deployed automatically with every release, and you can manage different values for staging vs production servers without touching the server manually.

Enable Automatic Deployments

In your project settings, enable automatic deployments for the main branch. Now the workflow is:

git push origin main → DeployHQ detects push → Deploys files → Runs migrations → Restarts Gunicorn → Done

Step 9: Deploy and Verify

Trigger your first deployment from the DeployHQ dashboard. Once it completes, verify:

# Check the app is running
sudo systemctl status django-app

# Check Nginx is proxying correctly
curl -I http://your-domain.com

# Check the Django logs
tail -f /var/log/django-app/error.log

Visit your domain in a browser. Your Django app should be live.


Production Checklist

Before considering your deployment production-ready, verify these:

  • [ ] DEBUG = False in production
  • [ ] SECRET_KEY is unique and not committed to Git
  • [ ] Database password is strong and stored in .env
  • [ ] SSL certificate is active (https:// works)
  • [ ] SECURE_SSL_REDIRECT = True
  • [ ] SSH root login is disabled
  • [ ] Firewall allows only SSH, HTTP, and HTTPS
  • [ ] Gunicorn runs as a non-root user
  • [ ] Static files are served by Nginx (not Django)
  • [ ] Database backups are scheduled
  • [ ] Log rotation is configured

Scaling Up

As your Django app grows, Hetzner makes it easy to scale:

Need Solution Cost
More CPU/RAM Upgrade to CX32 (4 vCPU, 8 GB) ~€8.50/mo
More storage Add a Hetzner Volume From €0.052/GB/mo
Database performance Move PostgreSQL to a separate server ~€4.35/mo
Multiple apps Add more servers in DeployHQ (Pro plan) €19/mo for 10 projects
Global CDN Add Cloudflare (free tier) €0

Even at the higher end — a CX32 server, separate database, and DeployHQ Pro — you're looking at under €30/month for a setup that handles significant traffic.


Common Questions

Why Hetzner over AWS or DigitalOcean?

Price. Hetzner's CX22 (2 vCPU, 4 GB RAM) costs €4.35/month. The equivalent on DigitalOcean is $12/month, and on AWS it's $10+/month for Lightsail. For a Django app that doesn't need AWS-specific services, Hetzner gives you more resources for less money.

Can I deploy multiple Django apps to one server?

Yes. Create separate directories, Gunicorn services, and Nginx server blocks for each app. In DeployHQ, add each app as a separate server entry pointing to different deployment paths.

What about managed databases?

Hetzner doesn't offer managed PostgreSQL (yet). If you want managed databases, consider using a separate service like Supabase (free tier) or Neon (free tier) while keeping your compute on Hetzner.

How do I handle Django Celery workers?

Add a second systemd service for the Celery worker, similar to the Gunicorn service. In your DeployHQ SSH command, restart both services after deployment.

What if a deployment fails?

With zero-downtime deployments enabled, a failed deployment doesn't affect your live site — the symlink still points to the previous release. Fix the issue, push again, and DeployHQ will retry. You can also roll back to any previous deployment from the DeployHQ dashboard.


The Bottom Line

A production Django deployment on Hetzner with DeployHQ costs under €5/month and takes about an hour to set up. You get a real server with full control, automated Git-based deployments, zero-downtime releases, and room to scale — all without the complexity of Kubernetes or the cost of managed platforms.

Start your free DeployHQ trial and connect it to your Hetzner server — no credit card required.