Set Up a Hetzner Cloud Server from Scratch
Create a Hetzner Cloud server from scratch: project setup, SSH key upload, cloud-init bootstrapping, cloud firewall, IPv6, and snapshots in one structured guide.
Before you start
- ▸A Hetzner Cloud account with a project API token
- ▸hcloud CLI installed locally (or access to the Hetzner web console)
- ▸An Ed25519 SSH key pair generated on your local machine
- ▸Basic familiarity with SSH and the Linux command line
Hetzner Cloud offers excellent price-to-performance ratios and a clean API-driven interface. Whether you're deploying a personal VPS or a production workload, getting the foundation right—SSH keys, cloud-init bootstrapping, a tight firewall, IPv6, and snapshot backups—saves you from painful retrofits later. This guide walks through every layer from project creation to a verified, hardened server.
Create a Project and Upload Your SSH Key
All Hetzner Cloud resources live inside a project, which acts as an isolation boundary for billing, firewalls, and networks. Log in to console.hetzner.cloud, click New project, and give it a meaningful name (e.g., prod-web).
Before creating a server, upload your SSH public key so it can be injected at boot time rather than relying on a root password.
Generate a key if you don't have one
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/hetzner_ed25519
Ed25519 is preferred over RSA for new keys: smaller, faster, and equally secure for this use case.
Add the public key to the project
In the Hetzner console, navigate to Security → SSH Keys → Add SSH Key. Paste the contents of ~/.ssh/hetzner_ed25519.pub. Give it a recognisable label like laptop-2025.
Alternatively, use the hcloud CLI (install it first):
# Debian/Ubuntu
sudo apt install hcloud-cli
# Fedora / RHEL family
sudo dnf install hcloud
# Arch
sudo pacman -S hcloud
hcloud context create prod-web # enter your API token when prompted
hcloud ssh-key create --name laptop-2025 --public-key-from-file ~/.ssh/hetzner_ed25519.pub
Write a cloud-init User-Data File
cloud-init runs once on first boot and can create users, install packages, write files, and run arbitrary commands—all before you ever log in. Providing user-data eliminates manual setup steps and makes your server reproducible.
Create a local file called cloud-init.yml:
cat > ~/cloud-init.yml <<'EOF'
#cloud-config
# Create a non-root admin user
users:
- name: deploy
groups: [sudo, systemd-journal]
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAA...your-public-key... [email protected]
# Disable root SSH login and password auth
write_files:
- path: /etc/ssh/sshd_config.d/99-harden.conf
content: |
PermitRootLogin no
PasswordAuthentication no
AuthenticationMethods publickey
# Update packages and install essentials
package_update: true
package_upgrade: true
packages:
- fail2ban
- unattended-upgrades
- curl
- vim
# Enable services
runcmd:
- systemctl enable --now fail2ban
- systemctl restart ssh
EOF
Replace the ssh_authorized_keys value with your actual public key string. The runcmd block uses systemctl directly—cloud-init runs as root, so no sudo is needed here.
Create the Server
In the console: Servers → Add Server. Pick a location close to your users, choose an OS (Ubuntu 24.04 LTS is a solid default), select your server type (the CX22 at 2 vCPUs / 4 GB RAM is a good starting point), attach your SSH key, and paste the contents of cloud-init.yml into the User data field.
With the CLI:
hcloud server create \
--name web-01 \
--type cx22 \
--image ubuntu-24.04 \
--location nbg1 \
--ssh-key laptop-2025 \
--user-data-from-file ~/cloud-init.yml
The server will be running within 30–60 seconds. Note the IPv4 and IPv6 addresses printed in the output.
Configure the Hetzner Cloud Firewall
Hetzner's cloud firewall is a network-level filter applied before traffic hits your server—independent of any in-guest firewall. Apply it as a first layer; run ufw or nftables inside the guest as a second layer.
Create and apply the firewall via CLI
# Allow SSH, HTTP, HTTPS inbound; block everything else
hcloud firewall create --name web-fw
hcloud firewall add-rule web-fw --direction in --protocol tcp --port 22 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule web-fw --direction in --protocol tcp --port 80 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule web-fw --direction in --protocol tcp --port 443 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall apply-to-server web-fw --server web-01
Outbound traffic is permitted by default. If you only need SSH for now, omit the port 80/443 rules and add them later. Restricting SSH to your own IP (--source-ips 203.0.113.5/32) is strongly recommended if you have a static address.
Guest-level firewall with ufw (Ubuntu/Debian)
ssh deploy@<server-ipv4>
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status verbose
Enable and Verify IPv6
Hetzner assigns a /64 IPv6 prefix to every server at no extra cost. Ubuntu 24.04 cloud images configure the primary IPv6 address automatically via SLAAC. Verify it is working:
ip -6 addr show eth0
curl -6 https://ifconfig.me
You should see your server's IPv6 address returned. If not, check that the interface has a global-scope address (not just link-local fe80::). On Hetzner, the full /64 block is routed to your server, so you can bind additional addresses from that range to the same interface:
# Add a second address from your /64 (replace the prefix)
sudo ip -6 addr add 2a01:4f8:xxxx:xxxx::2/64 dev eth0
To make it persistent, add a Netplan stanza on Ubuntu (/etc/netplan/60-extra-ipv6.yaml) or a systemd-networkd drop-in on other distros.
Take a Snapshot
Once your baseline server is configured and verified, take a snapshot before installing application software. This gives you a clean rollback point.
Snapshots require the server to be powered off for maximum consistency, though Hetzner does support live snapshots. Powering off is safer for boot-critical data:
hcloud server shutdown web-01
# Wait ~10 seconds for clean shutdown
hcloud server create-image --type snapshot --description "baseline-2025-07" web-01
hcloud server poweron web-01
List your snapshots with:
hcloud image list --type snapshot
Snapshots are billed at €0.0119/GB/month. They are not continuous backups—for those, enable Backups in the server settings (automated daily backups at 20% of the server cost).
Verify Everything
# Connect as the deploy user (not root)
ssh -i ~/.ssh/hetzner_ed25519 deploy@<server-ipv4>
# Confirm root login is blocked
ssh root@<server-ipv4> # should be refused
# Check cloud-init completed successfully
sudo cloud-init status --long
# Verify fail2ban is active
systemctl is-active fail2ban
# Test IPv6 outbound
curl -6 https://ifconfig.me
# List active ufw rules
sudo ufw status numbered
Troubleshooting
- SSH refused after creation: cloud-init may still be running. Wait 60–90 seconds and retry. Check the console's serial console (Server → Console) for boot output.
- cloud-init didn't run my packages: Validate your YAML syntax with
python3 -c "import yaml,sys; yaml.safe_load(sys.stdin)" < cloud-init.ymlbefore next boot. A single indentation error silently skips entire sections. - Locked out of SSH: Use the Hetzner console's built-in Rescue system—boot into it, mount your disk, and fix
/etc/ssh/sshd_config.d/99-harden.conf. - IPv6 shows only link-local address: Ensure the Hetzner network interface has IPv6 enabled. In rare cases,
sysctl net.ipv6.conf.eth0.disable_ipv6may be set to 1—set it to 0 and add the setting to/etc/sysctl.d/. - Firewall rule blocked a port you need: Hetzner's cloud firewall rules take effect immediately after creation. Use
hcloud firewall describe web-fwto audit current rules, then add or delete as needed.
Frequently asked questions
- Can I use a password instead of an SSH key on Hetzner Cloud?
- Yes, but you shouldn't. Hetzner will email a root password if no SSH key is attached, but our cloud-init config immediately disables password authentication. SSH keys with Ed25519 are faster, simpler, and far more secure.
- What is the difference between Hetzner snapshots and backups?
- Snapshots are manual, point-in-time images you create yourself; they persist until deleted and are billed per GB. Backups are automated daily images enabled per-server at 20% of the server's hourly cost, with Hetzner retaining the last seven.
- Do I need both the Hetzner Cloud Firewall and ufw inside the server?
- The Hetzner firewall is a network-level filter that stops traffic before it reaches your NIC—it keeps your attack surface small even if the guest OS is misconfigured. ufw inside the guest gives you process-level control and is a good defence-in-depth practice.
- Why use cloud-init instead of configuring things after first login?
- cloud-init makes your setup reproducible and scriptable. If you need to rebuild or clone the server, re-attaching the same user-data file recreates the baseline automatically, with no manual steps and no chance of forgetting a hardening step.
- How do I restore a server from a snapshot?
- Run `hcloud server rebuild --image <snapshot-id> web-01` to overwrite the server's disk with the snapshot, or use `hcloud server create --image <snapshot-id>` to spin up a new server from it. The snapshot ID is shown by `hcloud image list --type snapshot`.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.