---
title: Self-hosting a website on a home PC
date: 2026-10-21 21:00
tags: [selfhosting, cloudflare, windows]
description: 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:

```powershell
winget install --id Cloudflare.cloudflared
cloudflared tunnel login
```

Create a named tunnel and point the domain at it:

```bash
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`:

```yaml
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:

```bash
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:

```js
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.
