← Journey Phase 0 — Foundation

The network

Five devices, zero open ports, one mesh. How Tailscale, Pi-hole, Cloudflare Tunnel, and NPM fit together — and why it's wired this way.

Every service in this homelab is accessible from anywhere — but nothing is open to the internet by default. No port forwarding. No VPN to configure per device. No firewall rules to punch holes through.

Here’s how that works.

The full picture

Cloudflare
DNS
*.[domain]
→ Tailscale IPs via Pi-hole
R2 CDN
cdn.[domain]
artupinad bucket · static
Zero Trust Tunnel
immich.[domain]
public · → cloudflared on Pi4
↑ Internet
everything else is private
Tailscale or LAN required
Tailscale
Linode VPS
100.x.x.x
  • Pi-hole DNS
  • Tailscale exit node
  • Watchtower
always-on VPS
GMKtec
100.x.x.x
  • n8n
  • Penpot
  • CouchDB
  • Ollama / Open WebUI
  • AMD 780M · Vulkan · 3GB
always-on compute
Pi4
100.x.x.x
  • NPM / HTTPS
  • Immich
  • Homepage
  • Uptime Kuma
  • cloudflared
reliable infra
DaniAIR
100.x.x.x
  • Claude Code
  • Obsidian
  • Figma · Browser
daily driver
iPhone 15
100.x.x.x
  • Obsidian
  • Blink Shell
mobile

Five nodes. Two traffic models. One mesh tying it together.


The mesh — Tailscale

The foundation is Tailscale, a zero-config mesh VPN built on WireGuard. Every device — Mac, GMKtec, Pi4, Linode, iPhone — gets a stable 100.x.x.x IP that never changes regardless of what network it’s on. They all reach each other directly, peer-to-peer, without a central relay when possible.

Why Tailscale over a self-hosted WireGuard setup:

  • Zero configuration. No keys to distribute, no subnets to configure. Install, authenticate, done.
  • Works through NAT. Home networks, mobile data, corporate WiFi — it punches through all of them.
  • ACLs that don’t require an ops team. A JSON policy file controls who can reach what.
  • Subnet routing. Pi4 advertises 192.168.68.0/24 so every Tailscale device can reach home LAN IPs directly.

The mesh means internal services never need to be exposed publicly. n8n.[domain], penpot.[domain], couchdb:5984 — all Tailscale-only.


The exit node — Linode VPS

The Linode $5/month Nanode runs two things:

Pi-hole — a DNS sinkhole that blocks ad/tracker domains for every device on the mesh. All Tailscale devices point their DNS to 100.x.x.x (Linode’s Tailscale IP). It runs on the VPS instead of Pi4 because VPS uptime is more reliable — a Pi4 reboot or power cut doesn’t kill DNS for every device.

Tailscale exit node — when enabled, all internet traffic routes through Linode’s Singapore IP. Useful when on untrusted networks.


Public access — Cloudflare Tunnel

Only one service needs to be accessible from the internet without Tailscale: Immich (photo backup). The iPhone app runs in the background and can’t have Tailscale running simultaneously.

Cloudflare Tunnel handles this without opening a single port:

  1. cloudflared daemon runs on Pi4, maintains an outbound-only connection to Cloudflare’s network
  2. immich.[domain] DNS record points to the Cloudflare-managed tunnel hostname
  3. Requests from anywhere hit Cloudflare → travel through the tunnel → reach Pi4 → Immich
  4. Cloudflare Access sits in front of it (Google login + FaceID biometrics required)

Everything else — n8n, Penpot, CouchDB, Homepage, Uptime Kuma — has no public DNS record pointing anywhere reachable. Requests from the internet just fail silently.


HTTPS everywhere, zero open ports — NPM

Nginx Proxy Manager runs on Pi4 and terminates HTTPS for all internal services. It gets wildcard Let’s Encrypt certs via DNS Challenge through the Cloudflare API — no HTTP challenge, no open port 80.

The proxy host for n8n.[domain] forwards to 100.x.x.x:5678 (GMKtec’s Tailscale IP). Same for Penpot, CouchDB, Open WebUI. Pi4 is the single HTTPS entry point for the internal mesh.


Traffic paths

How a request actually moves depending on where you are:

On Tailscale Accessing any *.[domain] service from a connected device
💻 Device Mac / iPhone
🔒 Tailscale mesh VPN
🛡️ Pi-hole Linode DNS
100.x.x.x Tailscale IP
🔀 NPM on Pi4 HTTPS + Let's Encrypt
Service GMKtec n8n / Penpot / etc
From the internet Only Immich is reachable publicly — everything else returns nothing
🌐 Browser anywhere
☁️ Cloudflare DNS immich.[domain]
CF Tunnel Zero Trust
🍓 cloudflared Pi4 daemon
📸 Immich port 2283

The difference matters: internal requests never leave the Tailscale mesh. They resolve to a private IP, hit NPM on Pi4, and land on GMKtec — all within the 100.x.x.x address space. Public requests take the Cloudflare path, and only Immich is on that path.


The CDN — Cloudflare R2

Static assets for artupinad.com are served from a Cloudflare R2 bucket (artupinad) via cdn.[domain]. R2 is S3-compatible with zero egress fees — n8n can write files to it via the S3 node (Force Path Style ON, region auto, endpoint at the account-scoped R2 URL).

This is the only part of the stack where Cloudflare is doing actual content serving rather than just routing.


Why this complexity?

The alternative is simpler on paper: port-forward everything, run Nginx directly, use dynamic DNS. But:

  • Port forwarding exposes services to the public internet with no auth layer
  • Dynamic DNS breaks during ISP changes or router reboots
  • Every new service needs a new firewall rule
  • Mobile devices on different networks need separate VPN configs

This setup has a higher initial cost (got the Linode, configured Tailscale ACLs, wired DNS Challenge) but the ongoing cost is near zero. A new service goes live in three steps: deploy container on GMKtec, add proxy host in NPM, add Pi-hole DNS record. It’s reachable on all Tailscale devices in under two minutes, with HTTPS, no port changes.