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.
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
01Prerequisites
| What | Why | Cost |
|---|---|---|
| Hetzner account | Your server lives here | From ~€4.50/mo |
| Cloudflare account | DNS, CDN, DDoS protection | Free plan works |
| A domain name | Already on Cloudflare | Varies |
| A way to type commands | Only for initial setup — see below | Free |
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.
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.
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.
| Location | Code | Region | Good for |
|---|---|---|---|
| Falkenstein, DE | fsn1 | Europe | EU audiences, cheapest historically |
| Nuremberg, DE | nbg1 | Europe | EU audiences |
| Helsinki, FI | hel1 | Europe (North) | Nordics / Baltics / Russia-adjacent |
| Ashburn, VA, US | ash | US East | US / Canada East, Latin America |
| Hillsboro, OR, US | hil | US West | US West, West Canada |
| Singapore | sin | Asia-Pacific | SEA, South Asia, Australia |
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.
| Line | Tier | CPU | Notes | Use when |
|---|---|---|---|---|
| CX | Shared | Intel x86 | Classic all-rounder | Default choice for WordPress / PHP / Node |
| CPX | Shared | AMD x86 | Faster per-core than CX, small premium | Better perf on the same budget |
| CAX | Shared | Ampere ARM64 | Cheapest, excellent perf/€ | Only if your stack runs on ARM (most do — Ubuntu, NGINX, PHP, MariaDB, Node all fine) |
| CCX | Dedicated | AMD EPYC | Guaranteed vCPUs, 2–4× price | Sustained CPU load, game/app servers, CI |
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?
| Setup | Works for visitors? | Cost | Recommended? |
|---|---|---|---|
| IPv4 only | ✅ Everyone | ~€0.60/mo | OK — simple |
| IPv4 + IPv6 (dual-stack) | ✅ Everyone (IPv6 users get IPv6, others get IPv4) | ~€0.60/mo | ✅ Best — 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 |
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.
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 → Networks → Create 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
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.
Go to console.hetzner.cloud → Sign Up.
Enter email, password, verify email.
Hetzner may request ID verification (passport/licence). Normal — takes a few hours to a day.
Add a payment method under Billing.
Recommended Enable Two-Factor Authentication in account settings.
1.2Generate SSH Keys Recommended
SSH keys let your computer prove its identity cryptographically instead of using a password.
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.pubCopy the entire output line.
Add to Hetzner (browser)
Hetzner Cloud Console → your project → Security → SSH Keys → Add SSH Key → paste your public key.
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.
Click Add Server.
Required Location: Choose closest to your audience (Falkenstein, Nuremberg, Helsinki, Ashburn, or Hillsboro).
Required Image: Select Ubuntu 24.04.
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.
Required Networking: Leave defaults (Public IPv4 + IPv6).
Recommended SSH Key: Select your key from 1.2. If you skipped, leave blank.
Optional Firewall: Create a Cloud Firewall (SSH 22, HTTP 80, HTTPS 443). Can be done later via UFW.
Optional Name: e.g. web-server-01.
Click Create & Buy Now. Ready in ~30 seconds.
168.119.xxx.xxx) — you'll need it everywhere.1.4Connect to Your Server Required
Method A: Hetzner Web Console (browser)
Click your server name in the Hetzner dashboard.
Click the terminal icon (>_) — top-right of the page.
A black terminal opens in a new tab. Click inside it, press Enter.
Log in as root. If you used SSH keys, no password needed. If you skipped SSH keys, enter the password Hetzner emailed you.
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
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
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
Disable root login & password auth Recommended
nano /etc/ssh/sshd_configFind and change (Ctrl+W to search):
PermitRootLogin no PasswordAuthentication no
Remove # from the beginning of the line if present. Save: Ctrl+X → Y → Enter.
systemctl restart sshdIf 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
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
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
| Type | Name | Content | Proxy | TTL |
|---|---|---|---|---|
| A | @ | YOUR_SERVER_IP | Proxied (orange) | Auto |
| A | www | YOUR_SERVER_IP | Proxied (orange) | Auto |
Subdomains
| Type | Name | Content | Proxy | TTL |
|---|---|---|---|---|
| A | blog | YOUR_SERVER_IP | Proxied | Auto |
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.
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.comPaste:
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";
}
}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.
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.
3.2Access CloudPanel & First Login Path B Required
Open your browser: https://YOUR_SERVER_IP:8443
Required Create your admin account immediately.
Required Log in.
Recommended Enable Two-Factor Authentication: Account → Security.
3.3Cloudflare DNS for CloudPanel Path B Required
All in your browser.
Admin panel access Recommended
| Type | Name | Content | Proxy |
|---|---|---|---|
| A | admin | YOUR_SERVER_IP | DNS only (grey) |
Then CloudPanel → Admin Area → Settings → set hostname to admin.example.com.
Your websites Required
| Type | Name | Content | Proxy |
|---|---|---|---|
| A | @ | YOUR_SERVER_IP | Proxied (orange) |
| A | www | YOUR_SERVER_IP | Proxied (orange) |
3.4Add a WordPress Site Path B Required
All in the browser:
CloudPanel → Sites → Add Site → Create a WordPress Site.
Enter domain (www.example.com), site title, admin user/password, site user/password.
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:
Cloudflare: A record — blog → YOUR_SERVER_IP, Proxied.
CloudPanel: Sites → Add Site → blog.example.com → choose type → Create.
3.7Multiple Domains Path B Optional
Cloudflare: Add domain with A records (@, www) → your server IP.
CloudPanel: Sites → Add Site → www.otherdomain.com → configure → Create.
3.8SSL Certificates Path B Required
CloudPanel → your site → SSL/TLS tab → Actions → New Let's Encrypt Certificate → Create and Install.
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 → Backups → Enable. Costs ~20% of server price. Creates daily full snapshots.
CloudPanel + Hetzner API snapshots (browser) Optional
Hetzner → Security → API Tokens → Generate (Read & Write).
CloudPanel → Admin Area → Hetzner → Settings → paste token.
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
| What | Path (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.
sudo chown -R www-data:www-data /var/www/example.com/public_htmlScheduled 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:
Install the WP Crontrol plugin from your WordPress admin dashboard (Plugins → Add New → search "WP Crontrol").
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:
CloudPanel → your site → Cron Jobs tab.
Click Add Cron Job.
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
| Task | Command |
|---|---|
| Run a Python script | python3 /home/cloudpanel/htdocs/www.example.com/scripts/refresh.py |
| Run a Node.js script | node /home/cloudpanel/htdocs/www.example.com/scripts/sync.js |
| Run a bash/shell script | bash /home/cloudpanel/htdocs/www.example.com/scripts/cleanup.sh |
| Hit a URL / webhook | curl -s https://api.example.com/refresh |
| Database backup | mysqldump -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)
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.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
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.
Install a WordPress SMTP plugin. Go to Plugins → Add New → search for "WP Mail SMTP" (by WPForms). Install and activate.
Configure it. WP Mail SMTP → Settings → choose your email service → enter the API key or SMTP credentials from step 1.
Test it. WP Mail SMTP → Tools → Email Test → send a test email to yourself. Check it arrives in your inbox (not spam).
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 → Power → Reboot (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.
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
| Setting | Location | Value |
|---|---|---|
| SSL/TLS Mode | SSL/TLS → Overview | Full (strict) if you have Let's Encrypt; Full if using Cloudflare SSL only |
| Always Use HTTPS | SSL/TLS → Edge Certificates | On |
| Minimum TLS Version | SSL/TLS → Edge Certificates | TLS 1.2 |
| Automatic HTTPS Rewrites | SSL/TLS → Edge Certificates | On |
| Security Level | Security → Settings | Medium |
| Bot Fight Mode | Security → Bots | On |
Speed
| Setting | Location | Value |
|---|---|---|
| Auto Minify | Speed → Optimization | JS, CSS, HTML — all on |
| Brotli Compression | Speed → Optimization | On |
Caching
| Setting | Location | Value |
|---|---|---|
| Caching Level | Caching → Configuration | Standard |
| Browser Cache TTL | Caching → Configuration | Respect Existing Headers |
Network
| Setting | Location | Value |
|---|---|---|
| HTTP/3 (QUIC) | Network | On |
| WebSockets | Network | On |
Test Everything Works Required
Before calling it done, run through this checklist:
Go-Live Verification
Common Mistakes Recommended — Read This
These will save you hours of frustration. Every one of these is something real beginners hit regularly.
php -v and make sure the socket path matches: php8.3-fpm.sock vs php8.1-fpm.sock.sudo nginx -t then sudo systemctl reload nginx after any config change.sudo chown -R www-data:www-data /var/www/yoursite/public_html. NGINX/PHP run as www-data, not your user.sudo ufw status — make sure 80 and 443 are listed as ALLOW. For CloudPanel, also 8443.sites-available/ are templates. Only files linked (symlinked) in sites-enabled/ are active. Always check both.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
| Task | Command |
|---|---|
| Connect to server | ssh deploy@YOUR_SERVER_IP |
| Update system | sudo apt update && sudo apt upgrade -y |
| Restart NGINX | sudo systemctl restart nginx |
| Reload NGINX (no downtime) | sudo systemctl reload nginx |
| Test NGINX config | sudo nginx -t |
| Restart PHP-FPM | sudo systemctl restart php8.3-fpm |
| Restart MariaDB | sudo systemctl restart mariadb |
| Check disk space | df -h |
| Check memory | free -h |
| Live process monitor | htop (press q to exit) |
| NGINX error log | sudo tail -50 /var/log/nginx/error.log |
| Firewall status | sudo ufw status verbose |
| Fail2Ban status | sudo fail2ban-client status sshd |
| Renew SSL certs | sudo certbot renew |
| Backup database | mysqldump -u root -p db_name > backup.sql |
| Restore database | mysql -u root -p db_name < backup.sql |
| Cancel current command | Ctrl+C |
| Exit nano editor | Ctrl+X → Y → Enter |
| Reboot server | sudo reboot |