The problem: exposing a service behind NAT
Most residential and corporate connections place your machines behind a NAT: no inbound port is reachable from the outside without explicitly configuring your router. And even when that configuration is possible, it opens a direct path into your local network.
Real-world scenarios are common: a Synology NAS running Nextcloud you want to share with clients, a homelab under Proxmox hosting a dozen services, a Raspberry Pi running home automation, or a local development VM you want to test from outside. In all these cases, the usual solution — port forwarding — has two problems: it depends on the router, and it opens a direct path into your private network.
Common alternatives each have their limits: Cloudflare Tunnel passes all your traffic through Cloudflare's servers, raising privacy questions and creating a dependency on their infrastructure. Tailscale/Headscale builds a mesh network between your machines, which is excellent for point-to-point access — but is not designed to publicly expose an HTTP(S) service with a dedicated subdomain. Pangolin occupies a different space: a self-hosted WireGuard relay, with certificate and subdomain management, that you control entirely from your VPS.
What Pangolin brings over the alternatives
- Fully self-hosted — your traffic only transits through your VPS, no third party sees your application traffic.
- No inbound port required — the connection is initiated from the origin machine to the VPS (outbound), NAT is never an obstacle.
- Automatic HTTPS via Let's Encrypt — Pangolin manages certificates for each exposed subdomain, no manual intervention.
- Built-in web interface — add tunnels, manage resources and users from a web panel, no config files to edit.
- End-to-end WireGuard encryption — the tunnel between the origin machine and the VPS is encrypted at the transport layer, independently of application HTTPS.
- Multi-site — a single Pangolin instance on the VPS can relay dozens of services from different machines, under distinct subdomains.
Architecture: VPS relay, Gerbil and Newt
Pangolin relies on three components that split responsibilities:
[Origin machine] [ServOrbit VPS] [Internet]
service:8080 Pangolin (orchestration)
newt (client) ←─WireGuard UDP─→ Gerbil (WG tunnel) ←─HTTPS──→ visitor
Traefik (reverse proxy)Pangolin is the control plane: it manages tunnels, subdomains, certificates and users via a REST API and a web interface. It runs on the VPS.
Gerbil is the server-side WireGuard peer. It opens a UDP port, establishes tunnels with clients, and routes incoming traffic to exposed services. It also runs on the VPS, alongside Pangolin.
Newt is the lightweight client that runs on the origin machine (NAS, Pi, local VM). It contacts Gerbil on the WireGuard UDP port, keeps the tunnel active, and forwards received traffic to the local service on the configured port.
The concrete flow: a visitor arrives at nextcloud.yourdomain.com → Traefik (managed by Pangolin) receives the HTTPS request → forwards it via Gerbil through the active WireGuard tunnel → Newt receives it and relays it to localhost:8080 on the origin machine. The service responds via the same path in reverse.
Prerequisites
On the VPS side:
- VPS with root access and a dedicated IPv4 — the {{vps.start.name}} plan is enough to get started (1 vCPU, 1 GB RAM for light loads, 2 GB recommended for multiple active tunnels).
- Ubuntu 22.04 or 24.04 (Debian 12 also works).
- A free UDP port for WireGuard (default 51820, configurable).
- A domain name with DNS management access — you will need to create a wildcard A record *.yourdomain.com pointing to the VPS IP.
- Docker and Docker Compose installed (apt install docker.io docker-compose-plugin).
On the origin machine side:
- A Linux machine (Raspberry Pi, NAS running DSM 7+, Proxmox VM, physical server) with outbound UDP access to the WireGuard port on the VPS — most ISPs allow this.
- A service listening on a local port (Nextcloud, Gitea, Home Assistant, etc.).
Current version: Pangolin 1.23.0 (released September 16, 2026).
Deploying Pangolin and Gerbil on the VPS
Prepare the VPS and open the WireGuard port
Connect as root to the VPS and install Docker if not already present:
apt update && apt install -y docker.io docker-compose-plugin ufwOpen the UDP port for WireGuard (default 51820) and ensure HTTP/HTTPS ports are accessible:
ufw allow 80/tcp ufw allow 443/tcp ufw allow 51820/udp ufw enableTo restrict access to the admin panel (default port 3000), limit it to your IP:
ufw allow from YOUR_IP to any port 3000Create the docker-compose.yml file
Create a working directory and the configuration file:
mkdir -p /opt/pangolin && cd /opt/pangolinCreate
docker-compose.ymlwith the following content:cat > docker-compose.yml <<'EOF' services: pangolin: image: fosrl/pangolin:1.23.0 container_name: pangolin restart: unless-stopped volumes: - ./config:/app/config - ./data:/app/data ports: - "3000:3000" networks: - pangolin_net gerbil: image: fosrl/gerbil:latest container_name: gerbil restart: unless-stopped cap_add: - NET_ADMIN volumes: - ./data:/var/lib/gerbil ports: - "51820:51820/udp" networks: - pangolin_net depends_on: - pangolin networks: pangolin_net: driver: bridge EOFAdjust the
pangolinimage version number to the latest release available on GitHub.Configure Pangolin and start the stack
Create the configuration directory and a minimal
config.yml:mkdir -p /opt/pangolin/config cat > /opt/pangolin/config/config.yml <<'EOF' app: base_domain: yourdomain.com admin_email: [email protected] port: 3000 wireguard: port: 51820 subnet: 10.0.0.0/24 acme: enabled: true staging: false EOFReplace
yourdomain.comwith your actual domain. Start the stack:cd /opt/pangolin && docker compose up -d docker compose logs -f pangolinPangolin generates an admin password on first start — note it from the logs. The web interface is available at
http://VPS_IP:3000.Configure the wildcard DNS record
In your DNS zone, create a wildcard A record pointing to the VPS IP:
*.yourdomain.com → A → VPS_IPOn Cloudflare, create this record with the proxy disabled (DNS-only, grey cloud) so that Let's Encrypt certificates can be issued correctly via the HTTP-01 challenge used by Pangolin.
Create a systemd unit for automatic restart
Docker with
restart: unless-stoppedis generally sufficient, but if you prefer a dedicated systemd unit:cat > /etc/systemd/system/pangolin.service <<'EOF' [Unit] Description=Pangolin reverse tunnel stack After=docker.service Requires=docker.service [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/opt/pangolin ExecStart=/usr/bin/docker compose up -d ExecStop=/usr/bin/docker compose down [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable pangolin
Adding a tunnel with Newt on the origin machine
Create a tunnel in the Pangolin interface
Log into the Pangolin web interface at
http://VPS_IP:3000with your admin credentials. Go to Sites → Add site, give it a name (e.g.homelab) and note the generated site token. Then under Resources → Add resource, choose the site, enter the desired subdomain (nextcloud.yourdomain.com) and the local port of the origin machine (e.g.8080).Install Newt on the origin machine
On the machine hosting your service, download the Newt binary for your architecture:
# Linux x86_64 curl -Lo /usr/local/bin/newt \ https://github.com/fosrl/newt/releases/latest/download/newt-linux-amd64 chmod +x /usr/local/bin/newtFor a Raspberry Pi (ARM64):
curl -Lo /usr/local/bin/newt \ https://github.com/fosrl/newt/releases/latest/download/newt-linux-arm64 chmod +x /usr/local/bin/newtConfigure and start Newt
Launch Newt with the site token retrieved in the previous step:
newt \ --server https://yourdomain.com:3000 \ --token YOUR_SITE_TOKEN \ --target localhost:8080For automatic startup, create a systemd unit:
cat > /etc/systemd/system/newt.service <<'EOF' [Unit] Description=Newt WireGuard tunnel client After=network.target [Service] ExecStart=/usr/local/bin/newt \ --server https://yourdomain.com:3000 \ --token YOUR_SITE_TOKEN \ --target localhost:8080 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now newtVerify end-to-end connectivity
On the VPS, verify that the WireGuard tunnel is established:
docker exec gerbil wg showYou should see a peer with a recent
latest handshake. From any browser or command line:curl -I https://nextcloud.yourdomain.comExpected response:
HTTP/2 200(or your service's login page). The Let's Encrypt certificate is issued automatically on first access.
Exposing a service: Nextcloud behind NAT example
Configure Nextcloud to accept the proxy domain
Nextcloud blocks requests from undeclared domains. Add the public domain to
config.php:# From the origin machine, in the Nextcloud directory nano config/config.phpAdd to the
trusted_domainsarray:'trusted_domains' => array ( 0 => 'localhost', 1 => 'nextcloud.yourdomain.com', ), 'overwritehost' => 'nextcloud.yourdomain.com', 'overwriteprotocol' => 'https',Verify public access and certificate
Wait 30 to 60 seconds after first access for Let's Encrypt to issue the certificate, then:
curl -v https://nextcloud.yourdomain.com 2>&1 | grep -E 'subject|issuer|HTTP'The certificate is signed by Let's Encrypt and the public URL is now accessible from any network, with no open ports on the local network side.
Security: tokens, rotation and access control
Site token rotation — generate a new token from the Pangolin interface and update the Newt systemd unit on the origin machine. The old token is immediately invalidated. Schedule this rotation every 90 days or on any personnel change with access to the origin machine.
IP allowlisting in Pangolin — for each exposed resource, you can define an IP allowlist under Resources → Access Policy. Useful for restricting access to your backoffice or internal tools to your office IP range.
Authenticated access — Pangolin supports OIDC providers (Authentik, Keycloak, Zitadel) to add an authentication layer in front of any exposed resource, without modifying the application behind the tunnel.
Restrict the admin port — Pangolin's port 3000 must not be publicly accessible. Restrict it to your IP with ufw allow from YOUR_IP to any port 3000 && ufw deny 3000.
Troubleshooting
Tunnel won't establish — UDP blocked by ISP. Some ISPs filter outbound UDP on non-standard ports. Test from the origin machine: nc -u -v VPS_IP 51820. If the connection is refused, change the WireGuard port in config.yml to 443 (UDP) or 53 (UDP) — these ports pass almost universally. Update ufw on the VPS accordingly.
MTU mismatch — latency or random disconnects. WireGuard adds encapsulation overhead (roughly 60 bytes). If your service transfers large files and you observe disconnects, reduce the WireGuard interface MTU in Gerbil's configuration: mtu = 1380 is a safe value on most links.
Service unreachable despite active tunnel. First verify that the local service listens on 0.0.0.0 and not only on 127.0.0.1: ss -tlnp | grep 8080. Then confirm that the port configured in Pangolin matches the actual service port. Finally, check Newt's logs: journalctl -u newt -f.
Let's Encrypt certificate not issued. Pangolin uses the HTTP-01 challenge, which requires port 80 on the VPS to be reachable from the Internet. Check that ufw allow 80/tcp is active and no other service holds port 80 (ss -tlnp | grep :80). On Cloudflare, make sure the wildcard record is DNS-only (grey cloud).
Pangolin interface unreachable after reboot. If Docker restarts before the network is available, Pangolin may start without a network interface. Add network-online.target to the systemd dependency, or simply rerun docker compose up -d from /opt/pangolin.
Pangolin vs Cloudflare Tunnel vs Tailscale/Headscale
Scroll the table
| Criterion | Pangolin + Gerbil | Cloudflare Tunnel | Tailscale / Headscale |
|---|---|---|---|
| Hosting | Self-hosted on your VPS | Cloudflare SaaS | SaaS (Tailscale) or self-hosted (Headscale) |
| Cost | VPS cost only | Free up to certain thresholds, then subscription | Free (personal use), team subscription |
| E2E encryption | WireGuard between machine and VPS, HTTPS to visitor | TLS to Cloudflare servers (decrypted in transit) | WireGuard between all nodes (full mesh) |
| Public HTTP exposure | Yes, with automatic HTTPS and custom subdomains | Yes, with certificates managed by Cloudflare | Not natively designed (requires an additional reverse proxy) |
| Admin complexity | Medium — a Docker stack to maintain, a web interface | Low — connector is a single binary, everything managed by Cloudflare | Low (Tailscale) to Medium (self-hosted Headscale) |
| Traffic privacy | Traffic visible only on your VPS | Traffic decrypted by Cloudflare on their servers | Encrypted mesh traffic, never centralized |
Going further
Pangolin 1.23.0 introduces support for high availability in the Enterprise edition and multi-admin server management — features useful once your infrastructure grows beyond personal use.
To expose non-HTTP services (private SSH, databases, application UDP protocols), Pangolin now supports private TCP/UDP resources accessible via the Newt client — without exposing them publicly on a URL, but making them reachable within your WireGuard network.
If your need is different — coordinating access between multiple machines without public exposure — Headscale on VPS is the right tool. For exposure without an intermediate VPS using Cloudflare's infrastructure, Cloudflare Tunnel remains the simplest option. And to lay the groundwork for network encryption on a VPS, the WireGuard on VPS guide covers native WireGuard interface installation and configuration.