Hetzner Cloud — Complete Hosting Guide

From zero to multiple live websites on Hetzner Cloud, with Cloudflare DNS, WordPress, and CloudPanel. Written for someone who has never touched a server before.

📖
This guide covers two paths: a manual LEMP stack (NGINX + PHP + MySQL) for full control, and CloudPanel for a GUI-based approach. Both use Cloudflare for DNS. Read Part 1 (server setup) first — it applies to both paths.
Required You must do this — skipping will break things Recommended Strongly advised for security / usability Optional Nice to have — skip to keep things simple

00Overview & What You'll Build

✅ A Hetzner Cloud server running Ubuntu, fully secured

✅ SSH key authentication (no passwords)

✅ A firewall blocking everything except web traffic and SSH

✅ Cloudflare managing DNS with CDN/DDoS protection

✅ One or more WordPress websites

✅ Ability to add non-WordPress (static HTML, Node.js, PHP) sites

✅ Multiple domains and subdomains on one server

✅ Free SSL certificates

✅ Automated backups

The Architecture

Your Visitors
Cloudflare (CDN/DNS)
Hetzner Server
NGINX / CloudPanel
Your Websites

01Prerequisites

WhatWhyCost
Hetzner accountYour server lives hereFrom ~€4.50/mo
Cloudflare accountDNS, CDN, DDoS protectionFree plan works
A domain nameAlready on CloudflareVaries
A way to type commandsOnly for initial setup — see belowFree

02Choose Your Path

🔧 Path A — Manual LEMP Stack

Install NGINX, PHP, MySQL yourself. Full control, more terminal usage throughout.

🖥️ Path B — CloudPanel (GUI)

Free control panel. One-time terminal install, then everything else is browser-based. Best if you prefer not to use the terminal.

⚠️
Choose ONE path. Don't install a manual LEMP stack and then CloudPanel on the same server — they will conflict.

03How You'll Type Commands Required — Read This

You'll need to type a handful of commands during initial setup. Pick whichever option you're most comfortable with:

Option A: Hetzner Web Console (100% browser — no setup needed)

After creating your server, click the terminal icon (>_) on your server's dashboard page. A terminal opens in a new browser tab.

Pros: No software to install. Works on any computer, even a Chromebook.
Cons: Copy/paste can be clunky (use Ctrl+Shift+V). Slight input lag. Can time out if idle.

How: Hetzner Cloud Console → click your server → terminal icon (top-right) → new tab opens.

Option B: Your computer's built-in terminal

Mac: Terminal app (search Spotlight). Windows 10/11: Windows Terminal or PowerShell. Linux: Your terminal of choice.

Pros: Faster, better copy/paste. Cons: Requires SSH keys (section 1.2).

Option C: PuTTY (Windows graphical SSH client)

Download PuTTY. Enter your server IP, click Open, terminal appears.

💡
Our recommendation if you're not familiar with terminals: Use the Hetzner Web Console (Option A). Zero preparation — just click a button. Throughout this guide, 🖥️ Web Console notes flag anything different for browser-console users.

04Terminal Survival Guide Recommended — Read This

A few things that will save you a lot of confusion:

🔑 sudo — means "run this command as admin". Most commands in this guide use sudo because they change system settings. If you're logged in as root, you don't need sudo (you're already admin). If logged in as your deploy user, always use sudo for system commands.

⌨️ Ctrl+C — cancels/stops whatever command is currently running. If something hangs or you made a mistake, press Ctrl+C to abort and get back to a clean prompt.

📝 nano (the text editor) — when this guide says "edit a file", we use nano. Quick reference: Ctrl+W = search, Ctrl+X = exit, then Y to save, Enter to confirm filename. Arrow keys to move around.

👁️ Passwords are invisible — when typing a password in the terminal, nothing appears on screen. No dots, no asterisks, nothing. This is normal. Just type it and press Enter.

📋 Pasting — in most terminals: Ctrl+Shift+V (Linux/Windows) or Cmd+V (Mac). In Hetzner Web Console, try right-click → Paste if shortcuts don't work.

🚨 If a command fails — don't keep retrying randomly. Read the error message. It usually tells you exactly what's wrong. Common causes: typo in the command, missing sudo, or a service that hasn't started yet.

05Hetzner Projects Recommended — Read First

In Hetzner Cloud, everything (servers, firewalls, SSH keys, volumes, networks, backups, API tokens) lives inside a Project. A project is a container that groups related resources and isolates billing and access. You can have many projects under a single Hetzner account.

Good defaults: one project per client, one per major app, or one "personal" project for everything you own. Don't mix production and throwaway experiments in the same project — firewalls and SSH keys only exist within a project, so splitting keeps accidents contained.

How: Hetzner Cloud Console → top-left project dropdown → New Project → name it → enter it before creating your first server.

06Locations & Datacenters

Hetzner runs datacenters in several regions. Pick the one closest to your main audience — latency matters far more than marginal price differences. Location cannot be changed after creation; you'd have to migrate via snapshot.

LocationCodeRegionGood for
Falkenstein, DEfsn1EuropeEU audiences, cheapest historically
Nuremberg, DEnbg1EuropeEU audiences
Helsinki, FIhel1Europe (North)Nordics / Baltics / Russia-adjacent
Ashburn, VA, USashUS EastUS / Canada East, Latin America
Hillsboro, OR, UShilUS WestUS West, West Canada
SingaporesinAsia-PacificSEA, South Asia, Australia
💡
Not sure? With Cloudflare in front (which this guide uses), your static and cached content is served from Cloudflare's global edge regardless — so the origin location mainly impacts dynamic requests (WordPress admin, logged-in users, API calls).

07Server Types — CX vs CAX vs CPX vs CCX

Hetzner offers two tiers: Shared vCPU (cheaper, fine for most websites) and Dedicated vCPU (guaranteed cores, for CPU-heavy workloads). Within each tier there are different CPU architectures.

LineTierCPUNotesUse when
CXSharedIntel x86Classic all-rounderDefault choice for WordPress / PHP / Node
CPXSharedAMD x86Faster per-core than CX, small premiumBetter perf on the same budget
CAXSharedAmpere ARM64Cheapest, excellent perf/€Only if your stack runs on ARM (most do — Ubuntu, NGINX, PHP, MariaDB, Node all fine)
CCXDedicatedAMD EPYCGuaranteed vCPUs, 2–4× priceSustained CPU load, game/app servers, CI
⚠️
ARM caveat (CAX): some legacy plugins or proprietary binaries may lack ARM builds. Safe for vanilla WordPress; verify if you use niche tools. When in doubt, use CPX.

You can rescale freely between sizes within a line (e.g. CX22 → CX32), and also between shared lines that share architecture. Changing architecture (x86 ↔ ARM) requires a fresh install or snapshot-based migration.

08Images — What "Image" Means

The "Image" is what gets installed on your new server. Hetzner offers four kinds:

OS images — plain Ubuntu, Debian, Fedora, Rocky, Alma, CentOS Stream, openSUSE. Use Ubuntu 24.04 LTS for this guide.

Apps — prebuilt OS + software bundles (Docker, WordPress, LAMP, cPanel, Plesk, GitLab, etc.). Handy but less transparent; this guide prefers installing manually on Ubuntu.

Snapshots — point-in-time images you create from an existing server. Useful for cloning or migrating between locations / architectures.

Backups — automatic daily snapshots Hetzner manages for you (enable in the server's Backups tab; ~20% of server price).

09IPv4 vs IPv6 Recommended — Read This

Every server gets network addresses. You'll see both "IPv4" and "IPv6" checkboxes during server creation. Here's what they actually mean:

What they are

IPv4 — the old, short format (e.g. 168.119.12.34). Four numbers 0–255. The internet is running out of them, so Hetzner charges a small fee for each one (~€0.60/month). Every website visitor, every OS, every DNS provider understands IPv4. It is the lowest common denominator.

IPv6 — the new, long format (e.g. 2a01:4f8:c17:1a2b::1). Effectively unlimited supply, so Hetzner gives you a /64 block (18 quintillion addresses) for free. Supported by all modern networks, but not every ISP or corporate network has it turned on.

Which should I enable?

SetupWorks for visitors?CostRecommended?
IPv4 only✅ Everyone~€0.60/moOK — simple
IPv4 + IPv6 (dual-stack)✅ Everyone (IPv6 users get IPv6, others get IPv4)~€0.60/moBest — default
IPv6 only⚠️ Only IPv6-capable visitors (~40% globally). Most home/office networks still can't reach you.Free❌ Don't — you'll lose traffic
💡
Default for this guide: enable both. Leave the defaults in the Hetzner UI. You pay the same tiny IPv4 fee either way, and dual-stack means the widest reach.

What this means in DNS

A record → points a domain to an IPv4 address. Example: example.com A 168.119.12.34.

AAAA record ("quad-A") → points a domain to an IPv6 address. Example: example.com AAAA 2a01:4f8:c17:1a2b::1.

If you set up DNS through Cloudflare with the orange cloud (Proxied) — which this guide recommends — you only need the A record. Cloudflare talks to your server over whichever protocol is available; visitors talk to Cloudflare. So IPv6 on the origin is "nice to have" but not required for your site to be reachable over IPv6 globally.

If you go DNS-only (grey cloud) and want full IPv6 support for visitors, add an AAAA record too, pointing at the ::1 address from your Hetzner server's /64 range (Server → Networking → copy the IPv6).

Connecting via SSH

# Over IPv4
ssh deploy@168.119.12.34

# Over IPv6 (brackets not needed for ssh, but needed in URLs)
ssh deploy@2a01:4f8:c17:1a2b::1

Your home ISP must support IPv6 for the second one to work. If it hangs, your network probably can't route IPv6 — just use IPv4.

⚠️
Firewalls apply to both. UFW and Hetzner Cloud Firewalls filter IPv4 and IPv6 separately. The commands in this guide (ufw allow 80/tcp) apply to both by default, but if you write custom rules with explicit IPs, remember to cover v6 too.

10Private Networks Optional

A Private Network lets multiple servers in the same location talk to each other over a private subnet that is not exposed to the public internet. You'd use this when you eventually split your stack — e.g. one server for the web app, another for the database.

When you need it: multi-server setups, database server separate from web server, internal-only services (Redis, private APIs).

When you don't: single-server WordPress setup (this guide's default). Skip for now.

How: Hetzner Console → project → NetworksCreate Network → pick a range (e.g. 10.0.0.0/16) → attach servers to it. Free of charge.

11Volumes (Block Storage) Optional

Volumes are extra disks you can attach to a server. Your server's built-in disk is fine for most websites (CX22 = 40 GB, plenty for dozens of WordPress sites). Volumes matter when you outgrow that.

Key traits: 10 GB – 10 TB per volume. Independent of the server — you can detach from one server and attach to another. ~€0.044 per GB/month (EU). Only available in the same location as the server.

When you'd use one: large media libraries, big databases, shared storage you want to keep even if you rebuild the server.

Create in the browser (Volumes → Create Volume), attach to a server, then mount inside the OS (Hetzner shows the exact commands). You don't need this on day one — add it later if you hit a disk wall.

12Placement Groups Optional

A Placement Group tells Hetzner: "spread these servers across different physical hosts so they don't all go down together." Only useful if you run multiple servers that back each other up (e.g. two web servers behind a load balancer).

For a single-server setup, ignore this entirely. For HA setups: create a spread placement group, then assign each server to it at creation time.

13Cloud-init / User Data Optional — Power User

During server creation, there's a "Cloud config" / User Data field. This lets you paste a script that runs automatically on first boot — so your server comes up already updated, with users created and software installed, without touching the terminal manually.

Example — this auto-updates and installs basics:

#cloud-config
package_update: true
package_upgrade: true
packages:
  - ufw
  - fail2ban
  - htop
  - unattended-upgrades
runcmd:
  - ufw allow OpenSSH
  - ufw allow 80/tcp
  - ufw allow 443/tcp
  - ufw --force enable
  - systemctl enable --now fail2ban
💡
Great for spinning up identical servers repeatably. Skip on your first server — learn the manual steps first so you understand what cloud-init is automating.

14Labels Optional

Labels are key-value tags (env=prod, client=acme) you can attach to servers, volumes, networks, etc. Useful once you have many resources — filter the Console by label, or target groups of servers via the API. Ignore if you have 1–2 servers.

1.1Create Your Hetzner Account Required

Entirely in your browser.

1

Go to console.hetzner.cloudSign Up.

2

Enter email, password, verify email.

3

Hetzner may request ID verification (passport/licence). Normal — takes a few hours to a day.

4

Add a payment method under Billing.

5

Recommended Enable Two-Factor Authentication in account settings.

🎁
Hetzner often offers €20 free credit for new accounts.

1.2Generate SSH Keys Recommended

SSH keys let your computer prove its identity cryptographically instead of using a password.

💡
Can I skip this? If you'll only use the Hetzner Web Console, you can skip SSH keys. Hetzner will email you a root password instead. However, SSH keys are much more secure — password login is a common attack vector.

What are SSH keys?

🔑 Private key — stays on YOUR computer only. Never share this.

🔓 Public key — goes on the server. Anyone can see it, but only your private key unlocks it.

Generate the key pair

On your local computer (not the server), open your terminal:

ssh-keygen -t ed25519 -C "your-email@example.com"

Press Enter for default location, then set a passphrase (or Enter for none).

View the public key:

cat ~/.ssh/id_ed25519.pub

Copy the entire output line.

Add to Hetzner (browser)

Hetzner Cloud Console → your project → SecuritySSH KeysAdd SSH Key → paste your public key.

🖥️ Web Console users (skipping SSH keys)

Don't select any SSH key when creating your server. Hetzner will generate a root password and show it on-screen / email it. Change this password immediately after first login by typing passwd.

1.3Create Your Server Required

Entirely in your browser via Hetzner Cloud Console. Make sure you're inside the right Project before clicking Add Server — resources are project-scoped and can't be moved between projects later.

📖
Every field below maps to a concept explained above: Location · Image · Type · Networking (IPv4/IPv6) · Private Networks · Placement Groups · Cloud config · Labels. Scroll back up if any term is unclear.
1

Click Add Server.

2

Required Location: Choose closest to your audience (Falkenstein, Nuremberg, Helsinki, Ashburn, or Hillsboro).

3

Required Image: Select Ubuntu 24.04.

4

Required Type: CX22 (2 vCPU, 4 GB RAM, ~€4.50/mo) or CPX21 (3 vCPU AMD, ~€8/mo). You can upgrade later without data loss.

5

Required Networking: Leave defaults (Public IPv4 + IPv6).

6

Recommended SSH Key: Select your key from 1.2. If you skipped, leave blank.

7

Optional Firewall: Create a Cloud Firewall (SSH 22, HTTP 80, HTTPS 443). Can be done later via UFW.

8

Optional Name: e.g. web-server-01.

9

Click Create & Buy Now. Ready in ~30 seconds.

Note your server's IPv4 address (e.g. 168.119.xxx.xxx) — you'll need it everywhere.

1.4Connect to Your Server Required

Method A: Hetzner Web Console (browser)

1

Click your server name in the Hetzner dashboard.

2

Click the terminal icon (>_) — top-right of the page.

3

A black terminal opens in a new tab. Click inside it, press Enter.

4

Log in as root. If you used SSH keys, no password needed. If you skipped SSH keys, enter the password Hetzner emailed you.

💡
Pasting in Web Console: Use Ctrl+Shift+V or right-click → Paste.

Method B: Local terminal (SSH)

ssh root@YOUR_SERVER_IP

First time: type yes when asked about host authenticity.

First thing: update everything Required

apt update && apt upgrade -y

This installs all security patches. Takes a minute or two.

1.5Secure the Server Recommended

💡
Can I skip this? For CloudPanel (Path B), creating a separate user is optional since CloudPanel manages its own isolation. For Path A, this is strongly recommended.

Create a non-root user

# Create user (replace "deploy" with any name you like)
adduser deploy

# Give them admin privileges
usermod -aG sudo deploy

Set a strong password. Skip optional info by pressing Enter.

Copy your SSH key to the new user Recommended

Only needed if you set up SSH keys:

mkdir -p /home/deploy/.ssh
cp /root/.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

Test the new user Required before disabling root

🖥️ Web Console users

Open a second Web Console tab (click the terminal icon again). Log in as deploy with your password. Type sudo whoami — should output root.

Or from a local terminal, open a new window:

ssh deploy@YOUR_SERVER_IP
sudo whoami    # should output: root
🚨
Do NOT disable root login until you've confirmed the new user works with sudo. Otherwise you could lock yourself out permanently.

Disable root login & password auth Recommended

nano /etc/ssh/sshd_config

Find and change (Ctrl+W to search):

PermitRootLogin no
PasswordAuthentication no

Remove # from the beginning of the line if present. Save: Ctrl+XYEnter.

systemctl restart sshd
🖥️ Web Console users

If you skipped SSH keys, do NOT set PasswordAuthentication no — that locks out SSH entirely. You can still set PermitRootLogin no as long as your deploy user works, because the Web Console bypasses SSH.

1.6Firewall Setup (UFW) Recommended

sudo ufw default deny incoming
sudo ufw default allow outgoing

# CRITICAL — allow SSH before enabling!
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# CloudPanel users: also allow port 8443
sudo ufw allow 8443/tcp

sudo ufw enable
sudo ufw status verbose
🚨
Forgot SSH before enabling? The Hetzner Web Console always works (it bypasses SSH), so you can fix it from the browser.

1.7Install Fail2Ban Recommended

Auto-bans IPs that try too many failed logins.

sudo apt install -y fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo fail2ban-client status

1.8Automatic Security Updates Recommended

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

Select Yes. Critical patches will now auto-apply.

1.9Basic Monitoring Optional

Give yourself visibility into your server's health. Two commands to remember:

# Install htop — a visual process/CPU/memory monitor
sudo apt install -y htop

# Run it (press 'q' to exit)
htop
# Check disk space
df -h

# Check memory usage
free -h

That's it. Run these occasionally to make sure your server isn't running out of disk or memory. If disk usage is above 85%, it's time to clean up or upgrade.

🔐Quick Hardening Checklist

Before moving on to Part 2, tick everything off. This is your TL;DR safety net — go back and fix anything unchecked.

Server Security Checklist

Required
Recommended
Recommended
Recommended
Recommended
Recommended
Recommended
Recommended
Required
🎉
Part 1 Complete! Choose your path: Path A (Manual LEMP) or Path B (CloudPanel).

2.1Install NGINX Path A Required

sudo apt install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx

Visit http://YOUR_SERVER_IP in your browser. You should see the NGINX welcome page.

2.2Install PHP Path A Required for WordPress

sudo apt install -y php-fpm php-mysql php-curl php-gd php-intl \
  php-mbstring php-soap php-xml php-xmlrpc php-zip php-imagick php-cli
php -v
# Note the version (e.g. 8.3) — you'll need this later

2.3Install MariaDB Path A Required for WordPress

sudo apt install -y mariadb-server
sudo mysql_secure_installation

Switch to unix_socket auth?n · Change root password?Y (set a strong one) · Remove anonymous users?Y · Disallow root login remotely?Y · Remove test database?Y · Reload privilege tables?Y

Create a database for WordPress Required

sudo mysql

CREATE DATABASE wordpress_db;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'YOUR_STRONG_PASSWORD_HERE';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

2.4Cloudflare DNS Setup Required

Entirely in your browser — Cloudflare Dashboard.

Main domain

TypeNameContentProxyTTL
A@YOUR_SERVER_IPProxied (orange)Auto
AwwwYOUR_SERVER_IPProxied (orange)Auto

Subdomains

TypeNameContentProxyTTL
AblogYOUR_SERVER_IPProxiedAuto

One A record per subdomain. All point to the same IP.

Additional domains

For a different domain on Cloudflare: same A records (@ and www), same server IP. One server = unlimited domains.

💡
Proxied (orange) = traffic through Cloudflare CDN/DDoS. DNS only (grey) = direct to server. Use Proxied for websites, DNS only for SSH or non-web services.

2.5NGINX Virtual Hosts Path A Required

sudo mkdir -p /var/www/example.com/public_html
sudo chown -R deploy:deploy /var/www/example.com
sudo chmod -R 755 /var/www/example.com
sudo nano /etc/nginx/sites-available/example.com

Paste:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/example.com/public_html;
    index index.php index.html index.htm;

    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\. { deny all; }

    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
}
⚠️
Replace example.com with your domain. Match the PHP version: if php -v showed 8.1, use php8.1-fpm.sock.

Enable the site

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t      # ALWAYS test before reloading
sudo systemctl reload nginx

2.6SSL Certificates Path A Required

Option 1: Cloudflare handles SSL (easiest) Recommended

If DNS is Proxied: Cloudflare Dashboard → SSL/TLS → set mode to Full. Done. No terminal needed.

Option 2: Let's Encrypt Optional

Temporarily set Cloudflare DNS to grey cloud (DNS only), then:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run    # test auto-renewal

Switch Cloudflare back to Proxied after. Set SSL mode to Full (strict).

2.7Install WordPress Path A Required

cd /var/www/example.com/public_html
sudo wget https://wordpress.org/latest.tar.gz
sudo tar -xzf latest.tar.gz
sudo mv wordpress/* .
sudo rm -rf wordpress latest.tar.gz
sudo chown -R www-data:www-data /var/www/example.com/public_html
sudo find /var/www/example.com/public_html -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/public_html -type f -exec chmod 644 {} \;

Now visit your domain in a browser — the WordPress wizard appears. Enter your database details from section 2.3.

2.8Multiple Sites & Subdomains Path A Optional — when needed

Repeat the same process for each new site:

# 1. Create directory
sudo mkdir -p /var/www/newdomain.com/public_html

# 2. Copy & edit NGINX config
sudo cp /etc/nginx/sites-available/example.com /etc/nginx/sites-available/newdomain.com
sudo nano /etc/nginx/sites-available/newdomain.com
# Change: server_name, root, log paths

# 3. Enable, test, reload
sudo ln -s /etc/nginx/sites-available/newdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# 4. Add DNS in Cloudflare (browser)
# 5. Create database if WordPress (sudo mysql → CREATE DATABASE...)

Subdomains work identically — server_name blog.example.com; + A record in Cloudflare (blog → your IP).

2.9Non-WordPress Sites Path A Optional

Static HTML — simplified config (no PHP block)

server {
    listen 80;
    server_name static-site.com www.static-site.com;
    root /var/www/static-site.com/public_html;
    index index.html;
    location / { try_files $uri $uri/ =404; }
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ { expires 30d; }
}

Node.js — reverse proxy

server {
    listen 80;
    server_name app.example.com;
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_cache_bypass $http_upgrade;
    }
}

3.1Install CloudPanel Path B Required

Free, open-source hosting panel. One-time terminal install, then everything else is browser-based.

⚠️
Requires a fresh server. If you followed Path A, create a new server or rebuild.
apt update && apt -y upgrade && apt -y install curl wget sudo

MariaDB 11.4 Recommended

curl -sS https://installer.cloudpanel.io/ce/v2/install.sh -o install.sh; \
echo "19cfa702e7936a79e47812ff57d9859175ea902c62a68b2c15ccd1ebaf36caeb install.sh" | \
sha256sum -c && sudo CLOUD=hetzner DB_ENGINE=MARIADB_11.4 bash install.sh

MySQL 8.0 (alternative)

curl -sS https://installer.cloudpanel.io/ce/v2/install.sh -o install.sh; \
echo "19cfa702e7936a79e47812ff57d9859175ea902c62a68b2c15ccd1ebaf36caeb install.sh" | \
sha256sum -c && sudo CLOUD=hetzner DB_ENGINE=MYSQL_8.0 bash install.sh

Takes 5–10 minutes. Lots of text scrolling is normal.

🎉
This is the last time you need the terminal for Path B. Everything from here is browser-based.

3.2Access CloudPanel & First Login Path B Required

Open your browser: https://YOUR_SERVER_IP:8443

⚠️
Browser security warning is expected (self-signed cert). Click AdvancedProceed.
1

Required Create your admin account immediately.

2

Required Log in.

3

Recommended Enable Two-Factor Authentication: Account → Security.

🚨
Do this IMMEDIATELY. CloudPanel is public on port 8443. If you wait, someone else could create the admin account.

3.3Cloudflare DNS for CloudPanel Path B Required

All in your browser.

Admin panel access Recommended

TypeNameContentProxy
AadminYOUR_SERVER_IPDNS only (grey)

Then CloudPanel → Admin Area → Settings → set hostname to admin.example.com.

Your websites Required

TypeNameContentProxy
A@YOUR_SERVER_IPProxied (orange)
AwwwYOUR_SERVER_IPProxied (orange)

3.4Add a WordPress Site Path B Required

All in the browser:

1

CloudPanel → SitesAdd SiteCreate a WordPress Site.

2

Enter domain (www.example.com), site title, admin user/password, site user/password.

3

Click Create. CloudPanel auto-installs WordPress, creates the database, and configures NGINX.

3.5Add a PHP or Static Site Path B Optional

Sites → Add Site → choose: PHP App, Node.js, Python, or Static HTML. Enter domain, configure, Create. Upload files via SFTP (e.g. FileZilla) using the site user credentials.

3.6Subdomains Path B Optional

Two steps, both in browser:

1

Cloudflare: A record — blog → YOUR_SERVER_IP, Proxied.

2

CloudPanel: Sites → Add Site → blog.example.com → choose type → Create.

3.7Multiple Domains Path B Optional

1

Cloudflare: Add domain with A records (@, www) → your server IP.

2

CloudPanel: Sites → Add Site → www.otherdomain.com → configure → Create.

3.8SSL Certificates Path B Required

CloudPanel → your site → SSL/TLS tab → ActionsNew Let's Encrypt Certificate → Create and Install.

⚠️
If Cloudflare DNS is Proxied, Let's Encrypt can't verify via HTTP. Either: (1) temporarily switch to grey cloud, issue cert, switch back, or (2) just use Cloudflare's SSL (set mode to "Full") and skip Let's Encrypt.

3.9Backups & Snapshots Path B Recommended

What gets backed up?

Hetzner Snapshots/Backups — capture the entire server: all files, databases, configs, everything. Like taking a photo of the whole machine. Stored on Hetzner's infrastructure.

Database exports — just the database (posts, pages, settings). Stored as a .sql file wherever you put it.

For maximum safety: use Hetzner Backups for "everything" protection, and periodically export databases to a separate location (your local computer, Google Drive, S3, etc.).

Hetzner automatic backups (browser) Recommended

Hetzner Cloud Console → your server → BackupsEnable. Costs ~20% of server price. Creates daily full snapshots.

CloudPanel + Hetzner API snapshots (browser) Optional

1

Hetzner → Security → API Tokens → Generate (Read & Write).

2

CloudPanel → Admin Area → Hetzner → Settings → paste token.

3

Set frequency (e.g. every 6 hours) and retention.

Manual database backup (terminal) Optional

# Export a database to a .sql file
mysqldump -u root -p wordpress_db > ~/backup_$(date +%F).sql

# Download to your computer (run on YOUR machine, not the server)
scp deploy@YOUR_SERVER_IP:~/backup_*.sql ~/Downloads/

How often? Daily for active sites. Weekly minimum. Always before making big changes (updates, plugin installs, migrations).

Uploading Files to Your Server (SFTP) Recommended — Know This

SFTP (Secure File Transfer Protocol) is how you upload files to your server — themes, plugins, scripts, images, custom code. It works like drag-and-drop in a visual app. The most popular free SFTP client is FileZilla.

Install FileZilla

Download from filezilla-project.org (free, works on Mac/Windows/Linux). Install it like any other app.

Connect to your server

Open FileZilla and enter the connection details at the top:

Host: sftp://YOUR_SERVER_IP (note the sftp:// prefix — important)

Username: your site user (for CloudPanel, this is the site user you created when adding a site; for Path A, use deploy)

Password: the password for that user

Port: 22

Click Quickconnect. Accept the host key warning the first time. You'll see your local files on the left and your server files on the right.

Where to upload files

WhatPath (CloudPanel)Path (Manual / Path A)
WordPress files/home/cloudpanel/htdocs/www.example.com/var/www/example.com/public_html
WordPress themes.../wp-content/themes/.../wp-content/themes/
WordPress plugins.../wp-content/plugins/.../wp-content/plugins/
Custom scripts/home/cloudpanel/htdocs/www.example.com/scripts//var/www/example.com/scripts/

Drag files from the left panel (your computer) to the right panel (the server). FileZilla shows upload progress at the bottom.

💡
CloudPanel also has a File Manager in the browser for quick edits. Go to your site → File Manager. It's basic but works for editing config files or uploading small files without opening FileZilla.
⚠️
Permission issues after upload? If WordPress can't read files you uploaded, it's a permissions problem. For CloudPanel, the site user owns the files automatically. For Path A, you may need to run: sudo chown -R www-data:www-data /var/www/example.com/public_html

Scheduled Tasks / Cron Jobs Optional — When Needed

Cron jobs let you run tasks automatically on a schedule — daily data refreshes, weekly reports, hourly API calls, database cleanups, or anything else you can express as a command.

WordPress scheduled tasks

WordPress has its own built-in scheduling system (WP-Cron). For most WordPress tasks, you don't need server-level cron at all:

1

Install the WP Crontrol plugin from your WordPress admin dashboard (Plugins → Add New → search "WP Crontrol").

2

Go to Tools → Cron Events to see all scheduled tasks and add new ones.

This handles things like scheduled posts, cache clearing, backup plugin schedules, and email digests — all from the browser.

Server-level cron jobs (non-WordPress)

For anything outside WordPress — Python scripts, Node.js tasks, shell scripts, API calls — you use server cron jobs.

CloudPanel (browser) Path B

CloudPanel has a built-in cron manager:

1

CloudPanel → your site → Cron Jobs tab.

2

Click Add Cron Job.

3

Set the schedule and the command:

Minute: 0 · Hour: 6 · Day: * · Month: * · Weekday: * → runs at 6:00 AM every day

Minute: */30 · Hour: * · Day: * · Month: * · Weekday: * → runs every 30 minutes

Common cron job examples

TaskCommand
Run a Python scriptpython3 /home/cloudpanel/htdocs/www.example.com/scripts/refresh.py
Run a Node.js scriptnode /home/cloudpanel/htdocs/www.example.com/scripts/sync.js
Run a bash/shell scriptbash /home/cloudpanel/htdocs/www.example.com/scripts/cleanup.sh
Hit a URL / webhookcurl -s https://api.example.com/refresh
Database backupmysqldump -u root -p'PASSWORD' wordpress_db > /home/cloudpanel/backups/db_$(date +\%F).sql

Manual cron (terminal) Path A

If you're on Path A or want to manage cron from the terminal:

# Open the cron editor
crontab -e

# Add a line at the bottom. Format: minute hour day month weekday command
# Example: run a Python script every day at 6am
0 6 * * * python3 /var/www/example.com/scripts/refresh.py

# Example: run every 30 minutes
*/30 * * * * curl -s https://api.example.com/webhook

# Save and exit (Ctrl+X → Y → Enter in nano)
💡
Need a Python library? If your script uses something like requests or pandas, SSH in once and run sudo pip3 install requests pandas. This is a one-time setup — the cron job will use them automatically after that.
⚠️
Use full paths in cron. Don't write python3 script.py — write python3 /full/path/to/script.py. Cron runs in a minimal environment and doesn't know where your files are unless you tell it explicitly.

Email & Contact Forms Recommended — Read This

This catches almost everyone off guard: your Hetzner server cannot reliably send emails out of the box. If you install a WordPress contact form plugin (WPForms, Contact Form 7, etc.) and test it, the emails will either never arrive or land in spam.

Why?

Hetzner servers don't come with a mail server configured, and even if they did, emails from new/unknown servers are almost always flagged as spam by Gmail, Outlook, etc. You need a dedicated email service to send reliably.

The fix: use an SMTP plugin + email service

1

Choose an email sending service (all have free tiers):

Brevo (formerly Sendinblue) — 300 emails/day free. Easy setup. Good for most sites.

Mailgun — 1,000 emails/month free (on Flex plan). Popular with developers.

Gmail SMTP — use your existing Gmail. Limited to ~500/day. Simple but requires app passwords.

Amazon SES — 62,000 emails/month free (if sending from EC2, though you'd need to set it up separately from Hetzner). Very cheap beyond that.

2

Install a WordPress SMTP plugin. Go to Plugins → Add New → search for "WP Mail SMTP" (by WPForms). Install and activate.

3

Configure it. WP Mail SMTP → Settings → choose your email service → enter the API key or SMTP credentials from step 1.

4

Test it. WP Mail SMTP → Tools → Email Test → send a test email to yourself. Check it arrives in your inbox (not spam).

💡
This applies to ALL WordPress emails — not just contact forms. Password resets, order confirmations (WooCommerce), notification emails, everything. Without an SMTP plugin, none of these will work reliably.
⚠️
This is for sending email FROM your site. If you want a proper email address like hello@yourdomain.com for receiving and sending personal/business email, that's a separate service entirely — use Google Workspace, Zoho Mail (free tier), or Cloudflare Email Routing (free forwarding). Don't try to run your own mail server on Hetzner unless you really know what you're doing.

Reboots & Maintenance Recommended — Read This

Servers occasionally need to restart — after a kernel update, if something hangs, or during Hetzner maintenance windows. Here's what to know.

Do my sites come back automatically?

Yes — if services are "enabled" (which they are if you followed this guide). NGINX, PHP-FPM, MariaDB, CloudPanel, and Fail2Ban all start automatically on boot. Your websites will be back online within 30–60 seconds of a reboot.

⚠️ Exception: if you're running a Node.js app or custom process that you started manually (e.g. node app.js), it will NOT restart automatically. You'd need a process manager like PM2 to handle that. CloudPanel's Node.js sites handle this for you.

How to reboot

# Graceful reboot (from terminal or Web Console)
sudo reboot

Or from the browser: Hetzner Cloud Console → your server → PowerReboot (soft/graceful) or Reset (hard/forced — use only if server is unresponsive).

Hetzner scheduled maintenance

Hetzner occasionally performs maintenance on their physical hardware. They will email you in advance (usually 1–2 weeks notice). During maintenance, your server may be migrated to another physical host — this causes a brief reboot (usually under 5 minutes). Your data and IP address stay the same. No action needed from you.

When to manually reboot

🔄 After a kernel update — if apt upgrade says a reboot is required, you'll see a message. Reboot when convenient.

🔄 If the server is unresponsive — try the Hetzner Cloud Console first. If even the Web Console doesn't respond, use the Power → Reset button in the dashboard.

🔄 After changing major system settings — most changes don't need a reboot (just restart the relevant service), but kernel parameters and some package upgrades do.

💡
Check if a reboot is needed without actually rebooting: cat /var/run/reboot-required. If the file exists, a reboot is pending. If it says "No such file", you're fine.

Cloudflare Recommended Settings Recommended

All done in the Cloudflare dashboard (browser). Applies to both paths.

Security

SettingLocationValue
SSL/TLS ModeSSL/TLS → OverviewFull (strict) if you have Let's Encrypt; Full if using Cloudflare SSL only
Always Use HTTPSSSL/TLS → Edge CertificatesOn
Minimum TLS VersionSSL/TLS → Edge CertificatesTLS 1.2
Automatic HTTPS RewritesSSL/TLS → Edge CertificatesOn
Security LevelSecurity → SettingsMedium
Bot Fight ModeSecurity → BotsOn

Speed

SettingLocationValue
Auto MinifySpeed → OptimizationJS, CSS, HTML — all on
Brotli CompressionSpeed → OptimizationOn

Caching

SettingLocationValue
Caching LevelCaching → ConfigurationStandard
Browser Cache TTLCaching → ConfigurationRespect Existing Headers

Network

SettingLocationValue
HTTP/3 (QUIC)NetworkOn
WebSocketsNetworkOn
💡
"Under Attack" mode: If you're being DDoS'd, Security → Settings → enable "Under Attack Mode". Shows a challenge page to filter bots. Disable when attack stops.

Test Everything Works Required

Before calling it done, run through this checklist:

Go-Live Verification

💡
DNS not working yet? New DNS records can take up to 24–48 hours to propagate worldwide, though Cloudflare is usually under 5 minutes. Be patient. Use dnschecker.org to check propagation status.

Common Mistakes Recommended — Read This

These will save you hours of frustration. Every one of these is something real beginners hit regularly.

DNS not propagated yet
You added DNS records but the site doesn't load. Fix: wait 5–30 minutes (Cloudflare is fast). Check at dnschecker.org. Don't keep changing records — that resets the timer.
Cloudflare proxy breaking Let's Encrypt
Let's Encrypt needs to reach your server directly. Fix: temporarily set DNS to "DNS only" (grey cloud), issue the cert, then switch back to Proxied.
Wrong PHP socket version in NGINX config
NGINX error: "connect() failed" or 502 Bad Gateway. Fix: check php -v and make sure the socket path matches: php8.3-fpm.sock vs php8.1-fpm.sock.
Forgot to reload NGINX after config changes
You edited a config but nothing changed. Fix: always run sudo nginx -t then sudo systemctl reload nginx after any config change.
File permissions wrong (www-data vs deploy)
WordPress can't upload images, install plugins, or update. Fix: sudo chown -R www-data:www-data /var/www/yoursite/public_html. NGINX/PHP run as www-data, not your user.
Firewall blocking ports
Site doesn't load but server is running. Fix: sudo ufw status — make sure 80 and 443 are listed as ALLOW. For CloudPanel, also 8443.
"Too many redirects" loop
Cloudflare SSL set to "Flexible" while your server also forces HTTPS. Fix: set Cloudflare SSL to "Full" (not Flexible).
Editing the wrong NGINX file
Files in sites-available/ are templates. Only files linked (symlinked) in sites-enabled/ are active. Always check both.
Running commands without sudo
Error: "Permission denied". Fix: add sudo before the command. If logged in as root, sudo isn't needed.

How to Reconnect Later Recommended — Save This

When you close your terminal and come back tomorrow, here's how to get back in:

# From your local terminal:
ssh deploy@YOUR_SERVER_IP

# Or if you didn't set up a separate user:
ssh root@YOUR_SERVER_IP

📌 Your server IP does not change unless you delete and recreate the server. You can always find it on the Hetzner Cloud Console dashboard.

📌 The Hetzner Web Console is always available as a fallback — just click the terminal icon on your server's page in the dashboard.

📌 Tip: bookmark ssh deploy@YOUR_IP in a note on your computer so you don't have to look it up every time.

Scaling Later Optional — Read When Ready

What happens when your sites grow? You have three options, roughly in order of effort:

📈 Upgrade your server (vertical scaling) — in the Hetzner dashboard, click your server → Rescale. Pick a bigger plan. Takes a quick reboot, no data loss. This handles most growth scenarios.

🌐 Use Cloudflare's CDN more aggressively — you're already using it. Enable "Cache Everything" page rules for static sites. This offloads traffic from your server. Free.

🖥️ Add more servers (horizontal scaling) — for high-traffic sites, you can put a load balancer in front of multiple servers. This is advanced and usually only needed if you're getting thousands of concurrent visitors. Hetzner offers load balancers from ~€6/month.

🗄️ Separate database server — move MySQL/MariaDB to its own server for better performance. Advanced, but straightforward with Hetzner's private networking.

For most sites, upgrading the server size is all you'll ever need. Don't over-engineer until you have the traffic to justify it.

Troubleshooting

Error 521 (Web server is down)

Check NGINX: sudo systemctl status nginx. Check firewall: sudo ufw status. Check IP in Cloudflare matches your server.

Error 522 (Connection timed out)

Server overloaded or ports blocked. Check with htop and sudo ufw status.

Error 525/526 (SSL handshake failed)

Cloudflare SSL mode mismatch. Use "Full" if no server cert, "Full (strict)" if you have Let's Encrypt.

Can't connect via SSH

Use the Hetzner Web Console (always works). Check SSH is running: sudo systemctl status sshd. Check firewall: sudo ufw status.

NGINX shows wrong site

Each site needs a unique server_name. Run sudo nginx -t to check for config errors.

WordPress white screen of death

Check PHP error log: sudo tail -50 /var/log/nginx/example.com.error.log. Usually a plugin conflict or memory limit. Edit wp-config.php and add define('WP_DEBUG', true); to see the real error.

Command Cheat Sheet

TaskCommand
Connect to serverssh deploy@YOUR_SERVER_IP
Update systemsudo apt update && sudo apt upgrade -y
Restart NGINXsudo systemctl restart nginx
Reload NGINX (no downtime)sudo systemctl reload nginx
Test NGINX configsudo nginx -t
Restart PHP-FPMsudo systemctl restart php8.3-fpm
Restart MariaDBsudo systemctl restart mariadb
Check disk spacedf -h
Check memoryfree -h
Live process monitorhtop (press q to exit)
NGINX error logsudo tail -50 /var/log/nginx/error.log
Firewall statussudo ufw status verbose
Fail2Ban statussudo fail2ban-client status sshd
Renew SSL certssudo certbot renew
Backup databasemysqldump -u root -p db_name > backup.sql
Restore databasemysql -u root -p db_name < backup.sql
Cancel current commandCtrl+C
Exit nano editorCtrl+X → Y → Enter
Reboot serversudo reboot
🎉
You're all set! You now have a complete, secured Hetzner server hosting one or more websites with Cloudflare protection. Remember: keep the server updated, test NGINX configs before reloading, and back up regularly. If anything breaks, check Common Mistakes and Troubleshooting above, or use the Hetzner Web Console as a safety net.