[{"data":1,"prerenderedAt":820},["ShallowReactive",2],{"blog-posts":3},[4,674],{"id":5,"title":6,"author":7,"body":8,"date":658,"description":659,"draft":660,"extension":661,"image":662,"meta":663,"navigation":425,"path":670,"seo":671,"stem":672,"__hash__":673},"blog\u002Fblog\u002Fsplit-horizon-dns-homelab.md","Homelab Split-Horizon DNS: One URL, Two Networks","Connor Abbas",{"type":9,"value":10,"toc":651},"minimark",[11,23,37,40,59,62,72,78,81,86,95,104,110,120,123,126,140,146,162,169,175,187,200,203,263,266,269,273,282,291,304,307,475,494,504,508,511,524,531,542,545,572,575,589,592,596,602,631,634,638,641,644,647],[12,13,14,15,22],"p",{},"My homelab runs on a repurposed Lenovo ThinkPad with Ubuntu Server and ",[16,17,21],"a",{"href":18,"rel":19},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002F",[20],"nofollow","Docker Compose",". It hosts the usual collection of services that starts sensibly and then grows: Immich, Jellyfin, AdGuard Home, monitoring, file management, and a few Docker administration tools.",[12,24,25,26,30,31,36],{},"I wanted each service to have one normal HTTPS address no matter where I was. ",[27,28,29],"code",{},"immich.home.605software.com"," should work on my couch, and it should still work when I am away from home and connected through ",[16,32,35],{"href":33,"rel":34},"https:\u002F\u002Ftailscale.com\u002Fkb\u002F1151\u002Fwhat-is-tailscale\u002F",[20],"Tailscale",". I didn't want separate bookmarks, public web ports, or browser certificate warnings.",[12,38,39],{},"The setup sounds more complicated than it is. Each piece has one job:",[41,42,43,47,50,53,56],"ul",{},[44,45,46],"li",{},"The router tells devices on my home network to use AdGuard for DNS.",[44,48,49],{},"DNS sends local and remote devices to different private addresses.",[44,51,52],{},"Tailscale sends lookups for my homelab domain to Cloudflare when I am connected remotely.",[44,54,55],{},"Tailscale gives remote devices a private route back home.",[44,57,58],{},"Traefik handles HTTPS and sends each hostname to the right container.",[12,60,61],{},"Put together, the request paths look like this:",[63,64,70],"pre",{"className":65,"code":67,"language":68,"meta":69},[66],"language-text","At home: Browser -> AdGuard -> LAN IP -> Traefik -> Immich\n","text","",[27,71,67],{"__ignoreMap":69},[63,73,76],{"className":74,"code":75,"language":68,"meta":69},[66],"Away: Browser -> Tailscale split DNS -> Cloudflare -> Tailscale IP -> Traefik -> Immich\n",[27,77,75],{"__ignoreMap":69},[12,79,80],{},"Only the route to the server changes. Once the request reaches the server, Traefik and Docker handle it the same way.",[82,83,85],"h2",{"id":84},"dns-picks-the-route","DNS picks the route",[12,87,88,89,94],{},"On my home network, the router hands out my ",[16,90,93],{"href":91,"rel":92},"https:\u002F\u002Fgithub.com\u002FAdguardTeam\u002FAdGuardHome",[20],"AdGuard Home"," instance as the DNS server through DHCP. Devices pick that setting up automatically when they join the network, so I don't have to configure each one by hand.",[12,96,97,98,103],{},"AdGuard has a ",[16,99,102],{"href":100,"rel":101},"https:\u002F\u002Fgithub.com\u002FAdguardTeam\u002FAdGuardHome\u002Fwiki\u002FConfiguration#dns-rewrites",[20],"wildcard DNS rewrite",":",[63,105,108],{"className":106,"code":107,"language":68,"meta":69},[66],"*.home.605software.com -> 192.168.1.100\n",[27,109,107],{"__ignoreMap":69},[12,111,112,115,116,119],{},[27,113,114],{},"192.168.1.100"," is the homelab server's static LAN address. The wildcard means AdGuard returns that same address for Immich, Jellyfin, and anything else below ",[27,117,118],{},"home.605software.com",". Local requests go straight across the LAN instead of taking a detour through Tailscale.",[12,121,122],{},"I don't advertise a public resolver as a secondary DNS server on the LAN. Clients don't always treat secondary DNS as a strict fallback, so they could skip AdGuard and get the remote answer instead. A second local resolver would be the better option if I wanted DNS redundancy.",[12,124,125],{},"At home, I disconnect my devices from Tailscale, so the router-provided AdGuard resolver handles these lookups. When I am away, I connect to Tailscale and its DNS configuration takes over the homelab domain.",[12,127,128,129,133,134,137,138,103],{},"In the Tailscale admin console under ",[130,131,132],"strong",{},"Network -> DNS",", I added Cloudflare Public DNS as a nameserver and enabled ",[130,135,136],{},"Restrict to domain"," for ",[27,139,118],{},[63,141,144],{"className":142,"code":143,"language":68,"meta":69},[66],"Nameserver: Cloudflare Public DNS (1.1.1.1)\nRestricted domain: home.605software.com\n",[27,145,143],{"__ignoreMap":69},[12,147,148,149,154,155,157,158,161],{},"Tailscale calls this ",[16,150,153],{"href":151,"rel":152},"https:\u002F\u002Ftailscale.com\u002Fkb\u002F1054\u002Fdns#restricted-nameservers",[20],"split DNS",". A connected device sends only ",[27,156,118],{}," lookups to Cloudflare's resolver. Everything else continues through the device's normal DNS setup. The device also needs to have ",[130,159,160],{},"Use Tailscale DNS settings"," enabled for the rule to apply.",[12,163,164,165,168],{},"Cloudflare's resolver then finds the matching wildcard ",[27,166,167],{},"A"," record in my public DNS zone:",[63,170,173],{"className":171,"code":172,"language":68,"meta":69},[66],"*.home.605software.com -> 100.x.y.z\n",[27,174,172],{"__ignoreMap":69},[12,176,177,178,186],{},"This time the answer is the server's stable Tailscale address. Tailscale uses addresses from the reserved ",[16,179,182,185],{"href":180,"rel":181},"https:\u002F\u002Ftailscale.com\u002Fkb\u002F1015\u002F100.x-addresses\u002F",[20],[27,183,184],{},"100.64.0.0\u002F10"," range",". Publishing that address in DNS doesn't make the server reachable from the public internet; a device still needs to be connected to my tailnet before it has a route there.",[12,188,189,190,195,196,199],{},"I host the record with ",[16,191,194],{"href":192,"rel":193},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fdns\u002F",[20],"Cloudflare DNS",", but it's set to ",[130,197,198],{},"DNS-only",". Cloudflare answers the lookup and then gets out of the way. It can't proxy traffic to a private Tailscale address, and I don't need it to.",[12,201,202],{},"The public record would usually resolve through whatever DNS server a remote network provides. The Tailscale rule makes that behavior predictable: whenever I am connected to my tailnet, lookups for the homelab domain go to Cloudflare and return the Tailscale address without changing DNS for anything else.",[204,205,206,225],"table",{},[207,208,209],"thead",{},[210,211,212,216,219,222],"tr",{},[213,214,215],"th",{},"Device state",[213,217,218],{},"Resolver",[213,220,221],{},"DNS answer",[213,223,224],{},"Path to the server",[226,227,228,244],"tbody",{},[210,229,230,234,237,241],{},[231,232,233],"td",{},"Home LAN, Tailscale disconnected",[231,235,236],{},"AdGuard",[231,238,239],{},[27,240,114],{},[231,242,243],{},"Directly over the LAN",[210,245,246,249,255,260],{},[231,247,248],{},"Connected to Tailscale",[231,250,251,252],{},"Cloudflare ",[27,253,254],{},"1.1.1.1",[231,256,257],{},[27,258,259],{},"100.x.y.z",[231,261,262],{},"Through Tailscale",[12,264,265],{},"This is the split-horizon part of the setup: one hostname, with a different answer depending on which DNS resolver receives the question.",[12,267,268],{},"There is one small consequence worth knowing. If I leave Tailscale connected while I am at home, its restricted nameserver can take precedence over AdGuard. The service still works, but the request may use the Tailscale address instead of staying on the LAN.",[82,270,272],{"id":271},"both-paths-meet-at-traefik","Both paths meet at Traefik",[12,274,275,276,281],{},"DNS gets the browser to the correct machine, but it can't choose a container. Every service points to the same LAN or Tailscale address, so the next handoff belongs to ",[16,277,280],{"href":278,"rel":279},"https:\u002F\u002Ftraefik.io\u002Ftraefik\u002F",[20],"Traefik",".",[12,283,284,285,290],{},"Traefik listens on the server's web ports and reads routing labels through its ",[16,286,289],{"href":287,"rel":288},"https:\u002F\u002Fdoc.traefik.io\u002Ftraefik\u002Fproviders\u002Fdocker\u002F",[20],"Docker provider",". When a request arrives, it looks at the hostname, presents the HTTPS certificate, and forwards the request to the matching container.",[12,292,293,294,299,300,303],{},"I use the ",[16,295,298],{"href":296,"rel":297},"https:\u002F\u002Fgithub.com\u002Fconnorabbas\u002Ftraefik-docker-compose\u002Ftree\u002Fmaster\u002Fdns-challenge",[20],"DNS challenge setup"," from my Traefik Compose project. It creates a shared network named ",[27,301,302],{},"traefik_proxy",". Applications join that network only when Traefik needs to reach them.",[12,305,306],{},"Here is the relevant part of my Immich service:",[63,308,312],{"className":309,"code":310,"language":311,"meta":69,"style":69},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","services:\n  immich-server:\n    image: ghcr.io\u002Fimmich-app\u002Fimmich-server:release\n    networks:\n      - default\n      - traefik_proxy\n    labels:\n      - traefik.enable=true\n      - traefik.http.routers.immich.rule=Host(`immich.home.605software.com`)\n      - traefik.http.routers.immich.entrypoints=websecure\n      - traefik.http.routers.immich.tls.certresolver=letsencrypt\n      - traefik.http.services.immich.loadbalancer.server.port=2283\n\n...\n\nnetworks:\n  default:\n  traefik_proxy:\n    external: true\n","yaml",[27,313,314,327,335,347,355,364,372,380,388,396,404,412,420,427,434,439,447,455,463],{"__ignoreMap":69},[315,316,319,323],"span",{"class":317,"line":318},"line",1,[315,320,322],{"class":321},"swJcz","services",[315,324,326],{"class":325},"sMK4o",":\n",[315,328,330,333],{"class":317,"line":329},2,[315,331,332],{"class":321},"  immich-server",[315,334,326],{"class":325},[315,336,338,341,343],{"class":317,"line":337},3,[315,339,340],{"class":321},"    image",[315,342,103],{"class":325},[315,344,346],{"class":345},"sfazB"," ghcr.io\u002Fimmich-app\u002Fimmich-server:release\n",[315,348,350,353],{"class":317,"line":349},4,[315,351,352],{"class":321},"    networks",[315,354,326],{"class":325},[315,356,358,361],{"class":317,"line":357},5,[315,359,360],{"class":325},"      -",[315,362,363],{"class":345}," default\n",[315,365,367,369],{"class":317,"line":366},6,[315,368,360],{"class":325},[315,370,371],{"class":345}," traefik_proxy\n",[315,373,375,378],{"class":317,"line":374},7,[315,376,377],{"class":321},"    labels",[315,379,326],{"class":325},[315,381,383,385],{"class":317,"line":382},8,[315,384,360],{"class":325},[315,386,387],{"class":345}," traefik.enable=true\n",[315,389,391,393],{"class":317,"line":390},9,[315,392,360],{"class":325},[315,394,395],{"class":345}," traefik.http.routers.immich.rule=Host(`immich.home.605software.com`)\n",[315,397,399,401],{"class":317,"line":398},10,[315,400,360],{"class":325},[315,402,403],{"class":345}," traefik.http.routers.immich.entrypoints=websecure\n",[315,405,407,409],{"class":317,"line":406},11,[315,408,360],{"class":325},[315,410,411],{"class":345}," traefik.http.routers.immich.tls.certresolver=letsencrypt\n",[315,413,415,417],{"class":317,"line":414},12,[315,416,360],{"class":325},[315,418,419],{"class":345}," traefik.http.services.immich.loadbalancer.server.port=2283\n",[315,421,423],{"class":317,"line":422},13,[315,424,426],{"emptyLinePlaceholder":425},true,"\n",[315,428,430],{"class":317,"line":429},14,[315,431,433],{"class":432},"sBMFI","...\n",[315,435,437],{"class":317,"line":436},15,[315,438,426],{"emptyLinePlaceholder":425},[315,440,442,445],{"class":317,"line":441},16,[315,443,444],{"class":321},"networks",[315,446,326],{"class":325},[315,448,450,453],{"class":317,"line":449},17,[315,451,452],{"class":321},"  default",[315,454,326],{"class":325},[315,456,458,461],{"class":317,"line":457},18,[315,459,460],{"class":321},"  traefik_proxy",[315,462,326],{"class":325},[315,464,466,469,471],{"class":317,"line":465},19,[315,467,468],{"class":321},"    external",[315,470,103],{"class":325},[315,472,474],{"class":473},"sfNiH"," true\n",[12,476,477,478,481,482,485,486,489,490,493],{},"Those labels give Traefik everything it needs to complete the handoff. The ",[27,479,480],{},"Host"," rule matches the address in the browser, ",[27,483,484],{},"websecure"," handles the HTTPS request, ",[27,487,488],{},"letsencrypt"," selects the certificate resolver, and port ",[27,491,492],{},"2283"," tells Traefik where Immich is listening inside Docker.",[12,495,496,497,500,501,503],{},"Only ",[27,498,499],{},"immich-server",", the web-facing part of Immich, joins ",[27,502,302],{},". PostgreSQL, Valkey, and its machine-learning service stay on the project's private default network. Traefik can reach the application without also getting access to every supporting container.",[82,505,507],{"id":506},"traefik-gets-the-certificate-through-dns","Traefik gets the certificate through DNS",[12,509,510],{},"The remaining piece is HTTPS. A normal Let's Encrypt HTTP challenge would require Let's Encrypt to connect to the server from the public internet, which would mean opening a port on my router.",[12,512,513,514,519,520,523],{},"Instead, Traefik uses the ",[16,515,518],{"href":516,"rel":517},"https:\u002F\u002Fletsencrypt.org\u002Fdocs\u002Fchallenge-types\u002F#dns-01-challenge",[20],"DNS-01 challenge",". When it needs a certificate, Traefik uses a narrowly scoped Cloudflare API token to create a temporary ",[27,521,522],{},"TXT"," record. Let's Encrypt checks that record to confirm I control the domain, then Traefik removes it and stores the certificate.",[12,525,526,527,530],{},"I set ",[27,528,529],{},"DOMAIN"," to the internal zone:",[63,532,536],{"className":533,"code":534,"language":535,"meta":69,"style":69},"language-dotenv shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","DOMAIN=home.605software.com\n","dotenv",[27,537,538],{"__ignoreMap":69},[315,539,540],{"class":317,"line":318},[315,541,534],{},[12,543,544],{},"The TLS options tell Traefik to request a certificate for both the base name and every service below it:",[63,546,548],{"className":309,"code":547,"language":311,"meta":69,"style":69},"- --entrypoints.websecure.http.tls.certresolver=letsencrypt\n- --entrypoints.websecure.http.tls.domains[0].main=${DOMAIN}\n- --entrypoints.websecure.http.tls.domains[0].sans=*.${DOMAIN}\n",[27,549,550,558,565],{"__ignoreMap":69},[315,551,552,555],{"class":317,"line":318},[315,553,554],{"class":325},"-",[315,556,557],{"class":345}," --entrypoints.websecure.http.tls.certresolver=letsencrypt\n",[315,559,560,562],{"class":317,"line":329},[315,561,554],{"class":325},[315,563,564],{"class":345}," --entrypoints.websecure.http.tls.domains[0].main=${DOMAIN}\n",[315,566,567,569],{"class":317,"line":337},[315,568,554],{"class":325},[315,570,571],{"class":345}," --entrypoints.websecure.http.tls.domains[0].sans=*.${DOMAIN}\n",[12,573,574],{},"Cloudflare shows up in three places in this setup, and they are easy to mix together:",[41,576,577,583,586],{},[44,578,579,580,582],{},"Cloudflare's ",[27,581,254],{}," resolver handles the homelab lookups that Tailscale sends to it.",[44,584,585],{},"Cloudflare's authoritative DNS hosts the public wildcard record.",[44,587,588],{},"Cloudflare's API lets Traefik create the temporary record needed for certificate validation.",[12,590,591],{},"These are all DNS tasks. Application traffic never actually passes through Cloudflare.",[82,593,595],{"id":594},"following-one-request-all-the-way-through","Following one request all the way through",[12,597,598,599,601],{},"When I open ",[27,600,29],{},", the whole setup comes together:",[603,604,605,608,611,614,617,620],"ol",{},[44,606,607],{},"My device asks its current DNS resolver for the address.",[44,609,610],{},"At home, AdGuard answers with the server's LAN IP. When I am connected remotely, Tailscale sends the lookup to Cloudflare's resolver, which answers with the server's Tailscale IP.",[44,612,613],{},"My device uses either the LAN or Tailscale to reach the server.",[44,615,616],{},"The browser connects to Traefik over HTTPS.",[44,618,619],{},"Traefik presents the wildcard certificate and matches the Immich hostname.",[44,621,622,623,625,626,628,629,281],{},"Traefik sends the request across ",[27,624,302],{}," to port ",[27,627,492],{}," on ",[27,630,499],{},[12,632,633],{},"The browser sees the same hostname and trusted certificate in both places. The only part that changes is how the request reaches Traefik.",[82,635,637],{"id":636},"what-keeps-it-private","What keeps it private?",[12,639,640],{},"There is no port forwarding from my router to the homelab server. Devices at home can reach it over the LAN, while remote devices need to be authenticated to my tailnet. The public DNS record tells those devices which address to use, but it does not create a public route to that address.",[12,642,643],{},"Private networking still doesn't replace basic security. The Cloudflare token belongs in an ignored environment file or secret store, administrative services still need strong authentication, and access to the Docker socket gives Traefik significant control over the host. I also avoid publishing application ports unless a service genuinely needs one; Traefik is the normal path for all web traffic.",[12,645,646],{},"The result has been pleasantly uneventful. DNS picks the right private route, Tailscale carries remote traffic, Traefik handles certificates and routing, and Docker keeps the application pieces connected without exposing all of them. Most importantly, I only need one URL for each service, no matter where I happen to be.",[648,649,650],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":69,"searchDepth":329,"depth":329,"links":652},[653,654,655,656,657],{"id":84,"depth":329,"text":85},{"id":271,"depth":329,"text":272},{"id":506,"depth":329,"text":507},{"id":594,"depth":329,"text":595},{"id":636,"depth":329,"text":637},"2026-09-13","How I combine Tailscale, AdGuard Home, Traefik, and Docker to give private homelab services the same HTTPS addresses at home and on the go.",false,"md","\u002Fimages\u002Fblog\u002Fsplit-horizon-dns.png",{"tags":664},[665,666,667,668,669],"homelab","docker","tailscale","traefik","dns","\u002Fblog\u002Fsplit-horizon-dns-homelab",{"title":6,"description":659},"blog\u002Fsplit-horizon-dns-homelab","KMnDV7kCAxq9HO7ViOPIjPH1U5VuvQfB2l2EbG4PhUM",{"id":675,"title":676,"author":7,"body":677,"date":812,"description":813,"draft":660,"extension":661,"image":814,"meta":815,"navigation":425,"path":816,"seo":817,"stem":818,"__hash__":819},"blog\u002Fblog\u002Fpowered-by-pangolin.md","Powered by Pangolin",{"type":9,"value":678,"toc":804},[679,687,690,699,705,709,712,715,719,722,725,728,732,740,747,751,757,760,764,779,782,791,795,798,801],[12,680,681,682,686],{},"Most of the time, I would deploy a site like this to a standard VPS. ",[16,683,280],{"href":684,"rel":685},"https:\u002F\u002Ftraefik.io\u002Ftraefik",[20]," would run beside the application stack, Docker labels would define the routes, and the server would handle both public ingress and the application itself. It is a proven, straightforward approach.",[12,688,689],{},"For 605software.com, I wanted to try something different. I already owned a VPS, but I also had capable hardware sitting at home. Using it meant lowering the cost of hosting additional projects, learning more about self-hosted infrastructure, and putting equipment I already owned to work.",[12,691,692,693,698],{},"The site runs in Docker on a server in my basement, while a small public VPS with just 2 vCPUs and 2 GB of RAM runs a self-hosted instance of ",[16,694,697],{"href":695,"rel":696},"https:\u002F\u002Fpangolin.net\u002F",[20],"Pangolin"," at the edge. The VPS manages public DNS, TLS certificates, ports 80 and 443, and the policies that determine what reaches the site, including UFW, Fail2ban, and other standard security measures.",[63,700,703],{"className":701,"code":702,"language":68,"meta":69},[66],"Internet -> Pangolin VPS -> Gerbil\u002FNewt tunnel -> home server Docker containers\n",[27,704,702],{"__ignoreMap":69},[82,706,708],{"id":707},"making-use-of-hardware-i-already-owned","Making use of hardware I already owned",[12,710,711],{},"The home server is an older Dell OptiPlex desktop with an Intel Core i5-7500, 8 GB of RAM, and a 500 GB SATA SSD. It is not fancy hardware, but it is more than capable of hosting this site and several other personal or business services.",[12,713,714],{},"That capacity is the point. A small marketing site does not need a large cloud instance, and neither do many internal tools or low-traffic applications. The OptiPlex lets me use hardware I already own for the workloads that fit it, while keeping the VPS focused on public ingress.",[82,716,718],{"id":717},"pangolin-sits-at-the-edge","Pangolin sits at the edge",[12,720,721],{},"The VPS is the only machine exposed to the public internet. Pangolin's bundled Traefik instance handles HTTPS and reverse proxying there, so I do not need a second public reverse proxy or an open port on my home router.",[12,723,724],{},"I specifically did not want to rely on a proprietary tunneling service such as Cloudflare Tunnel for this setup. Pangolin is open source, gives me direct control of the edge infrastructure, and keeps the routing model understandable. I also did not want to expose services by forwarding ports from my home router (for obvious reasons). Avoiding that direct inbound path keeps the home network boundary smaller and easier to manage.",[12,726,727],{},"The split makes responsibilities clear: the VPS accepts public traffic, and the home server runs the applications.",[82,729,731],{"id":730},"the-connection-starts-at-home","The connection starts at home",[12,733,734,739],{},[16,735,738],{"href":736,"rel":737},"https:\u002F\u002Fgithub.com\u002Ffosrl\u002Fnewt",[20],"Newt"," runs alongside the application containers on the home server and creates an outbound tunnel to the VPS. Because the connection originates from inside the network, there is no inbound port forwarding and no need to reveal the home IP address.",[12,741,742,743,746],{},"When a request arrives for 605software.com, Pangolin sends it through that established tunnel to the Nuxt container running the site. Docker DNS resolves the service name on the shared ",[27,744,745],{},"pangolin_public"," network.",[82,748,750],{"id":749},"keeping-the-application-network-small","Keeping the application network small",[12,752,753,754,756],{},"Only the web-facing application containers join the ",[27,755,745],{}," network. Databases, caches, queues, workers, and the Docker API stay on private networks. The application does not publish a host port; Pangolin reaches it through Newt instead.",[12,758,759],{},"This is a useful default for a small self-hosted setup. Public routing is explicit, and adding another site means creating one HTTP resource with a hostname, Docker service name, and internal port.",[82,761,763],{"id":762},"multiple-services","Multiple services",[12,765,766,767,772,773,778],{},"The OptiPlex also runs ",[16,768,771],{"href":769,"rel":770},"https:\u002F\u002Fumami.is\u002F",[20],"Umami"," for privacy-focused analytics and ",[16,774,777],{"href":775,"rel":776},"https:\u002F\u002Fwww.bugsink.com\u002F",[20],"Bugsink"," for error monitoring. Both let me keep application data on infrastructure I control while avoiding usage-limited hosted analytics plans and paid cloud error-tracking services such as Sentry.",[12,780,781],{},"Where practical, I prefer dependable, open-source software that I can self-host rather than adding another proprietary service or recurring subscription. Pangolin, Umami, and Bugsink all fit that approach without compromising the capabilities this site needs.",[12,783,784,785,790],{},"I am happy with how straightforward the setup was, from routing traffic through Pangolin to bringing each Docker service online. Next, I plan to put the server to work hosting more services, including ",[16,786,789],{"href":787,"rel":788},"https:\u002F\u002Finfisical.com\u002F",[20],"Infisical"," for secrets management.",[82,792,794],{"id":793},"more-than-a-tunnel","More than a tunnel",[12,796,797],{},"This setup uses only a small part of what Pangolin can do. For 605software.com, I primarily use it to route public traffic through an encrypted tunnel to a Docker service on my home server.",[12,799,800],{},"Pangolin can also provide zero-trust network access, securely expose private homelab services, and restrict resources to authenticated users or approved clients. Those capabilities make it useful for administrative tools and internal applications that should never be openly accessible from the internet.",[12,802,803],{},"My current setup is intentionally simple, but Pangolin leaves room to expand the home server without redesigning how remote access and public ingress work.",{"title":69,"searchDepth":329,"depth":329,"links":805},[806,807,808,809,810,811],{"id":707,"depth":329,"text":708},{"id":717,"depth":329,"text":718},{"id":730,"depth":329,"text":731},{"id":749,"depth":329,"text":750},{"id":762,"depth":329,"text":763},{"id":793,"depth":329,"text":794},"2026-09-06","How 605software.com securely routes traffic from a public edge VPS to a server in my basement.","\u002Fimages\u002Fblog\u002Fpowered-by-pangolin-bg.svg",{},"\u002Fblog\u002Fpowered-by-pangolin",{"title":676,"description":813},"blog\u002Fpowered-by-pangolin","Hhx4DExR9gOKsUebFvcKKKynLoWYugLgXu4LYzJXXak",1789315037496]