Skip to content
Farhan Munim Analyst & developer Unavailable for new projects
← Blog

· Guides · Coolify  · 52 min read

Hetzner Cloud — Complete Hosting Guide

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

Contents 69 chapters
  • (Required) You must do this — skipping will break things
  • (Recommended) Strongly advised for security / usability
  • (Optional) Nice to have — skip to keep things simple

Getting Started

00 Overview & 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

01 Prerequisites #

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

02 Choose 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 built for traditional hosting (WordPress, PHP apps, static sites). One-time terminal install, then everything else is browser-based. Best if you mainly want to host WordPress or PHP sites.

🚀 Path C — Coolify (Self-Hosted PaaS)

Open-source, self-hosted alternative to Heroku / Vercel / Netlify. Deploys apps from a Git repo (Node, Python, PHP, static sites), databases, and Docker containers — all from a browser UI. Best if you want to deploy modern web apps or one-click tools like Ghost, n8n, Plausible, etc.

03 How 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.

💬 What’s a “terminal”? A plain text window where you type commands and the computer types back. No mouse, no icons — the whole interaction is lines of text. Don’t worry: this guide tells you exactly what to type.

🔐 What’s SSH? SSH (“Secure Shell”) is how your computer talks to your server over the internet, through an encrypted tunnel. You open a terminal on your machine, SSH in, and it feels like you’re typing directly on the server.

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.

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. root is the built-in superuser (always admin, no sudo needed). A sudo user (like deploy) is a normal user who’s allowed to temporarily borrow admin powers by typing sudo. Using a sudo user day-to-day is safer than logging in as root.

⌨️ 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.


Hetzner Concepts

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.

06 Locations & 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

07 Server 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

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.

08 Images — 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 manually. Kept forever until you delete them. Good for cloning a server or migrating between locations / architectures.

Backups — daily snapshots Hetzner takes automatically. Rolling window (only the last 7 are kept). Enable on the server’s Backups tab, ~20% of server price. The beginner-friendly “set and forget” option.

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. 203.0.113.10). 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. 2001:db8: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/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 203.0.113.10.

AAAA record (“quad-A”) → points a domain to an IPv6 address. Example: example.com AAAA 2001:db8: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@203.0.113.10

# Over IPv6 (brackets not needed for ssh, but needed in URLs)
ssh deploy@2001:db8: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.

10 Private 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.

11 Volumes (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.

12 Placement 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.

13 Cloud-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

14 Labels 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.


Part 1 — Server Setup

1.1 Create Your Hetzner Account Required #

Entirely in your browser.

  1. Go to console.hetzner.cloud → Sign 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.

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.pub

Copy the entire output line.

Add to Hetzner (browser)

Hetzner Cloud Console → your project → Security → SSH Keys → Add 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.3 Create 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.

  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.

1.4 Connect 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.

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.

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.

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
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+X → Y → Enter.

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.

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

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
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

Select Yes. Critical patches will now auto-apply.

1.9 Basic 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

  • System updated (apt update && apt upgrade) (Required)
  • Non-root user created with sudo privileges (Recommended)
  • SSH keys set up and working (Recommended)
  • Root login disabled (PermitRootLogin no) (Recommended)
  • Password authentication disabled (if keys used) (Recommended)
  • UFW firewall enabled and active (Recommended)
  • Fail2Ban installed and running (Recommended)
  • Automatic security updates enabled (Recommended)
  • Can still SSH in (or access via Web Console) (Required)

Part 2A — Manual Stack

2.1 Install 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.2 Install PHP (Path A) Required for WordPress #

🐘 PHP is the programming language WordPress is written in. PHP-FPM (“FastCGI Process Manager”) is the background worker that actually runs PHP code when a visitor hits your site. NGINX receives the request and hands any .php file to PHP-FPM via a “socket” — a tiny internal pipe between the two programs.

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.3 Install MariaDB (Path A) Required for WordPress #

🗄️ MariaDB vs MySQL. A database is where WordPress stores your posts, users, and settings. MySQL is the classic one; MariaDB is a free, drop-in replacement by the original MySQL creators. Pick either — WordPress treats them identically. This guide uses MariaDB because it’s the Ubuntu default.

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.4 Cloudflare DNS Setup Required #

Entirely in your browser — Cloudflare Dashboard.

📖 DNS in one paragraph. DNS is the internet’s phonebook: it turns a name like example.com into an IP address like 203.0.113.10. You add records to tell the world where your site lives.

📍 A record — maps a name to an IPv4 address. This is the one you care about.

🏷️ Name field — @ means the bare domain (example.com). www means www.example.com. blog means blog.example.com (a subdomain — any prefix before your main domain).

⏱️ TTL (“time to live”) — how long other computers are allowed to cache this record. “Auto” is fine.

🌐 Nameservers — the servers that answer DNS questions for your domain. For Cloudflare to manage your DNS, your domain registrar must point at Cloudflare’s nameservers (Cloudflare shows you which ones when you add the domain).

⏳ Propagation — the wait for your new records to spread across the world. Usually a few minutes on Cloudflare; can take up to 24–48 hours in rare cases.

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.

2.5 NGINX Virtual Hosts (Path A) Required #

🏠 What’s a virtual host? One server, many websites. A virtual host (or “server block” in NGINX) is a config file that says: “when a request comes in for example.com, serve files from this folder.” One file per site — that’s how you host multiple domains on a single IP.

📁 Two folders, one link. NGINX reads configs from sites-enabled/. You write them in sites-available/ and create a symbolic link (like a shortcut) into sites-enabled/ to activate them. Unlink to deactivate without deleting.

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";
    }
}

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.6 SSL Certificates (Path A) Required #

🔒 SSL certificate = the padlock. It encrypts traffic between visitors and your site, and it’s what makes URLs show https:// instead of http://. Browsers now warn on any site without one.

🆓 Let’s Encrypt is a free certificate authority. You prove you own the domain, it hands you a cert, and it auto-renews every 90 days. Certbot is the tool that does all that talking for you.

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.7 Install 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.8 Multiple 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.9 Non-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

🔁 What’s a reverse proxy? Your Node app listens on an internal port (like 3000), which isn’t reachable from the internet. NGINX sits in front on ports 80/443, takes the public request, and quietly forwards it to your app on port 3000. The visitor only ever talks to NGINX. Same trick powers CloudPanel and Coolify under the hood.

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;
    }
}

Part 2B — CloudPanel

3.1 Install 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
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.2 Access CloudPanel & First Login (Path B) Required #

Open your browser: https://YOUR_SERVER_IP:8443

  1. (Required) Create your admin account immediately.
  2. (Required) Log in.
  3. (Recommended) Enable Two-Factor Authentication: Account → Security.

3.3 Cloudflare DNS for CloudPanel (Path B) Required #

All in your browser.

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.4 Add a WordPress Site (Path B) Required #

All in the browser:

  1. CloudPanel → Sites → Add Site → Create 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.5 Add 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.6 Subdomains (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.7 Multiple 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.8 SSL Certificates (Path B) Required #

CloudPanel → your site → SSL/TLS tab → Actions → New Let’s Encrypt Certificate → Create and Install.

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 Cloud Console → your server → Backups → Enable. 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).


Part 2C — Coolify

4.1 What is Coolify? (Path C) Required — Read This #

Coolify is a free, open-source control panel that turns your Hetzner server into your own mini “Heroku” or “Vercel”. Instead of uploading files over SFTP, you point Coolify at a Git repository (GitHub, GitLab, etc.) and it builds and deploys your app automatically — every time you push code.

What Coolify can deploy for you:

🧱 Apps from Git — Node.js, Next.js, Python, Laravel/PHP, Ruby, Go, static sites, anything with a Dockerfile.

🗄️ Databases — PostgreSQL, MySQL/MariaDB, MongoDB, Redis, KeyDB, Clickhouse, Dragonfly. One-click create, with automatic backups.

🧰 “One-click” services — WordPress, Ghost, n8n, Plausible Analytics, Umami, Uptime Kuma, Gitea, MinIO, and 100+ others from a template library.

🐳 Any Docker container — if it has a Docker image, Coolify can run it.

Official docs: coolify.io/docs

Wait — what’s Docker? What’s a container?

Coolify runs everything using Docker, so it’s worth understanding the basic idea (you don’t need to use Docker directly — Coolify hides it).

📦 A container is like a sealed lunchbox for software. Inside the box is everything the app needs to run — the code, the right version of Node or PHP, system libraries, config — all packed together. The box runs the same way on any computer, because it brings its own environment with it. No more “but it worked on my laptop”.

🏭 Docker is the tool that builds and runs these lunchboxes.

🧱 An image is the recipe / blueprint for a lunchbox. A container is a running copy of it. You can start, stop, and throw away containers freely — the image stays put, ready to make another one.

🏘️ On your Hetzner server, Coolify runs one container per app and one per database. Each one is isolated from the others — if one app crashes, the rest keep running. If you want to remove an app, Coolify deletes its container and it’s gone cleanly, no leftover files scattered across the system (which is a common pain with manual LEMP setups).

🌐 Coolify also runs a special container called Traefik (the “reverse proxy”). Traefik watches the traffic hitting your server on ports 80/443, and routes each incoming request to the correct app container based on the domain — that’s how one server can host many sites on the same IP.

4.2 Install Coolify (Path C) Required #

One-time terminal install. After this, everything is browser-based.

  1. SSH into your server (or use the Hetzner Web Console) as root.
  2. Make sure the system is up to date:
apt update && apt -y upgrade && apt -y install curl
  1. Run the official Coolify installer:
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash

This installs Docker, pulls the Coolify containers, and starts them. Expect 3–8 minutes of scrolling text — that’s normal.

  1. When it finishes, the script prints a URL like http://YOUR_SERVER_IP:8000. Keep that handy.

Wait — what’s a port? What’s a firewall?

🚪 A port is like a numbered door on your server. Your server has one address (the IP), but thousands of numbered doors. Different services listen at different doors: web traffic uses door 80 (HTTP) and 443 (HTTPS), SSH uses door 22, Coolify’s dashboard uses door 8000, and so on.

🛡️ A firewall (UFW) is a bouncer that decides which doors are open to the outside world. By default in Part 1 we told UFW to only open doors 22, 80, and 443 — everything else is blocked.

✅ “Allow port X” = tell the bouncer to unlock door X. “Close port X” = lock it again. You do this with one command; nothing dangerous happens if you type it wrong — you can just rerun it.

4.3 Access Coolify & First Login (Path C) Required #

Open your browser: http://YOUR_SERVER_IP:8000

  1. (Required) You’ll see a “Register” screen. Create your admin account (name, email, strong password).
  2. (Required) Log in.
  3. (Recommended) Top-right avatar → Profile → enable Two-Factor Authentication.

4.4 Cloudflare DNS for Coolify (Path C) Required #

You want two kinds of DNS records: one for the Coolify dashboard itself, and one (per app) for each site you deploy.

TypeNameContentProxy
AcoolifyYOUR_SERVER_IPDNS only (grey)

Then in Coolify: Settings → Instance Settings → set Instance’s Domain to https://coolify.example.com. Coolify will auto-issue a Let’s Encrypt certificate and redirect port 8000 to HTTPS.

Your apps (Required)

For each app you plan to deploy, add a record like:

TypeNameContentProxy
AappYOUR_SERVER_IPDNS only (grey) during SSL issue
A@YOUR_SERVER_IPDNS only (grey) during SSL issue

4.5 Connect Your Server (Path C) Required #

Coolify is already managing the server it’s installed on — it calls it localhost. You don’t need to add it manually. Check it:

  1. Left sidebar → Servers → you should see localhost with a green “Reachable” / “Usable” status.
  2. (Optional) Later, to deploy to another Hetzner server, click + Add, paste its IP, and give Coolify an SSH key it can use to connect.

Coolify groups things in Projects (e.g. “My Blog”, “Client ACME”) and each project has Environments (production, staging, etc.). Apps, databases, and services live inside an environment.

  1. Left sidebar → Projects → + Add → name it (e.g. “My Blog”) → Create.
  2. Open the project → you’ll see a default production environment. Click into it.

4.7 Deploy Your First App from Git (Path C) Required #

This is Coolify’s superpower: deploying a site straight from a GitHub repo.

  1. Inside your environment → + New → Application.
  2. Choose a source:
    • Public Repository — simplest: paste any public GitHub/GitLab URL.
    • GitHub App — for private repos and auto-deploy on push. Click Add GitHub App and follow the OAuth flow.
  3. Coolify detects the stack (Next.js, Node, static, Laravel, Dockerfile, etc.). Pick the right Build Pack:
    • Nixpacks (default) — zero-config; works for most Node/Python/PHP/Go projects.
    • Static — for plain HTML / built front-ends.
    • Dockerfile — if your repo has one, this is the most predictable option.
  4. Set the Domain field to something like https://app.example.com (matching the DNS record from 4.4). Coolify handles SSL automatically.
  5. (Optional) Open the Environment Variables tab and add any secrets your app needs (DATABASE_URL, API_KEY, etc.).
  6. Click Deploy. Watch the build log in real time. First build can take 2–10 minutes; subsequent deploys are much faster thanks to caching.

4.8 One-Click Apps (WordPress, Ghost, n8n…) (Path C) Optional #

You don’t need a Git repo for popular tools — Coolify ships templates.

  1. Inside your environment → + New → Service.
  2. Browse or search the template list: WordPress, Ghost, n8n, Plausible, Umami, Uptime Kuma, Gitea, MinIO, Directus, etc.
  3. Pick a template → set the domain (e.g. https://blog.example.com) → Deploy. The associated database is created and wired up automatically.

4.9 Databases (Path C) Optional #

If your custom app needs its own database:

  1. Environment → + New → Database → choose PostgreSQL / MySQL / MariaDB / MongoDB / Redis → Create.
  2. Coolify generates credentials and an internal URL (something like postgres://user:pass@postgres-xyz:5432/app).
  3. Copy that URL into your app’s environment variables (e.g. DATABASE_URL) — your app and database are now on the same internal Docker network, no public port needed.

4.10 SSL Certificates (Path C) Required #

Coolify ships with a Traefik reverse proxy that issues and renews Let’s Encrypt certificates automatically — you just need to:

  1. Use https:// in the app’s Domain field.
  2. Point the domain’s A record at your server IP.
  3. Keep the DNS record unproxied (grey cloud) in Cloudflare while Coolify issues the cert. Switch to Proxied afterwards if you want.

If a cert fails to issue, open the app → Logs tab → Proxy to see Traefik’s reason (usually DNS not yet propagated, or Cloudflare proxy enabled too early).

Same advice as the other paths: enable Hetzner Cloud Backups on the server (Console → server → Backups → Enable). ~20% of the server price, daily full snapshots — the simplest safety net.

  1. Open any database in Coolify → Backups tab.
  2. Turn on Scheduled Backups and set a cron (e.g. 0 3 * * * = every day at 3 AM).
  3. (Recommended) Add an S3-compatible storage destination (Hetzner Object Storage, Cloudflare R2, Backblaze B2) in Settings → S3 Storages, then select it in the backup config. Off-server copies survive server loss.

Git = backup for app code (Tip)

Because apps deploy from Git, your source code is already backed up in GitHub/GitLab. You only need to worry about backing up databases and uploaded user files (volumes).

This part confuses everyone coming from traditional hosting, so read slowly.

🚫 There is no /var/www/html to SFTP into. With CloudPanel or a manual LEMP stack, you upload files into a folder on the server. With Coolify, the files live inside the container (the sealed lunchbox), not in a normal server folder you can browse. That’s by design — it’s what makes deploys repeatable.

So “editing files on the server” is replaced by three patterns, depending on what you’re trying to do.

Pattern 1 — App code (HTML, PHP, JS, theme files…)

Don’t edit it on the server. Edit it on your computer, git push, and Coolify rebuilds automatically. If you connected the GitHub App (4.7), the site updates within a minute of your push. This is the whole point of Coolify.

Pattern 2 — User uploads & persistent data (WordPress uploads, database files, avatars…)

These live in Docker volumes. A volume is a folder on the real server that Coolify mounts into the container — so the data survives when the container is rebuilt.

Each app in Coolify has a Storage tab where you can see and add volume mounts. For WordPress, for example, there’s a volume mounted at /var/www/html/wp-content/uploads so media uploads persist across deploys.

Pattern 3 — “I just need to look at / upload one file”

Two easy options from the Coolify UI:

  1. Terminal tab — open your app → Terminal tab → a shell inside the container opens in your browser. ls, cat, nano work just like normal.
  2. SFTP directly to the host server — connect to the Hetzner server with FileZilla (see the Uploading Files section) as your deploy user. Volume data lives under /var/lib/docker/volumes/ (requires root to read). Prefer Pattern 1 whenever possible.

Where does each thing actually live?

Your source code — GitHub / GitLab (and cached inside the container image).

Build outputs (node_modules, compiled files) — inside the container; disposable, rebuilt every deploy.

Uploaded files / databases — Docker volumes on the host at /var/lib/docker/volumes/.

App logs — in Coolify’s UI: app → Logs tab. No need to SSH.

Coolify’s own config — /data/coolify/ on the server.

4.13 Security in Coolify (Path C) Required — Read This #

Containers help a lot (each app is isolated), but they don’t replace basic server hygiene. Here’s the beginner-friendly checklist.

Things you already did in Part 1 still apply

UFW firewall, Fail2Ban, automatic updates, SSH key login, disabled root password login — all of that protects Coolify too. Don’t skip Part 1 just because you’re using a control panel.

Coolify-specific checklist

  • Admin account registered immediately after install (first visitor becomes admin!) (Required)
  • Strong, unique password for the Coolify admin account (Required)
  • Two-Factor Authentication enabled (Profile → 2FA) (Recommended)
  • Coolify dashboard moved off http://IP:8000 onto https://coolify.example.com (section 4.4) (Recommended)
  • After the dashboard works on your HTTPS domain, re-lock door 8000 (it’s no longer needed — visitors use the domain now). See command below. (Recommended)
  • Firewall only has these doors open: 22 (SSH login), 80 (HTTP), 443 (HTTPS), 6001 & 6002 (Coolify live logs/terminal) (Recommended)

How to re-lock port 8000 (once the HTTPS domain works)

After you’ve finished section 4.4 and can reach Coolify at https://coolify.example.com, you don’t need the raw http://IP:8000 entrance anymore. Lock that door back up:

# Lock door 8000 again
sudo ufw delete allow 8000/tcp

# Check which doors are currently open
sudo ufw status

The second command prints a little table showing every open port. If you ever need to reopen it temporarily (e.g. your HTTPS domain is broken and you need to get back in), just run sudo ufw allow 8000/tcp again — it’s fully reversible.

Rest of the checklist

  • Databases use internal URLs only — “Public Port” toggle is OFF unless you truly need remote access (Required)
  • Secrets (API keys, DB passwords) kept in Coolify Environment Variables — never committed to Git (Required)
  • Coolify updated regularly: Settings → Check for updates → Update (Recommended)
  • Hetzner Cloud Backups enabled on the server (Recommended)

A few gotchas worth knowing

🐳 Docker can unlock doors behind the firewall’s back. If you flip the “Public Port” switch on a database or app in Coolify, Docker opens that door directly — UFW won’t stop it, even if you think the door is locked. So treat “Public Port” as a loaded gun: leave it OFF unless you genuinely need to connect from outside the server, and always set a strong password if you turn it on.

🔑 GitHub App permissions. When connecting via the GitHub App, only grant access to the repos Coolify needs to deploy — not all your repos.

📜 Don’t run random templates blindly. Coolify’s one-click services are convenient, but each one is a third-party app with its own security track record. Stick to well-known ones (WordPress, Ghost, n8n, Plausible) and keep them updated.

🧹 Old containers/images accumulate and eat disk. Occasionally run docker system prune -a on the server (or let Coolify do it via Settings → Advanced → Cleanup).


Day-to-Day

SFTP (Secure File Transfer Protocol) is how you upload files to your server — themes, plugins, scripts, images, custom code. Think of it as “Google Drive for your server”: a drag-and-drop window showing your computer on one side and the server on the other, with the same secure SSH connection underneath. 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.

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.

⏰ What’s cron? Cron is a built-in Linux scheduler — like a recurring alarm clock that runs commands for you. Each scheduled task is called a cron job.

🗓️ The five stars. A cron schedule is five fields: minute hour day month weekday. A * means “every”. So 0 6 * * * = minute 0 of hour 6, every day — i.e. 6:00 AM daily. Stuck? Paste your schedule into crontab.guru for a plain-English translation.

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)

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.

  1. Install a WordPress SMTP plugin. Go to Plugins → Add New → search for “WP Mail SMTP” (by WPForms). Install and activate.
  2. Configure it. WP Mail SMTP → Settings → choose your email service → enter the API key or SMTP credentials from step 1.
  3. Test it. WP Mail SMTP → Tools → Email Test → send a test email to yourself. Check it arrives in your inbox (not spam).

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.

Reference

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

Test Everything Works Required #

Before calling it done, run through this checklist:

Go-Live Verification

  • Site loads in browser (type your domain — do you see your site?)
  • HTTPS works (padlock icon in address bar, no warnings)
  • http:// redirects to https:// automatically
  • WordPress admin loads (yourdomain.com/wp-admin)
  • NGINX config test passes (sudo nginx -t)
  • Can still SSH in / access via Web Console
  • Firewall is active (sudo ufw status)
  • www and non-www both work (one should redirect to the other)
  • Subdomains load (if configured)

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.

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

Backups & Monitoring

Synology NAS Backup #

This section covers how to automatically back up your entire Coolify server to a Synology NAS every night. The backup runs on its own — you don’t need to do anything once it’s set up.

How it works

Every night at 4am, your server runs a script that copies all your Coolify data and Docker app data to your Synology NAS using a tool called rsync. It connects using a special SSH key so no password is needed. Everything is automated.

What gets backed up:

  • Coolify settings, config, and its own database
  • All Docker volumes — WordPress, MariaDB, Redis, Uptime Kuma, and any other apps
  • SSL certificates and proxy config

Step 1 — Install and connect Tailscale

If Tailscale is not already on your server, install it:

curl -fsSL https://tailscale.com/install.sh | sh
tailscale up

It gives you a link — open it in your browser, log in with your Tailscale account, and approve the new device. Then confirm your NAS is visible:

tailscale status

You should see your Synology listed as active. If it’s not there, the backup cannot reach the NAS.

Step 2 — Create an SSH key for the backup

This creates a special key that lets your server log into the NAS automatically without needing a password every time:

ssh-keygen -t ed25519 -f /root/.ssh/nas_push -N ""

This creates two files:

  • /root/.ssh/nas_push — the private key. Never share this.
  • /root/.ssh/nas_push.pub — the public key. This goes on the NAS.

Step 3 — Copy the public key to the NAS

First, show your public key:

cat /root/.ssh/nas_push.pub

Copy the entire line it shows. Then SSH into your NAS (replace 100.64.0.2 with your NAS’s Tailscale IP, and nas-user with your NAS username):

ssh nas-user@100.64.0.2

It will ask for your NAS password. Once logged in, add your public key:

echo "PASTE_YOUR_PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys
exit

Now test the passwordless connection works:

ssh -i /root/.ssh/nas_push nas-user@100.64.0.2 "echo test"

If it prints test with no password prompt — it worked.

Step 4 — Enable rsync on the Synology

Log into your Synology web interface and go to:

Control Panel → File Services → rsync tab

  • Tick Enable rsync service ✅
  • Leave Enable rsync account unticked ❌
  • Click Apply

Step 5 — Create the backup script

Copy and paste this entire block into your terminal — it creates the backup script in one go:

cat > /etc/cron.daily/coolify-backup << 'EOF'
#!/bin/bash
LOGFILE="/var/log/coolify-backup.log"
echo "=== Backup started: $(date) ===" >> $LOGFILE
rsync -avz -e "ssh -i /root/.ssh/nas_push" \
  /data/coolify/ \
  nas-user@100.64.0.2:/volume1/Coolify-Backup/ >> $LOGFILE 2>&1
rsync -avz -e "ssh -i /root/.ssh/nas_push" \
  /var/lib/docker/volumes/ \
  nas-user@100.64.0.2:/volume1/Coolify-Backup/docker-volumes/ >> $LOGFILE 2>&1
echo "=== Backup finished: $(date) ===" >> $LOGFILE
EOF
chmod +x /etc/cron.daily/coolify-backup

Replace nas-user and 100.64.0.2 with your own NAS username and Tailscale IP.

Step 6 — Test it

bash /etc/cron.daily/coolify-backup
tail /var/log/coolify-backup.log

You should see files being listed and a finish time at the end with no errors. The backup will now run automatically every night.

Checking the backup is working

What to checkCommand
View the backup logtail /var/log/coolify-backup.log
Check cron ran the scriptgrep -i "coolify-backup" /var/log/syslog | tail -20
Run a backup right nowbash /etc/cron.daily/coolify-backup
Check Tailscale is connectedtailscale status
Test SSH connection to NASssh -i /root/.ssh/nas_push nas-user@100.64.0.2 "echo test"
Check files on NAS from terminalssh -i /root/.ssh/nas_push nas-user@100.64.0.2 "ls -la /volume1/Coolify-Backup/"

You can also check manually in the Synology File Station — go to volume1 → Coolify-Backup and check the Modified Date column on the folders. They should show today’s date after a successful backup.


Uptime Kuma Monitoring #

Uptime Kuma is a monitoring tool. It watches your services and alerts you if something goes down. You can set it up through Coolify in a few minutes.

Install Uptime Kuma via Coolify

  1. In Coolify, click New Resource → search “uptime” → select Uptime Kuma (the plain one, no database needed).
  2. Set your domain — for example status.yourdomain.com:3001. Uptime Kuma requires port 3001. Make sure your Cloudflare DNS has an A record pointing this subdomain to your server IP.
  3. Click Save then deploy. Once running, open the URL in your browser.
  4. Choose SQLite as the database — it’s the simplest option and works perfectly for monitoring.
  5. Create your admin account and log in.

Setting up a Synology heartbeat monitor

A heartbeat monitor works differently to a normal website monitor. Instead of Uptime Kuma checking if something is online, your Synology calls Uptime Kuma every minute to say “I’m alive”. If it stops calling, Uptime Kuma marks it as down and alerts you.

  1. In Uptime Kuma, click Add New Monitor.
  2. Set Monitor Type to Push.
  3. Set Friendly Name to Synology.
  4. Leave Heartbeat Interval at 60 seconds.
  5. Click Save. Uptime Kuma gives you a Push URL — copy it.
  6. Log into your Synology web interface → Control Panel → Task Scheduler. Find your heartbeat task (or create a new one) and set it to run every minute with this script — replacing the URL with your new Push URL:
curl -fsS --max-time 10 "https://YOUR_PUSH_URL?status=up&msg=OK&ping=" > /dev/null

Synology Task Scheduler settings

SettingValue
Task nameVault Heartbeat (or anything you like)
RepeatDaily
FrequencyEvery 1 minute
Scriptcurl -fsS --max-time 10 "YOUR_PUSH_URL" > /dev/null