How I Deployed a Personal Website on Hetzner VPS

On this page 6 sections

Update, September 2026: This describes the original C-server deployment. The site now generates static HTML and serves it directly through nginx; the C server has been retired from production. The walkthrough below remains a record of the original setup.

A step-by-step walkthrough of how I deployed my personal website, built with C and Lit HTML, on a virtual private server#

How I Deployed a Personal Website on Hetzner VPS

My personal website, where you are currently reading this article, is fully self-hosted: the backend/HTTP protocol is built from scratch with C, the front end is done with lit-html, and the hosting is on Hetzner virtual private server (VPS) for $5 per month.

I used tools like Vercel and Heroku before, but thought that I wanted to have full control over my server and decided to tap into the world of DevOps a bit, so in this article, I’ll walk you step by step through the process and the deployment pipeline (as of the date of writing this post).

Step 1: Creating a server#

Of course, the first step is to create an actual machine. Head to Hetzner and create an account. Once you have an account (ideally with 2FA), go to console → servers and create a server. Select the desired disk space, OS, e.g., I have Ubuntu, and location (I’ve put mine in Helsinki, Finland). When you are ready, press create, and you’ll get your personal virtual server.

Step 2: SSH, Fail2Ban & Firewall#

The next step I took was setting up SSH, configuring the firewall, and installing Fail2Ban. From the Hetzner UI, navigate to your new server and find the firewall rules. Allow TCP ports:

  • 22 (SSH – you can change this later for extra security)
  • 80 (HTTP – web traffic)
  • 443 (HTTPS – secure web traffic)

With your server selected, you can navigate to the main dashboard and open the server’s terminal.

You will be prompted to log in with the root user. If you don’t know the root password (I didn’t get one when creating my server), go to the Rescue tab in the dashboard and click “Reset root password”. This will let you create a new root password you can use to log in.

Once you are in your server’s terminal, configure a new user, SSH, and fail2ban.

It’s not a good idea to always use root. Let’s add a user with sudo privileges:

# replace "john" with your name/username
adduser john

# add the new user to the sudo group
usermod -aG sudo john

Now let’s set up SSH access so you can connect securely from your local machine.

On your local machine (e.g., laptop), generate an SSH key if you don’t already have one:

ssh-keygen -t ed25519 -C "[email protected]"

This will create a key pair in ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public).

Copy your public key to the server (replace john and your_server_ip):

ssh-copy-id john@your_server_ip

If ssh-copy-id isn’t available, you can copy the contents of ~/.ssh/id_ed25519.pub manually and paste them into your server under:

mkdir -p ~/.ssh
nano ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh

Test your login:

ssh john@your_server_ip

If that works, disable root login and password authentication for security. Edit the SSH config:

sudo nano /etc/ssh/sshd_config

Update the following:

PermitRootLogin no
PasswordAuthentication no

Then restart SSH:

sudo systemctl restart ssh

Now, let’s set up UFW Firewall. Ubuntu comes with UFW (Uncomplicated Firewall), which makes firewall management simple. In your server:

sudo apt update
sudo apt install ufw -y

# allow SSH, HTTP, HTTPS
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# enable firewall
sudo ufw enable

# check status
sudo ufw status

The last step is to install and configure fail2ban. Fail2Ban helps block brute-force login attempts.

sudo apt install fail2ban -y

By default, Fail2Ban works out of the box, but we’ll create a local config so updates don’t overwrite it:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Open the config:

sudo nano /etc/fail2ban/jail.local

Look for the [sshd] section and ensure it’s enabled:

[sshd]
enabled = true
port    = ssh
filter  = sshd
logpath = /var/log/auth.log
maxretry = 5

Restart Fail2Ban:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Check its status:

sudo fail2ban-client status sshd

At this point, you have a new user with sudo privileges, SSH key login (no passwords, root login disabled), Firewall, and Fail2Ban protecting against brute-force attempts.

Step 3: Server Binaries & Deployment#

With the server secure, the next step is to actually deploy the website code.

1. Install essential tools

First, update your server and install the basic tools you’ll need:

sudo apt update && sudo apt upgrade -y
sudo apt install build-essential git -y
  • build-essential gives you GCC, make, and other compiling tools.
  • git lets you clone and update your project.

2. Clone your repository

On your server, choose a directory (I use /var/www for web projects):

cd /var/www
sudo git clone https://github.com/yourusername/yourproject.git
cd yourproject

(If your repo is private, set up SSH keys between your server and GitHub/GitLab first.)

3. Build your server

Since my backend was written in C, I compiled it directly on the server with GCC, but of course, this step depends on what you actually use for the backend development.

Step 4: Running with Systemd

Right now, if you close the terminal, your server stops. To keep it running in the background and restart automatically on boot, we’ll use systemd.

Create a new service file:

sudo nano /etc/systemd/system/mywebsite.service

Example configuration:

[Unit]
Description=My Personal Website
After=network.target

[Service]
User=john
WorkingDirectory=/var/www/yourproject
ExecStart=/var/www/yourproject/server
Restart=always
RestartSec=5
Environment=PORT=80

[Install]
WantedBy=multi-user.target

Save and exit, then enable the service:

sudo systemctl daemon-reload
sudo systemctl enable mywebsite
sudo systemctl start mywebsite

Check status:

sudo systemctl status mywebsite

Now your server binary will run in the background and auto-start after a reboot 🚀.

Hetzner gives you a raw IP address. To make it production-ready, let’s add a domain and HTTPS.

1. Point the domain to Hetzner

  • Go to your domain registrar.
  • Set an A record pointing to your Hetzner VPS IP.
  • (If you use Cloudflare, set the record in the Cloudflare DNS dashboard and enable proxy.)

2. Install Nginx as a reverse proxy

Instead of exposing your raw server directly, use Nginx to handle TLS, static files, and reverse proxying.

sudo apt install nginx -y

Create a config:

sudo nano /etc/nginx/sites-available/mywebsite

Example reverse proxy:

server {
    server_name yourdomain.com www.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Enable and restart:

sudo ln -s /etc/nginx/sites-available/mywebsite /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

3. Add HTTPS with Let’s Encrypt

Using Certbot:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certificates auto-renew, and your site now has HTTPS.


Conclusion#

By this point, you should have a fully working VPS setup:

  • A secure server with SSH keys, UFW, and Fail2Ban
  • Your website source is pulled from Git and compiled directly on the machine
  • A systemd service keeping your server binary alive and restarting it automatically
  • (Optionally) Nginx and Let’s Encrypt handling domains, HTTPS, and reverse proxying

Compared to services like Vercel or Heroku, this approach gives you complete control over your stack: from the low-level server process to the way traffic flows through your machine. Of course, that also means you’re responsible for monitoring, updates, and keeping things secure — but that’s part of the fun and learning.

If you’ve followed along, you now have your own little slice of the internet running on Hetzner for just a few dollars a month 🚀. For me, building my site this way was as much about the learning experience as the final result, and I hope this walkthrough helps you on your journey to running your own projects on a VPS.

Next Steps#

Getting my site live on a VPS is a big milestone, but there’s always more you can do to improve reliability, security, and developer experience. Here are a few directions I’m exploring next:

  • Monitoring & Logs – tools like htop, journalctl, or services like Grafana + Prometheus to track server health, uptime, and resource usage.
  • Backups – Regularly back up your code and configuration files (Hetzner also offers snapshot backups of entire servers).
  • CI/CD Deployment – Automate deployments with GitHub Actions or GitLab CI so every push to main can rebuild and restart the systemd service on the server.
  • Scaling & Load Balancing – If the project grows, consider running multiple VPS instances behind a load balancer or using containerization (Docker, Kubernetes).
  • Extra Hardening – Change the default SSH port, enable 2FA for SSH with tools like google-authenticator, or set up a VPN to restrict server access.

With these improvements, you’ll move from just running a website to maintaining a production-ready server setup — all while staying in control of your infrastructure.

Roman
Roman Software engineer in Finland. Making things and sharing what I learn.