Tutorial

Pangolin on VPS: expose a service behind NAT without open ports

Deployment11 min read11 steps

You have a service running on a NAS, a Raspberry Pi, or a local VM, and you want it reachable from the Internet — without opening a port on your router, without a static IP, and without handing your traffic over to a third-party SaaS. Pangolin, paired with Gerbil, solves exactly this: a WireGuard relay you operate yourself on a VPS, with automatic HTTPS and a web interface to manage everything.

Contents· The problem: exposing a service behind NAT1/11
  1. 01The problem: exposing a service behind NAT
  2. 02What Pangolin brings over the alternatives
  3. 03Architecture: VPS relay, Gerbil and Newt
  4. 04Prerequisites
  5. 05Deploying Pangolin and Gerbil on the VPS
  6. 06Adding a tunnel with Newt on the origin machine
  7. 07Exposing a service: Nextcloud behind NAT example
  8. 08Security: tokens, rotation and access control
  9. 09Troubleshooting
  10. 10Pangolin vs Cloudflare Tunnel vs Tailscale/Headscale
  11. 11Going further

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

  1. 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 ufw

    Open 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 enable

    To restrict access to the admin panel (default port 3000), limit it to your IP:

    ufw allow from YOUR_IP to any port 3000
  2. Create the docker-compose.yml file

    Create a working directory and the configuration file:

    mkdir -p /opt/pangolin && cd /opt/pangolin

    Create docker-compose.yml with 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
    EOF

    Adjust the pangolin image version number to the latest release available on GitHub.

  3. 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
    EOF

    Replace yourdomain.com with your actual domain. Start the stack:

    cd /opt/pangolin && docker compose up -d
    docker compose logs -f pangolin

    Pangolin generates an admin password on first start — note it from the logs. The web interface is available at http://VPS_IP:3000.

  4. Configure the wildcard DNS record

    In your DNS zone, create a wildcard A record pointing to the VPS IP:

    *.yourdomain.com  →  A  →  VPS_IP

    On 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.

  5. Create a systemd unit for automatic restart

    Docker with restart: unless-stopped is 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

  1. Create a tunnel in the Pangolin interface

    Log into the Pangolin web interface at http://VPS_IP:3000 with 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).

  2. 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/newt

    For 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/newt
  3. Configure 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:8080

    For 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 newt
  4. Verify end-to-end connectivity

    On the VPS, verify that the WireGuard tunnel is established:

    docker exec gerbil wg show

    You should see a peer with a recent latest handshake. From any browser or command line:

    curl -I https://nextcloud.yourdomain.com

    Expected 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

  1. 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.php

    Add to the trusted_domains array:

    'trusted_domains' =>
      array (
        0 => 'localhost',
        1 => 'nextcloud.yourdomain.com',
      ),
    'overwritehost' => 'nextcloud.yourdomain.com',
    'overwriteprotocol' => 'https',
  2. 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

CriterionPangolin + GerbilCloudflare TunnelTailscale / Headscale
HostingSelf-hosted on your VPSCloudflare SaaSSaaS (Tailscale) or self-hosted (Headscale)
CostVPS cost onlyFree up to certain thresholds, then subscriptionFree (personal use), team subscription
E2E encryptionWireGuard between machine and VPS, HTTPS to visitorTLS to Cloudflare servers (decrypted in transit)WireGuard between all nodes (full mesh)
Public HTTP exposureYes, with automatic HTTPS and custom subdomainsYes, with certificates managed by CloudflareNot natively designed (requires an additional reverse proxy)
Admin complexityMedium — a Docker stack to maintain, a web interfaceLow — connector is a single binary, everything managed by CloudflareLow (Tailscale) to Medium (self-hosted Headscale)
Traffic privacyTraffic visible only on your VPSTraffic decrypted by Cloudflare on their serversEncrypted 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.

A VPS with root access and a dedicated IPv4

Pangolin and Gerbil need an open UDP port and a dedicated IPv4 to listen for incoming WireGuard connections. ServOrbit VPS plans provide both, with your choice of OS and full root access.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab