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 (€4/mo), Netcup VPS 200 G11 (€3/mo), OVH VPS Starter (~€3.5/mo), or Oracle Cloud Free (4 ARM cores / 24 GB, $0).
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:

  1. Create the admin user.
  2. Create an Organisation.
  3. Create a Site (e.g. home-pi).
  4. The dashboard now shows:
    • PANGOLIN_ENDPOINT (= https://pangolin.example.com)
    • NEWT_ID
    • NEWT_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:

  1. Open the Site β†’ Resources β†’ Add Resource.
  2. Subdomain: grafana β†’ public URL becomes https://grafana.example.com.
  3. Target: http://192.168.1.50:3000. Must be reachable from the Pi. If the service runs on the Pi itself, it must bind to 0.0.0.0, not 127.0.0.1.
  4. Auth: pick one (SSO, password, PIN, OTP, or public).
  5. 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:

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