Self-hosting a website on a home PC
How this blog is served from a desktop at home with Node.js and a Cloudflare Tunnel, without opening a single port.
This blog is served from an ordinary desktop at home. No VPS, no static hosting service. Here's how it's wired up.
The pieces#
| Piece | Job |
|---|---|
| Node.js | Runs a small web server that turns Markdown posts into pages |
| cloudflared | Keeps an outgoing tunnel open to Cloudflare |
| Cloudflare | Owns the DNS for meyo.dev, handles HTTPS and sends visitors down the tunnel |
| Task Scheduler | Starts everything at login; a watchdog restarts anything that crashes |
Why a tunnel instead of port forwarding?#
The classic way is to open a port on the router and point the domain at your home IP address. A tunnel flips that around: the PC makes an outgoing connection to Cloudflare, and visitors come in through it.
- No ports open on the router
- The home IP address stays private
- HTTPS certificates are handled automatically
- It still works behind CGNAT, where port forwarding is impossible
The web server only listens on
127.0.0.1, so nothing on the local network can reach it directly either. The tunnel is the only way in.
Setting it up#
Install cloudflared and link it to a Cloudflare account:
winget install --id Cloudflare.cloudflared
cloudflared tunnel login
Create a named tunnel and point the domain at it:
cloudflared tunnel create meyo-dev
cloudflared tunnel route dns meyo-dev meyo.dev
cloudflared tunnel route dns meyo-dev www.meyo.dev
Then tell the tunnel where to send traffic, in ~/.cloudflared/config.yml:
tunnel: <tunnel-id>
credentials-file: ~/.cloudflared/<tunnel-id>.json
ingress:
- hostname: meyo.dev
service: http://127.0.0.1:8080
- hostname: www.meyo.dev
service: http://127.0.0.1:8080
- service: http_status:404
And start it:
cloudflared tunnel run meyo-dev
The web server#
The server is a single Node.js file built on the standard http module. Posts are Markdown files with a few lines of front matter on top. They're read from disk on every request, so publishing a post is just saving a file:
const posts = loadPosts(POSTS_DIR);
const post = posts.find(p => p.slug === slug);
sendPage(res, 200, views.post(post, renderMarkdown(post.body)));
The catch#
The site is only up while the PC is. If it sleeps, reboots or loses power, meyo.dev goes down with it. For a personal blog, that's a fair trade.