Expose home services to the internet without opening a single router port and without your home IP ever appearing in DNS β using Pangolin (server on a cheap VPS) + Newt (client at home) + a Cloudflare-managed domain.
This works even behind CGNAT, dynamic IPs, and double-NAT.
How it works
Internet user
β
βΌ
Cloudflare DNS (*.example.com β VPS public IP, grey cloud)
β
βΌ
VPS (public IP β runs pangolin + gerbil + traefik)
β²
β WireGuard tunnel (UDP 51820, initiated outbound from home)
βΌ
Pi / home server (NAT'd, runs newt only)
β
βΌ
Local services (Grafana, Vaultwarden, Home Assistant, etc.)
- The home server only makes outbound connections. No inbound ports, no port forwarding, no UPnP.
- The VPS is the only public IP in DNS. The home WAN IP is never published.
- CGNAT, dynamic home IP, double-NAT β none of it matters.
- TLS certs issued by Let’s Encrypt on the VPS (Traefik handles renewal).
- Auth (SSO / password / PIN / OTP) enforced by Pangolin before traffic ever reaches the home network.
What you need
| Thing | Notes |
|---|---|
| Domain on Cloudflare | Any registrar works; this guide uses Cloudflare DNS. |
| VPS with public IPv4 | 2 GB RAM is enough. Hetzner CX22 ( |
| Home server / Pi 4 | Raspberry Pi OS 64-bit or any arm64/x86 Linux. Docker or Podman installed. |
| 5 minutes on the router | Just to confirm the device has outbound internet. No port forwards needed. |
Cost and exposure summary
| Asset | Public on internet? |
|---|---|
| Home LAN IP | No |
| Home WAN IP | No |
| Router ports | None opened |
| VPS public IP | Yes (only this in DNS) |
| Service URLs | Yes, via *.example.com β VPS β tunnel β home |
Part 1 β Cloudflare DNS
In Cloudflare dashboard for your zone, add:
| Type | Name | Content | Proxy status |
|---|---|---|---|
| A | pangolin |
<VPS public IP> |
DNS only (grey cloud) |
| A | * |
<VPS public IP> |
DNS only (grey cloud) |
Proxy must be OFF (grey cloud). Reasons:
- Traefik on the VPS issues real Let’s Encrypt certs via HTTP-01. Orange cloud breaks that challenge unless you switch to DNS-01.
- WireGuard (UDP 51820) cannot proxy through Cloudflare regardless.
- Orange cloud β 525 / 526 errors and broken renewals.
The wildcard record means every new resource (grafana.example.com, vault.example.com, β¦) works without touching DNS again.
Part 2 β VPS: install Pangolin server
SSH into the VPS as a sudo user.
2.1 Open firewall
sudo ufw allow 22/tcp # keep SSH
sudo ufw allow 80,443/tcp # Traefik
sudo ufw allow 51820/udp # Gerbil / WireGuard
sudo ufw enable
If using a cloud-provider firewall panel instead, open the same three rules there.
2.2 Install Docker (if missing)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
2.3 Run the official installer
mkdir -p ~/pangolin && cd ~/pangolin
curl -fsSL https://digpangolin.com/get-installer.sh -o installer.sh
chmod +x installer.sh
sudo ./installer.sh
Answer the prompts:
| Prompt | Answer |
|---|---|
| Base domain | example.com |
| Dashboard subdomain | pangolin (β pangolin.example.com) |
| Admin email | real address (Let’s Encrypt notices) |
| Enable open signup | no |
| Use Traefik | yes |
| Use CrowdSec | no (add later if wanted) |
The installer writes a docker-compose.yml (pangolin + gerbil + traefik) plus a config/ directory.
2.4 Bring it up
docker compose up -d
docker compose logs -f traefik
Watch for obtained certificate.
If it fails, port 80 is not reachable from the internet β fix the cloud-provider firewall first.
2.5 First login
Browse to https://pangolin.example.com:
- Create the admin user.
- Create an Organisation.
- Create a Site (e.g.
home-pi). - The dashboard now shows:
PANGOLIN_ENDPOINT(=https://pangolin.example.com)NEWT_IDNEWT_SECRET
Copy all three. They go on the home server next.
Part 3 β Home server: install Newt client
SSH into the Pi or home server.
3.1 Create a working directory
mkdir -p ~/pangolin-newt && cd ~/pangolin-newt
3.2 Write the Newt compose file
services:
newt:
image: fosrl/newt:latest
container_name: newt
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=https://pangolin.example.com
- NEWT_ID=replace-with-id-from-dashboard
- NEWT_SECRET=replace-with-secret-from-dashboard
# No ports published β Newt is outbound-only.
Replace the three placeholders with the values from the Pangolin dashboard.
3.3 Start Newt
Docker:
docker compose up -d
docker logs -f newt
Podman (rootless works fine):
podman compose up -d
podman logs -f newt
Look for tunnel established / registered with pangolin. The Site indicator in the dashboard flips to Online within seconds.
The home server now has an outbound WireGuard tunnel to the VPS. Nothing changed on the router.
Part 4 β Expose a local service
Example: Grafana running on the Pi at http://192.168.1.50:3000.
In the Pangolin dashboard:
- Open the Site β Resources β Add Resource.
- Subdomain:
grafanaβ public URL becomeshttps://grafana.example.com. - Target:
http://192.168.1.50:3000. Must be reachable from the Pi. If the service runs on the Pi itself, it must bind to0.0.0.0, not127.0.0.1. - Auth: pick one (SSO, password, PIN, OTP, or public).
- Save.
Visit https://grafana.example.com:
- Real Let’s Encrypt cert (issued on the VPS).
- Pangolin auth page (unless you picked public).
- After auth, the Grafana UI served through the WireGuard tunnel.
Repeat for every service. Wildcard DNS already covers all subdomains.
Running on a Raspberry Pi 4 (4 GB)
Both ghcr.io/fosrl/pangolin and fosrl/newt publish linux/arm64 manifests. A Pi 4 with 4 GB RAM is more than enough β both binaries are Go and idle at ~100 MB combined.
Newt client on Pi (recommended setup): follow Parts 3β4 above. The Pi is purely a tunnel agent; the heavy lifting is on the VPS.
Pangolin server on Pi (advanced): only viable if the Pi has a real public IPv4 with ports 80, 443, and 51820 forwarded from the router β i.e. no CGNAT. Run the same official installer from Part 2. Put ./config/ on an external SSD over USB 3, not the SD card β Traefik and SQLite do constant small writes and SD cards die.
Survival commands
Home server / Pi (Newt):
docker logs -f newt # watch tunnel
docker compose restart # bounce
docker compose pull && docker compose up -d # update
docker compose down # stop
Newt has no local state β wiping the container loses nothing.
VPS (Pangolin server):
docker compose logs -f pangolin
docker compose logs -f gerbil
docker compose logs -f traefik
docker compose pull && docker compose up -d
tar czf pangolin-backup-$(date +%F).tgz config/ # back up SQLite + certs + config
Back up config/ regularly β that directory is the entire state of the server.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Newt: connect: connection refused |
VPS not reachable | Check PANGOLIN_ENDPOINT URL, DNS resolves to VPS IP, UDP 51820 open on VPS firewall. |
| Site stays Offline | Wrong NEWT_ID / NEWT_SECRET |
Regenerate from dashboard, redeploy Newt. |
| 502 Bad Gateway on resource | Newt OK but cannot reach target | From the Pi: curl -v http://<target-ip>:<port>. Service may bind to 127.0.0.1 only. |
Cert warning / NET::ERR_CERT_AUTHORITY_INVALID |
Let’s Encrypt failed on VPS | Cloudflare proxy is ON β switch records to grey cloud, or port 80 blocked on VPS. Check traefik log. |
| Cloudflare 525 / 526 | Cloudflare proxy ON in front of Traefik | Switch the A records to grey cloud (DNS only). |
Newt restart loop, iptables errors |
Pi running nftables backend | sudo update-alternatives --set iptables /usr/sbin/iptables-legacy, reboot. |
| Resource works locally but not externally | Wildcard DNS missing | Add * A record to VPS IP in Cloudflare, grey cloud. |
Why not Cloudflare Tunnel instead?
Cloudflare Tunnel does the same outbound-only trick and is a valid choice. Pangolin’s value over it:
- Self-hosted β no third-party dependency on the data plane; your traffic doesn’t leave your VPS.
- Built-in auth (SSO, password, PIN, OTP) at the gateway without per-app config.
- TCP/UDP forwarding (not just HTTP) since it tunnels at WireGuard level.
- Multi-site RBAC β one dashboard, multiple sites, explicit org model.
Cloudflare DNS is still useful here just for the records pointing at the VPS.
FAQ
Why not just use UPnP?
UPnP (Universal Plug and Play) automates port forwarding β an application on your LAN can ask the router to open a port on its behalf without you touching the admin panel. Convenient, but it has serious problems for self-hosting:
UPnP security risks π
- Any device or malware on the LAN can open ports without your knowledge, bypassing the firewall entirely.
- No authentication β the router trusts any local UPnP request unconditionally.
- Exposes your home WAN IP in DNS β anyone can see your home IP address.
- Breaks under CGNAT β if your ISP gives you a shared public IP (very common on mobile and some residential ISPs), UPnP cannot forward ports that don’t exist at the public IP level.
- Dynamic IP problem β if your home IP changes, DNS records go stale until you update them.
Pangolin’s outbound-tunnel approach sidesteps all of this: the router never gets touched, the home IP never appears in DNS, and CGNAT is irrelevant.
What is UPnP actually useful for?
Gaming consoles, video-conferencing apps, and peer-to-peer software that need temporary ports opened for a session β and where you accept the security trade-off. For permanent self-hosted services with a real subdomain and TLS, UPnP is the wrong tool.
Do I need to disable UPnP on my router if I use Pangolin?
Not required, but recommended. Pangolin doesn’t rely on it, and disabling UPnP removes an attack surface regardless of what tunnelling solution you use.
References
- Pangolin docs: https://docs.pangolin.net/
- Newt install: https://docs.pangolin.net/manage/sites/install-site
- Pangolin source: https://github.com/fosrl/pangolin
- Newt source: https://github.com/fosrl/newt
Comments