The Lab

How I Self-Hosted This Blog on a Mini PC

I got tired of the free tier anxiety. Vercel and Netlify are solid until they change their pricing or disappear, and I did not want a recurring bill for a static site. I wanted a machine sitting in my house actually serving the files.

The machine

The site runs on a generic mini PC — an N100 processor, 16GB of RAM, and an SSD. Mini PCs like this are worth knowing about if you are doing homelab work on a budget. They pull around 6W at idle, run silently, and handle a static site without any trouble. Overkill for this use case, but that just means it will never struggle.

Building the site with Hugo

I went with Hugo. It is written in Go, fast to build, and produces flat HTML files. No database, no plugins, no moving parts.

I built the theme from scratch rather than picking one from the library. It is called Analog and it has three sections: Lab, Field, and Grind. Building it myself took longer but I do not have to fight someone else’s CSS when I want to change something.

The workflow is straightforward: write in Markdown, run hugo --minify, and everything gets compressed into a public/ folder. That is the entire site.

Exposing the server: Cloudflare Tunnel

I registered the domain through Cloudflare. They sell domains at cost and keeping DNS in the same place as the tunnel setup makes things simpler.

The part that puts most people off self-hosting is exposing a local server to the internet. I did not want to open ports on my router or expose my home IP. cloudflared solves both problems. It is a small daemon that creates an outbound tunnel to Cloudflare’s edge. Traffic hits the domain, travels through Cloudflare’s network, and comes down the tunnel to the mini PC. No port forwarding needed.

Setting up the tunnel:

# Install cloudflared
curl -L https://pkg.cloudflare.com/cloudflare-main.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared

# Authenticate with your Cloudflare account
cloudflared tunnel login

# Create a named tunnel
cloudflared tunnel create blog

# Create the config directory
mkdir -p ~/.cloudflared

The config file at ~/.cloudflared/config.yml:

tunnel: <your-tunnel-id>
credentials-file: /home/<user>/.cloudflared/<tunnel-id>.json

ingress:
  - hostname: yourdomain.com
    service: http://localhost:1313
  - service: http_status:404

Run it as a system service so it starts on boot:

sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

DNS records

In the Cloudflare dashboard, add a CNAME pointing the root domain at the tunnel:

TypeNameTarget
CNAME@<tunnel-id>.cfargotunnel.com

Proxy it through Cloudflare (orange cloud on). The home IP never appears anywhere.

The deploy script

When I push new content the flow is:

  1. Push to GitHub
  2. SSH into the server and run the deploy script
  3. Hugo builds and the server picks up the changes

The script:

#!/bin/bash
set -e
cd /home/server-anjelo/blog
GIT_SSH_COMMAND="ssh -i /home/server-anjelo/.ssh/github_blog" git pull origin main
/snap/bin/hugo --minify
echo "Deployed at $(date)"

A dedicated SSH key handles the GitHub authentication so credentials are not mixed. The set -e means if anything fails the script stops rather than deploying a broken build.

What it costs

Domain registration through Cloudflare is roughly 10 to 15 NZD a year depending on the TLD. The tunnel is free on Cloudflare’s free plan. The mini PC was a one-time purchase and sits at around 6 to 8W at idle. That is the full cost.

Why bother

Managed hosting is convenient but you are always a guest on someone else’s platform. Setting this up taught me more about DNS, Linux services, and how traffic actually moves than any one-click deploy ever did. It is also a machine I understand end to end, which matters when something goes wrong at 11pm.