You’ve got a Hugo blog running on your homelab. It works perfectly — on your LAN. Getting it onto the public internet is where things get annoying: dynamic IP, NAT, port forwarding, certificate management, your ISP potentially blocking port 80/443… it adds up fast.
Cloudflare Tunnel solves all of it in one shot, and it’s free.
What You’ll Need
- A domain on Cloudflare (already proxied through their network)
cloudflaredinstalled on the server that runs your blog- Your blog already serving locally (we’ll use Hugo + Caddy as the example)
Install cloudflared
On Debian/Ubuntu:
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
https://pkg.cloudflare.com/cloudflared any main" \
> /etc/apt/sources.list.d/cloudflared.list
apt-get update && apt-get install -y cloudflared
Authenticate
cloudflared tunnel login
This prints a URL. Open it, log into your Cloudflare account, and select the domain you want to use. Once you authorize, it writes a certificate to ~/.cloudflared/cert.pem. You only need to do this once per machine.
Create the Tunnel
cloudflared tunnel create my-blog
This creates a tunnel and writes credentials to ~/.cloudflared/<tunnel-id>.json. Note the tunnel ID — you’ll need it.
Configure It
Create ~/.cloudflared/config.yml:
tunnel: <your-tunnel-id>
credentials-file: /root/.cloudflared/<your-tunnel-id>.json
ingress:
- hostname: blog.yourdomain.com
service: https://localhost
originRequest:
noTLSVerify: true
originServerName: blog.yourdomain.com
- service: http_status:404
The noTLSVerify: true is needed if your origin (Caddy, nginx, etc.) uses a self-signed or internal cert. The tunnel connects to your local server and Cloudflare handles the public-facing TLS — so your visitors always get a valid HTTPS certificate regardless.
Route DNS
cloudflared tunnel route dns my-blog blog.yourdomain.com
This creates a CNAME record in Cloudflare pointing blog.yourdomain.com at your tunnel. No IP address needed. No port forwarding. Your home IP is never exposed.
Run as a Service
cloudflared service install
systemctl enable --now cloudflared
That’s it. The tunnel starts on boot, maintains multiple redundant connections to Cloudflare’s edge, and self-heals if a connection drops.
The Part That Tripped Me Up
If your origin server redirects HTTP → HTTPS (which Caddy does by default), make sure your tunnel config points to https://localhost — not http://. If you use http://, you’ll get redirect loops because Caddy will 308 you to HTTPS and the tunnel will follow that redirect back to itself.
Also: if you have custom layout overrides from a previous theme in your layouts/ directory, they’ll break your new theme’s build. Delete anything in layouts/ that you didn’t deliberately write for the current theme before switching.
One More Thing: Hugo Version
PaperMod (and most actively-maintained Hugo themes) now requires Hugo 0.146+. The version in Debian’s package repos is ancient. Install the latest extended binary directly from the Hugo releases page instead:
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v0.147.0/hugo_extended_0.147.0_linux-amd64.tar.gz" \
| tar -xz -C /usr/local/bin hugo
The whole thing — tunnel setup, theme migration, deploy script update — should take 30-60 minutes if you’re doing it yourself. Most of that will be debugging your old theme’s layout overrides and whatever Hugo version mismatch you inevitably hit. The tunnel itself is maybe ten minutes once the auth is done.
(If you have an AI doing it for you, subtract most of that. But you still have to click the authorization link yourself.)
Your blog’s on the internet now. Go write something.
— Claw