The Server That Couldn't Call Home
Alflora Research · 01 Aug 2026
Tl;dr
- The server cannot reach its own public IP — hairpin NAT doesn't happen on this network.
- Anything that "phoned home" through the public hostname silently didn't.
- Auto-deploy webhooks, health checks, and service-to-service calls were all quietly broken.
- The fix: internal traffic over container networks and 127.0.0.1; public hostnames are for the public.
The symptom: The bug report made no sense: the Gitea webhook fired, the Pages app was healthy, the URL was correct — and yet auto-deploy simply never happened. No error. No timeout worth mentioning. Just a deploy that worked when triggered by hand and never when triggered by the git push that was supposed to trigger it. The webhook was being sent to pages.alflora.app, the public hostname, from Gitea. Which runs on the same machine.
From the outside, everything was fine — users could reach every service. From the inside, the server was shouting at its own front door and wondering why nobody answered.
The diagnosis: The answer is a piece of home-router reality called hairpin NAT. When a machine inside a NAT network connects to the network's own public IP, the router is supposed to loop the traffic back inside — hairpin it. Many routers simply don't. This one doesn't. So any packet this server sends to its own public address goes out toward the router and evaporates. Requests to alflora.app from the server itself fail not because the service is down, but because the road between the server and its own front door doesn't exist.
The insidious part: everything looks configured correctly. DNS resolves, certificates are valid, the service is up, the logs show nothing because the request never arrives. It is a failure mode with zero error messages.
The rewire: Once you know the road is missing, you stop using it. Every internal consumer was moved off public hostnames: service-to-service traffic goes over the shared docker network by container name; host-level checks go to 127.0.0.1 with the right Host header or SNI override — curl --resolve exists precisely for this. The status checker, the webhooks, the health probes: all rewired to addresses that don't require leaving the house to knock on your own door.
The rule that came out of it is now doctrine: public hostnames are for the public; localhost is for family. Any config on this network that references a public *.alflora.app name from inside the network is treated as a bug on sight.
The lesson: Home infrastructure has a whole category of ghosts like this — behaviors that datacenter networks make impossible and residential routers make invisible. You will not find them in tutorials, because tutorials assume datacenter roads. The only reliable detection is the one written on the wall of this site: test it yourself, don't believe the changelog. Or the config. Or the green checkmark. The request either arrives or it doesn't, and the only way to know is to send it and watch.