<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anlian Coder</title>
    <description>The latest articles on DEV Community by Anlian Coder (@anlian_coder).</description>
    <link>https://dev.to/anlian_coder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4221536%2F63be8075-46d1-48ae-ac40-620b9fb1c8a7.jpg</url>
      <title>DEV Community: Anlian Coder</title>
      <link>https://dev.to/anlian_coder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anlian_coder"/>
    <language>en</language>
    <item>
      <title>My budget VPS had ports 80/443 blocked by NAT — here's the architecture that got my site online published: true</title>
      <dc:creator>Anlian Coder</dc:creator>
      <pubDate>Mon, 02 Nov 2026 05:19:49 +0000</pubDate>
      <link>https://dev.to/anlian_coder/my-budget-vps-had-ports-80443-blocked-by-nat-heres-the-architecture-that-got-my-site-online-209g</link>
      <guid>https://dev.to/anlian_coder/my-budget-vps-had-ports-80443-blocked-by-nat-heres-the-architecture-that-got-my-site-online-209g</guid>
      <description>&lt;h2&gt;
  
  
  1. The Symptom: When "Correct" Means Down
&lt;/h2&gt;

&lt;p&gt;The browser gave me the dreaded &lt;code&gt;ERR_CONNECTION_TIMED_OUT&lt;/code&gt;, alternating with intermittent &lt;code&gt;Connection reset by peer&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;My &lt;code&gt;A&lt;/code&gt; record resolved cleanly to the IP listed in the hosting control panel. Inside the box, &lt;code&gt;systemctl status caddy&lt;/code&gt; showed green across the board. The local firewall had ports open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw status
&lt;span class="c"&gt;# Status: active&lt;/span&gt;
&lt;span class="c"&gt;# To                         Action      From&lt;/span&gt;
&lt;span class="c"&gt;# --                         ------      ----&lt;/span&gt;
&lt;span class="c"&gt;# 80/tcp                     ALLOW       Anywhere&lt;/span&gt;
&lt;span class="c"&gt;# 443/tcp                    ALLOW       Anywhere&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yet hitting the site over the internet yielded dead silence. No handshakes, no access logs, no errors in Caddy’s journal. Packets were disappearing into the void before they ever reached the virtual network interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Dead Ends
&lt;/h2&gt;

&lt;p&gt;Before finding the real culprit, I burned an hour chasing ghosts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flushing DNS caches:&lt;/strong&gt; I assumed propagation lag. I queried Cloudflare (&lt;code&gt;1.1.1.1&lt;/code&gt;) and Google (&lt;code&gt;8.8.8.8&lt;/code&gt;) directly via &lt;code&gt;dig +short mydomain.com&lt;/code&gt;. The IP matched instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scouring Caddyfile syntax:&lt;/strong&gt; I stripped the configuration down to a static &lt;code&gt;respond "hello world"&lt;/code&gt; block, suspecting an edge-case redirect loop. Still unreachable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Questioning Tailscale:&lt;/strong&gt; I had installed Tailscale earlier for remote access. I wondered if MagicDNS or subnet routing had hijacked default routes or confused bind interfaces. Disabling Tailscale changed nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrutinizing iptables:&lt;/strong&gt; I dumped the raw packet chains (&lt;code&gt;iptables -L -n -v&lt;/code&gt;). Packets on the host loopback incremented; WAN counters sat dead at zero.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. The Diagnosis: The Two Commands That Cracked It
&lt;/h2&gt;

&lt;p&gt;The realization clicked when I stepped back from application configs and checked basic network interfaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 1: Interface Inspection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interface (&lt;code&gt;eth0&lt;/code&gt;) didn't carry the public IP shown on my invoice. Instead, it held an address in the carrier-grade/shared range: &lt;code&gt;10.x.x.x&lt;/code&gt; (or &lt;code&gt;100.64.x.x&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;The provider was using 1:N NAT. The public IP assigned to my account belonged to a perimeter gateway shared across multiple tenants. Unless the provider explicitly routed inbound traffic on low ports to my private IP, my machine would never receive an unsolicited TCP handshake on port 80 or 443.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 2: Loopback TLS Validation
&lt;/h3&gt;

&lt;p&gt;To verify whether the server stack itself was healthy, I forced a local request against the local interface while passing the domain header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-Iv&lt;/span&gt; https://localhost &lt;span class="nt"&gt;--resolve&lt;/span&gt; mydomain.com:443:127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TLS handshake succeeded immediately. Caddy served the certificate, completed the negotiation, and returned the payload without complaint. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The verdict:&lt;/strong&gt; The application was fine. Inbound routing was non-existent.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Why the Standard Fixes Fell Short
&lt;/h2&gt;

&lt;p&gt;In this scenario, traditional workarounds fall into two buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Begging support for port forwarding:&lt;/strong&gt; Some budget hosts offer NAT port mapping via their control panel. But they usually hand you non-standard ports (e.g., mapping external port &lt;code&gt;18443&lt;/code&gt; to internal &lt;code&gt;443&lt;/code&gt;). That breaks zero-configuration web browsing; nobody wants to type &lt;code&gt;mydomain.com:18443&lt;/code&gt; into a mobile browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paying for a dedicated public IPv4:&lt;/strong&gt; That often costs $2 to $4 a month extra. On a $3/month hobby box, that doubles or triples your recurring spend. It defeats the entire point of hunting down a bargain VPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than pay more money or settle for ugly port numbers, I flipped the traffic model upside down: &lt;strong&gt;stop listening for incoming connections, and punch out instead.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The Architecture: Split-Horizon with Tunnels
&lt;/h2&gt;

&lt;p&gt;The design splits traffic strictly between public visitors and private management:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2t4mczvyiquj1guvihoa.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2t4mczvyiquj1guvihoa.jpg" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;br&gt;
Format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Public Web Visitor]
        │
        ▼ (HTTPS)
 [Cloudflare Edge]
        │
        ▼ (Encrypted QUIC / Outbound Tunnel)
[cloudflared daemon] ─── (localhost:443) ───► [Caddy Web Server] ───► [App]
                                                     ▲
                                                     │ (Tailscale IP only)
[My Laptop / Workstation] ── (Encrypted Mesh) ───────┴─── [Admin / SSH / DB]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 1: Public Ingress (Cloudflare Tunnel + Caddy)
&lt;/h3&gt;

&lt;p&gt;Instead of waiting for incoming SYN packets on ports 80/443, the lightweight &lt;code&gt;cloudflared&lt;/code&gt; daemon runs locally on the VPS and establishes persistent, outbound-only connections to Cloudflare's edge data centers.&lt;/p&gt;

&lt;p&gt;Because the connection originates from &lt;em&gt;inside&lt;/em&gt; the box, the provider's NAT gateway allows it out freely. Public users hit Cloudflare, which shuttles requests down the tunnel to &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To keep HTTPS clean between &lt;code&gt;cloudflared&lt;/code&gt; and Caddy, Caddy acquires valid certificates using the &lt;strong&gt;DNS-01 ACME challenge&lt;/strong&gt; (via the Cloudflare API plugin). This skips HTTP-01 port-80 challenges entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# /etc/caddy/Caddyfile
mydomain.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }

    reverse_proxy localhost:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: Private Control Plane (Tailscale)
&lt;/h3&gt;

&lt;p&gt;SSH and administrative dashboards are stripped off the public internet entirely. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The public firewall drops all inbound traffic on &lt;code&gt;eth0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tailscale binds to its virtual &lt;code&gt;tailscale0&lt;/code&gt; interface.&lt;/li&gt;
&lt;li&gt;SSH listens exclusively on the Tailscale mesh IP (&lt;code&gt;100.x.y.z&lt;/code&gt;), secured by WireGuard keys and two-factor SSO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No open ports. No brute-force bot sweeps hitting port 22.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Gotchas That Cost Me the Most Time
&lt;/h2&gt;

&lt;p&gt;Making the components talk to each other was fast. Dealing with subtle edge-case behaviors took hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 1: Cloudflare 502 via Origin Server Name (SNI)
&lt;/h3&gt;

&lt;p&gt;Once the tunnel came up, requests through the browser threw immediate &lt;code&gt;502 Bad Gateway&lt;/code&gt; errors. &lt;/p&gt;

&lt;p&gt;If your tunnel points to &lt;code&gt;https://localhost:443&lt;/code&gt;, &lt;code&gt;cloudflared&lt;/code&gt; attempts a TLS handshake using &lt;code&gt;localhost&lt;/code&gt; as the SNI. Caddy only holds certificates for &lt;code&gt;mydomain.com&lt;/code&gt;, so it terminates the handshake. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; In the Cloudflare Tunnel ingress configuration, explicitly configure the &lt;code&gt;originServerName&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ~/.cloudflared/config.yml&lt;/span&gt;
&lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mydomain.com&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://localhost:443&lt;/span&gt;
    &lt;span class="na"&gt;originRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;originServerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mydomain.com&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http_status:404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gotcha 2: Bot Fight Mode Killed Google's Sitemap Fetch
&lt;/h3&gt;

&lt;p&gt;A few days later, Google Search Console threw recurring errors: "Couldn't fetch sitemap."&lt;/p&gt;

&lt;p&gt;Cloudflare's default "Bot Fight Mode" aggressively challenges requests that fail standard browser heuristic checks. Google's crawler received managed challenge pages instead of XML. Turning off basic Bot Fight Mode in favor of granular WAF rules resolved crawler access immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 3: The Caching SPA Fallback Trap
&lt;/h3&gt;

&lt;p&gt;My frontend router was configured to return &lt;code&gt;index.html&lt;/code&gt; on 404s. During an asset deployment, a hashed CSS chunk (&lt;code&gt;chunk-7a8b.css&lt;/code&gt;) failed to upload. &lt;/p&gt;

&lt;p&gt;Cloudflare dutifully cached the &lt;code&gt;200 OK&lt;/code&gt; response containing &lt;code&gt;index.html&lt;/code&gt; under the CSS file's URL. When the new bundle loaded, browsers attempted to parse HTML as CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refused to apply style from '...' because its MIME type ('text/html') is not a supported stylesheet MIME type.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had to purge the Cloudflare cache and set strict cache-control headers in Caddy for static assets to keep fallback paths from being stored upstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Verification: Smoke Tests &amp;amp; Health Checks
&lt;/h2&gt;

&lt;p&gt;To make sure everything stays intact across reboots or upstream outages, I keep a quick verification script (&lt;code&gt;scripts/verify-networking.sh&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"==&amp;gt; 1. Checking local services..."&lt;/span&gt;
systemctl is-active &lt;span class="nt"&gt;--quiet&lt;/span&gt; caddy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✔ Caddy is running"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✘ Caddy down"&lt;/span&gt;
systemctl is-active &lt;span class="nt"&gt;--quiet&lt;/span&gt; cloudflared &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✔ cloudflared is running"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✘ cloudflared down"&lt;/span&gt;
systemctl is-active &lt;span class="nt"&gt;--quiet&lt;/span&gt; tailscaled &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✔ tailscale is running"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✘ tailscale down"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;==&amp;gt; 2. Testing local TLS resolution..."&lt;/span&gt;
&lt;span class="nv"&gt;HTTP_STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://localhost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resolve&lt;/span&gt; mydomain.com:443:127.0.0.1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HTTP_STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="o"&gt;(&lt;/span&gt;200|301|302&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✔ Local TLS handshake OK (&lt;/span&gt;&lt;span class="nv"&gt;$HTTP_STATUS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✘ Local TLS failed (&lt;/span&gt;&lt;span class="nv"&gt;$HTTP_STATUS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;==&amp;gt; 3. Testing public ingress..."&lt;/span&gt;
&lt;span class="nv"&gt;EDGE_STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://mydomain.com&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EDGE_STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="o"&gt;(&lt;/span&gt;200|301|302&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✔ Public edge return OK (&lt;/span&gt;&lt;span class="nv"&gt;$EDGE_STATUS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✘ Public edge failed (&lt;/span&gt;&lt;span class="nv"&gt;$EDGE_STATUS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;==&amp;gt; 4. Checking Tailscale connectivity..."&lt;/span&gt;
tailscale ping &lt;span class="nt"&gt;-c&lt;/span&gt; 1 my-laptop &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✔ Tailscale mesh reachable"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✘ Tailscale unreachable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this after kernel updates or container restarts takes five seconds and catches detached tunnels instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Making It Repeatable: Packaging It as a Skill
&lt;/h2&gt;

&lt;p&gt;After setting this up by hand, I didn't want to repeat the manual troubleshooting on my next low-cost VPS instance. &lt;/p&gt;

&lt;p&gt;I codified the process into an agent skill definition (&lt;code&gt;SKILL.md&lt;/code&gt;) that documents the exact checks, common configuration blocks, and diagnostic steps for automated runners and AI tooling. It acts as an executable runbook covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial interface NAT sanity checks&lt;/li&gt;
&lt;li&gt;Automated Caddy installation with Cloudflare DNS plugins&lt;/li&gt;
&lt;li&gt;Systemd service templates for &lt;code&gt;cloudflared&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fail-safe firewall rule generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository and automation manifests are available here:&lt;br&gt;&lt;br&gt;
👉 &lt;code&gt;https://github.com/AnandaAp/vps-hybrid-networking&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. When This Fits (And When It's Overkill)
&lt;/h2&gt;

&lt;p&gt;This pattern solved my problem cleanly, but it isn't a universal hammer.&lt;/p&gt;

&lt;h3&gt;
  
  
  This architecture shines when:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You're using NAT VPS providers:&lt;/strong&gt; If you're running on ultra-cheap VPS hosts, internal home servers, or shared-IP VPS tiers where port 80/443 ingress is blocked or charged at a premium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want total port closure:&lt;/strong&gt; You want zero exposed listening ports on the public interface, minimizing bot scanning and automated exploit probes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need split administrative access:&lt;/strong&gt; Tailscale provides clean, encrypted access to admin panels, metrics, and SSH without maintaining a public bastion host.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Skip it if:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You run high-throughput raw TCP/UDP services:&lt;/strong&gt; Cloudflare Free tunnels have bandwidth considerations and are designed primarily for HTTP/HTTPS/WebSockets. High-bandwidth media streaming or non-HTTP traffic (like game servers) will struggle here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor lock-in is a dealbreaker:&lt;/strong&gt; Relying on Cloudflare for ingress ties your DNS and perimeter routing directly to their ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already have a routable static public IPv4:&lt;/strong&gt; If inbound 80 and 443 are unobstructed, native Caddy with automated HTTP-01 challenges remains the simplest, most direct deployment path available.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>selfhosted</category>
      <category>claude</category>
      <category>cloudflarechallenge</category>
    </item>
  </channel>
</rss>
