---
date: '2026-03-24T10:17:21+01:00'
draft: false
title: 'How to self-host a blog'
author: "Jirka"
tags: ["caddy", "docker", "wireguard", "vps", "guide", "tutorial", "self-hosted"]
categories: ["blog"]
description: "How this blog came online without public IP."
---

In last post [Let's start a blog!](./01-blog.md) I've introduced idea and how-to guide how I will write this blog, but left it offline.
That is going to change in this post.

> [!WARNING]
> If you decide to follow my steps, please, be careful and use your head. I do not guarantee they will work for you and do so at your own risk.

## Requirements

This whole blog/website project is about self-hosting. That said, I will host this page my on server/PC at home. For that, you usually have public IP address.

In this setup, I will act like I do not have one (or you do not want to use it) and setup proxy with VPS in cloud. 
You do not have to copy my setup. If you have public IP address, you can use it and skip whole VPS and WireGuard setup. Or you can host the entire web on VPS. The path I have chosen is the most complicated, so you should be able to make it easier for your self.

That said, here are requirements for my setup:

- Home server with Docker ([Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/))
- VPS with public IPv4 address
- Domain (Like [JirkaBuilds.dev](https://jirkabuilds.dev))
- The desire to set it all up

## Target solution

Static web files are generated by Hugo in `public` folder, how to set up Hugo I've explained [here](./01-blog.md). We will look on how to automate everything in the future posts, but for now, just assume you can always build Hugo files and copy them to the web folder.

Those web files you need to somehow serve, for that purpose there are web servers like [Apache](https://httpd.apache.org/), [Nginx](https://nginx.org/en/), etc. I will use [Caddy](https://caddyserver.com/), it is (at least by me) the easiest one to set up, and it will handle HTTPS certificates for us automatically.

For connection between VPS and our home server, we will use WireGuard and HAProxy for correct source address pass-through via VPN to Caddy.

And that is all, pretty straight forward, isn't it?

When you will connect to [JirkaBuilds.dev](https://jirkabuilds.dev), you will type the URL into the browser, it will find DNS entry for the domain and return IP address of the VPS. Then your browser will connect to VPS, which will wrap the request via HAProxy and sent it via WireGuard to my home server. There, the caddy will serve the static web for you and send it via WireGuard via VPS back to your browser.

As you can see, there is actually no way for you to know, that the web is running in different place than VPS, and that is the purpose.

## Set up VPS

I assume that you've already purchased some domain. Setting up the VPS will be similar, you have to find a provider which will rent you a server and public IPv4 address. Which one is best depends on your location and how powerful you want your VPS to be.
For my purpose, I've purchased the cheapest one possible. I do not need much power and 1 CPU with 1 GB of RAM will work just fine. Remember that the only purpose of VPS is to redirect the connection. All the heavy lifting will be done on my home server.

From this point, I assume that you do have set up domain and DNS to your IPv4 address (how to do so depends on from who you bought the domain), and you do have SSH access to your VPS.

Also, I am using VPS with Ubuntu 24, so all commands count with that.

### Set up SSH

This should be your first step. You do not want to enable connection to your VPS just via password. You should generate keys and set up your VPS to accept SSH connection only via them. Good guide how to do from DigitalOcean is [here](https://www.digitalocean.com/community/tutorials/how-to-configure-ssh-key-based-authentication-on-a-linux-server).

Second thing I suggest is to change ssh port, it is not mandatory, but it is generally better to change from 22, so no one can just try to connect to port 22.
Before changing port, you should open it in your firewall first. I suppose `ufw` is active and only open port is ssh.
It beginning it should look like this:
```text
root@server:~# ufw status
Status: active
To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere                  
OpenSSH (v6)               ALLOW       Anywhere (v6)
```

You should open your new port with:

```bash
ufw allow 2222/tcp # or any port you desire
```

Change ssh port in `/etc/ssh/sshd_config`. Find line with `#Port 22` and change it to `Port 2222` (or any port you choose).
Now restart ssh `systemctl restart ssh` or server and try to connect via new port. If it works, then you can remove port 22 from `ufs` via:

```bash
ufw delete allow OpenSSH
```

If it doesn't work... well, you have to somehow regain access to your server. You should be able to connect via some kind of console provided by you VPS provider(I cannot really help you with that). And correct your setup.


### Set up WireGuard

WireGuard is our next step.

First install WireGuard tools on VPS:
```bash
apt install wireguard-tools
```

Now, generate key pairs. It does not really matter where you do so, but I do prefer store them somehow locally.

```bash
wg genkey | tee vps_private | wg pubkey > vps_public
wg genkey | tee home_private | wg pubkey > home_public
```

Those keys will be used for authentication between VPS and our server, you should keep them for your self.

Now, let's describe our networks on VPS. Create file `/etc/wireguard/wg0.conf` will follow content:
```toml
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <VPS_PRIVATE_KEY>

[Peer]
# Home Server
PublicKey = <HOME_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32
```

What that means. We are creating new network, where VPS have address `10.0.0.1` and Peer (our home server) will get address `10.0.0.2` (We will set up mirror config on home server later).

Now we can enable the network by:
```bash
systemctl enable --now wg-quick@wg0
```

### Set up HAProxy

Now the HAProxy. I think there is good to say a few words why. If we would not set up HAProxy, then from point of view of our home server, all connections would have origin IP of our VPS. We would not be able to check from where did they come from.
What HAProxy will do for us is that it will add right origin IP address in front of the requests and Caddy will read it and correctly replace the IP address in the requests.

That said, we can move to the set-up. Start with installing the HAProxy:
```bash
apt install haproxy
```
I should automatically enable HAProxy service.

Now add following line to the end of `/etc/haproxy/haproxy.cfg`
```text
frontend http_front
    bind *:80
    mode tcp
    option tcplog
    default_backend caddy_http

frontend https_front
    bind *:443
    mode tcp
    option tcplog
    default_backend caddy_https

backend caddy_http
    mode tcp
    server home_caddy 10.0.0.2:80 send-proxy-v2

backend caddy_https
    mode tcp
    server home_caddy 10.0.0.2:443 send-proxy-v2
```

What does it mean? Frontend configuration specifies on which port listen and to what backend passes the packets. And the backend just specifies where to send packets.

You can see that we will be listening on port 80 and 443 (for HTTP and HTTPS) and the communication will be passed via WireGuard network to our home server.

Now restart the HAProxy.
```bash
systemctl restart haproxy
```

### Set up firewall

Last step on side of VPS is to open firewall for HAProxy and WireGuard. This is done similarly as for ssh earlier:
```bash
# Allow HTTP and HTTPS for HAProxy
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow WireGuard connection via UDP
sudo ufw allow 51820/udp
```

And that's it! VPS is all set up.

## Set up home server

Only requirement for our home server is running docker.

Let's start by creating `docker-compose.yml`:
```yaml docker-compose.yml
# Shared network between containers (for later use)
networks:
  proxy_network:

services:
  wireguard:
    image: linuxserver/wireguard:latest
    container_name: wireguard
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    volumes:
      - ./wg-config:/config
      - /lib/modules:/lib/modules:ro
    restart: unless-stopped
    # We will add wireguard to this network, but limit access from WG itself
    networks:
      - proxy_network

  caddy:
    image: caddy:latest
    container_name: caddy
    # Caddy shares all networks with WireGuard
    network_mode: "service:wireguard" 
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
      - ./site_data:/srv
      - ./caddy/data:/data
      - ./caddy/config:/config
    restart: unless-stopped
```

Now, in same directory copy and rename `public` folder generated by Hugo to `site_data`. Also create folders `caddy` and `wg-config`.

Now setup Caddy to host web via WireGuard and use data from HAProxy. Create `caddy/Caddyfile` with following content:
```text
{
    servers {
        listener_wrappers {
            proxy_protocol {
                timeout 2s
                allow 10.0.0.1/32
            }
            tls
        }
    }
}

jirkabuilds.dev {
    root * /srv
    file_server
}
```

And final setup for WireGuard. Create `wg-config/wg0.conf` with following content:
```toml
[Interface]
PrivateKey = <HOME_PRIVATE_KEY>
Address = 10.0.0.2/24
# Do not allow access to anything outside wg0 network
PostUp = iptables -I FORWARD -i wg0 -j DROP
PreDown = iptables -D FORWARD -i wg0 -j DROP

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = <PUBLIC_IP_VPS_IP>:51820
# Only requests for VPS are routed
AllowedIPs = 10.0.0.1/32
# This will send packet every 25 seconds from home server to keep active connection with VPS
# Remember we do not have public IP to connect to from VPS
PersistentKeepalive = 25
```

And that's it! All should be good to go! Just start the containers with:
```bash
docker compose up -d
```

And your web should be online.

---
This article is one from series about this blog and self-hosting. All connected articles can found [here](categories/blog).
