Hetzner Cloud is one of the best-value Linux VPS providers in 2026. The hardware is good, the network is fast, and the pricing is a fraction of AWS or GCP for comparable specs. The default setup is not production-ready. Here's what to do before you put real traffic on it.
1. Firewall — default is wide open
A fresh Hetzner server has no firewall rules. Every port is accessible from the internet. The first thing to do after SSH access is confirmed:
sudo apt update && sudo apt install ufw -y sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow ssh sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable sudo ufw status verbose
Docker users: UFW does not protect Docker container ports. Bind containers to 127.0.0.1 — use 127.0.0.1:PORT:PORT in docker-compose.yml. See the Docker UFW bypass fix.
2. SSH hardening
# Disable password authentication (key-only): sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config # Disable root login: sudo sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config sudo systemctl restart sshd
Before disabling password auth: confirm your SSH key works in a second terminal session. Locking yourself out of a Hetzner server requires a rescue boot.
3. fail2ban
sudo apt install fail2ban -y # Create local config (never edit jail.conf directly): sudo tee /etc/fail2ban/jail.local << 'EOF' [DEFAULT] bantime = 1h findtime = 10m maxretry = 3 backend = systemd [sshd] enabled = true port = ssh maxretry = 3 bantime = 24h [recidive] enabled = true logpath = /var/log/fail2ban.log banaction = %(banaction_allports)s bantime = 1w findtime = 1d maxretry = 5 EOF sudo systemctl enable --now fail2ban sudo fail2ban-client status sshd
4. SSL certificate
# Install certbot (snap method — recommended on Ubuntu 22.04): sudo snap install --classic certbot sudo ln -s /snap/bin/certbot /usr/bin/certbot # Obtain certificate (Nginx): sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Verify auto-renewal timer: sudo systemctl status snap.certbot.renew.timer # Test renewal: sudo certbot renew --dry-run
5. Docker setup (if using containers)
# Install Docker:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Critical: bind all container ports to localhost:
# In docker-compose.yml use:
# ports:
# - "127.0.0.1:PORT:PORT"
# NOT:
# - "PORT:PORT"
# Set default log rotation in /etc/docker/daemon.json:
sudo tee /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOF
sudo systemctl restart docker
6. Unattended security updates
sudo apt install unattended-upgrades -y sudo dpkg-reconfigure -pmedium unattended-upgrades # Verify it's enabled: cat /etc/apt/apt.conf.d/20auto-upgrades
Should show APT::Periodic::Unattended-Upgrade "1";.
7. Swap (for low-RAM servers)
Hetzner's CX11 (2GB RAM) and CX21 (4GB RAM) servers can run out of memory under load. Add swap as a safety net:
sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
8. Basic monitoring
# Check current resource usage: htop df -h free -h # Watch Nginx error log: sudo tail -f /var/log/nginx/error.log # Watch fail2ban: sudo fail2ban-client status sudo journalctl -u fail2ban --since "1 hour ago"
Production-ready checklist
- UFW enabled with default-deny incoming
- SSH key-only authentication (password auth disabled)
- Root login disabled
- fail2ban active with sshd jail and recidive jail
- SSL certificate installed and auto-renewal verified
- Docker containers bound to 127.0.0.1 (not 0.0.0.0)
- Docker log rotation configured
- Unattended security updates enabled
- Swap configured (for servers under 4GB RAM)
Audit your Hetzner server config — firewall rules, Docker port exposure, SSL expiry, and reverse proxy setup.