DB
ALPHA
Photos

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

Terminal window
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

Terminal window
add user <my-name>
usermod -aG sudo <my-name> // give user admin privileges
getent 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

Terminal window
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 no
PasswordAuthentication no
Port 2222 # change from default 22
AllowUsers <my-name>

Ok, let’s restart

sudo systemctl restart ssh

Set up SSH key auth for your user

Terminal window
# On your local machine, generate new ssh keys if you don't have them
ssh-keygen -t ed25519 -C "your_email@example.com"
# Copy public key to server
ssh-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)

Terminal window
mkdir -p /home/myname/.ssh
chmod 700 /home/myname/.ssh
chown myname:myname /home/myname/.ssh

Add your new ssh public key

Terminal window
vim /home/myname/.ssh/authorized_keys

Now let’s set permissions

Terminal window
chmod 600 /home/myname/.ssh/authorized_keys
chown myname:myname /home/myname/.ssh/authorized_keys

Ok, we can now log in through SSH using:

Terminal window
ssh -i ~/.ssh/mykeyname -p 2222 myname@yourserverip

Updating everything

Before configuring our environment, it is better to updated essential packages

Terminal window
sudo apt update && sudo apt upgrade -y

Fine, now we can install a few utilities

Terminal window
apt install curl vim ufw git -y

Setting up firewall

Ok, first of all let’s setup some firewall rules using ufw

Terminal window
$ 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

Terminal window
ufw allow 2222/tcp # your new SSH port
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS

Ok, now let’s enable our firewall

Terminal window
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:

Terminal window
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
# Add your user to docker group
sudo usermod -aG docker yourusername
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo 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.

Terminal window
mkdir -p ~/docker/{traefik,websites}
cd ~/docker

Create a docker network

Terminal window
docker network create web

Create a docker compose file for traefik

Terminal window
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

Terminal window
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/traefik
touch acme.json
chmod 600 acme.json
mkdir logs

Adding further security

Setup fail 2 ban

Terminal window
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Setup security updates

Terminal window
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

Configure log rotation

Terminal window
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

Terminal window
cd ~/docker/traefik
docker-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

Terminal window
mkdir -p ~/docker/websites
cd ~/docker/websites

Now upload the project folder

Terminal window
# Upload entire folder
scp -P 2222 -i ~/.ssh/your_key_id -r /path/to/your/project yourusername@your_server_ip:~/docker/websites/

We also need to setup docker:

Terminal window
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

Terminal window
docker-compose up -d

Check DNS propagation

Terminal window
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

Terminal window
docker logs traefik -f

Check if website can be reached

Terminal window
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