Setting up multiple websites on a Hetzner VPS
Part1 - A detailed step by step guide from zero to static website hosting
Buy a VPS and create a SSH key
If you are looking for a low-budget VPS, Hetnzer has a nice After setting up your account, you should create a SSH key to connect to your server
ssh root@<my-ip>
Ok, now are accessing your Hetner remote machine from your computer! Feeling Matrix vibes? Not yet? Well, let’s move on with the next steps
Create a new user
We all know that security through obscurity isn’t going to take you anywhere, but i think that it should be better to avoid using “root” user. It helps keeping things organized and logs end up being more understandable. Let’s create a new user with sudo privileges
add user <my-name>usermod -aG sudo <my-name> // give user admin privilegesgetent group sudo // check if user is in sudo group
Remember to ssh as <my-name>@<my-ip>
Now that we have our own user, we can deactivate root login. I’ll use vim as text editor, but you can use nano if you prefer
sudo vim /etc/ssh/sshd_config# You can also use nano: sudo nano /etc/ssh/sshd_config
To keep ssh as the only way of connecting, deactivate root login and password authentication. Let’s even change port and allow the newly created user
PermitRootLogin noPasswordAuthentication noPort 2222 # change from default 22AllowUsers <my-name>
Ok, let’s restart
sudo systemctl restart ssh
Set up SSH key auth for your user
# On your local machine, generate new ssh keys if you don't have themssh-keygen -t ed25519 -C "your_email@example.com"
# Copy public key to serverssh-copy-id -p 2222 yourusername@your_server_ip
Using SSH key for a newly created user
Create the .ssh directory (if it doesn’t exist)
mkdir -p /home/myname/.sshchmod 700 /home/myname/.sshchown myname:myname /home/myname/.ssh
Add your new ssh public key
vim /home/myname/.ssh/authorized_keys
Now let’s set permissions
chmod 600 /home/myname/.ssh/authorized_keyschown myname:myname /home/myname/.ssh/authorized_keys
Ok, we can now log in through SSH using:
ssh -i ~/.ssh/mykeyname -p 2222 myname@yourserverip
Updating everything
Before configuring our environment, it is better to updated essential packages
sudo apt update && sudo apt upgrade -y
Fine, now we can install a few utilities
apt install curl vim ufw git -y
Setting up firewall
Ok, first of all let’s setup some firewall rules using ufw
$ ufw default deny incoming$ ufw default allow outgoing
We denied incoming traffic, but the rule is not active yet: we still have to enable the ufw service. Before doing that, we should avoid to be shut off of our beloved VPS, so we have to allow SSH connections
ufw allow 2222/tcp # your new SSH portufw allow 80/tcp # HTTPufw allow 443/tcp # HTTPS
Ok, now let’s enable our firewall
ufw enable
All right, we have a nice firewall up and running.
Installing Docker
Everything on our server should be Dockerized, so let’s start with installing the right tools:
# Install Dockercurl -fsSL https://get.docker.com -o get-docker.shsh get-docker.sh
# Add your user to docker groupsudo usermod -aG docker yourusername
# Install Docker Composesudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-composesudo chmod +x /usr/local/bin/docker-compose
Reverse proxy with traefik
Now it’s time to think about reverse proxy: we’ll use traefik, which is a good choice when using Docker.
mkdir -p ~/docker/{traefik,websites}cd ~/docker
Create a docker network
docker network create web
Create a docker compose file for traefik
vim ~/docker/traefik/docker-compose.yml
services: traefik: image: traefik:v3.0 container_name: traefik restart: unless-stopped ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yml:/traefik.yml:ro - ./acme.json:/acme.json - ./logs:/logs networks: - web environment: - TRAEFIK_API_DASHBOARD=true
networks: web: external: true
Create traefik yml file configuration
vim ~/docker/traefik/traefik.yml
api: dashboard: true insecure: false
entryPoints: web: address: ":80" http: redirections: entrypoint: to: websecure scheme: https websecure: address: ":443"
providers: docker: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false network: web
certificatesResolvers: letsencrypt: acme: email: your-email@domain.com storage: acme.json httpChallenge: entryPoint: web
log: level: INFO filePath: "/logs/traefik.log"
accessLog: filePath: "/logs/access.log"
Setup SSL certificate storage
cd ~/docker/traefiktouch acme.jsonchmod 600 acme.jsonmkdir logs
Adding further security
Setup fail 2 ban
sudo apt install fail2ban -ysudo systemctl enable fail2bansudo systemctl start fail2ban
Setup security updates
sudo apt install unattended-upgrades -ysudo dpkg-reconfigure -plow unattended-upgrades
Configure log rotation
sudo vim /etc/logrotate.d/docker-logs
Add a few options:
/home/yourusername/docker/traefik/logs/*.log { daily rotate 30 compress delaycompress missingok notifempty create 644 yourusername yourusername}
Good, now you can start traefik
cd ~/docker/traefikdocker-compose up -d
We’re ready to go!
Ok, now let’s try to upload a static site:
First of all, create a new folder for alll future websites that will be hosted on our VPS
mkdir -p ~/docker/websitescd ~/docker/websites
Now upload the project folder
# Upload entire folderscp -P 2222 -i ~/.ssh/your_key_id -r /path/to/your/project yourusername@your_server_ip:~/docker/websites/
We also need to setup docker:
vim docker-compose.yml
And configure traefik to handle certificates and correctly redirect from yourdomain.com to your project:
services: mysite: image: nginx:alpine container_name: mysite restart: unless-stopped volumes: - ./:/usr/share/nginx/html:ro networks: - web labels: - "traefik.enable=true" - "traefik.http.routers.mysite.rule=Host(`yourdomain.com`)" - "traefik.http.routers.mysite.entrypoints=websecure" - "traefik.http.routers.mysite.tls.certresolver=letsencrypt" - "traefik.http.services.mysite.loadbalancer.server.port=80"
networks: web: external: true
Now we can start the container
docker-compose up -d
Check DNS propagation
nslookup yourdomain.com# this should print your Hetzner VPS IP. If it doesn't, try to allow some time for DNS propagation
Traefik should get SSL certificates
docker logs traefik -f
Check if website can be reached
curl https://mysite.com
Nice! Now you have your static website ready to rock!
Next time we’ll talk about CI/CD using GitHub Actions