How To Set Up Pangolin as a Self-Hosted VPN / Reverse Proxy
Tricknowtech Team 15 min read
Goal
In this tutorial, you will deploy Pangolin on a VPS with Docker Compose, connect an internal machine to it using the Newt tunnel client, and securely expose an internal service on a public subdomain with automatic TLS — without opening any inbound ports on the internal network.
Prerequisites
A VPS with a public IPv4 address and root or sudo access
Docker and Docker Compose installed on the VPS
A registered domain name with DNS you control (this tutorial uses example.com as a placeholder)
A second machine — home server, NAS, or lab box — behind NAT or without a public IP, where the services you want to expose are running, with root/sudo access
Basic familiarity with the command line, DNS records, and Docker
Let an AI agent do this for you
Copy a ready-made prompt for an AI coding assistant with terminal access to your server (Claude Code, Cursor, or similar) — it can carry out the steps below for you. Review what it plans to run before it executes anything.
Pangolin (github.com/fosrl/pangolin) is an open source, self-hosted tunneled reverse proxy — an alternative to Cloudflare Tunnel or Tailscale Funnel that you run entirely on infrastructure you control. A central Pangolin server, deployed with Docker Compose on a VPS with a public IP, runs a management dashboard and uses Traefik under the hood as the actual reverse proxy. A lightweight client called Newt runs on whichever internal machine has the services you want to expose — behind home NAT, in a lab, anywhere without a public IP — and opens an outbound WireGuard tunnel back to the server. No inbound ports need to be opened on the internal network; the tunnel is always initiated from the inside out.
Step 1 — Point DNS at Your VPS
Before starting anything, create an A record for the domain you'll use for the Pangolin dashboard, pointing at your VPS's public IP. This has to happen first because Pangolin needs to complete a Let's Encrypt HTTP-01 or DNS-01 challenge to issue a TLS certificate for that hostname, and that challenge will fail if the domain doesn't resolve to your server yet.
text
Type: A
Name: pangolin.example.com
Value: 203.0.113.30
TTL: 300 (or your provider's default)
Confirm it has propagated before moving on:
bash
dig +short pangolin.example.com
“Don't start the Pangolin stack until this command returns your VPS's IP. Starting it too early can cause the initial certificate request to fail, and you'll have to trigger it again later.”
Step 2 — Confirm Docker and Docker Compose Are Installed
Pangolin ships as a Docker Compose stack, so Docker Engine and the Compose plugin need to already be present on the VPS. If they aren't installed yet, follow Docker's own official installation instructions for your distribution at docs.docker.com — the exact install command varies by OS and release and changes over time, so use the official docs rather than a script reproduced elsewhere.
Once installed, confirm both pieces are present:
bash
docker --version
docker compose version
Step 3 — Get the Pangolin Quick-Start Files
Clone the Pangolin repository and follow the Docker Compose quick-start in its README. The exact commands the README walks you through can change as the project evolves, so treat the repo's own instructions as authoritative rather than anything reproduced elsewhere — what it produces, at the end, is a docker-compose.yml and a config.yml scoped to your domain.
Tricknowtech VPS Hosting
Dedicated KVM resources and full root access — deployed in under 60 seconds, no ticket required.
git clone https://github.com/fosrl/pangolin.git
cd pangolin
# Follow the "Docker Compose" quick-start section of README.md from here —
# it will generate docker-compose.yml and config.yml for you.
Conceptually, config.yml needs to know three things: the base domain Pangolin will issue subdomains under (example.com), the specific hostname the dashboard itself will be served on (pangolin.example.com), and an admin email address used for Let's Encrypt notices. The shape looks roughly like this — confirm the exact key names against the current README rather than copying this verbatim, since they can shift between releases:
yaml
# Illustrative shape only — verify exact keys in the project's README
app:
base_domain: "example.com"
dashboard:
domain: "pangolin.example.com"
users:
server_admin:
email: "[email protected]"
Step 4 — Configure the Firewall
Open only what Pangolin actually needs: SSH (so you don't lock yourself out), HTTP/HTTPS for the dashboard and proxied traffic, and the WireGuard UDP port that Newt clients connect to. Check the ports: section of the docker-compose.yml you just generated to see which UDP port is published for tunnel connections, and allow that specific port rather than opening a wide range or guessing at a default.
bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Replace <WG_PORT> with the exact UDP port your docker-compose.yml
# publishes for Newt/WireGuard connections — do not assume a default.
sudo ufw allow <WG_PORT>/udp
sudo ufw enable
sudo ufw status
“Never disable ufw entirely to "make it work." If a connection isn't getting through, open the specific port it needs and leave everything else closed.”
Give it a minute on first boot — it needs to request and receive the initial TLS certificate for the dashboard domain before HTTPS will respond.
Step 6 — Complete Initial Dashboard Setup
Visit https://pangolin.example.com in a browser. You should land on a first-run setup flow:
Create the initial admin account — use a strong, unique password; Pangolin's docs describe additional login hardening options such as 2FA and SSO if you want to layer those on afterward
Create an Organization, which is the top-level container your Sites and Resources will live under
Step 7 — Create a Site (the Newt Tunnel)
Inside your Organization, create a new Site and choose the Newt type — this is the WireGuard-based tunnel connection to whatever internal machine will run the client. Creating it generates a set of connection credentials: an endpoint, a Site ID, and a secret. These are what the Newt client uses to authenticate and establish the tunnel.
“Treat the Site's endpoint, ID, and secret as sensitive as a private key. Anyone who has them can connect to your network as that Site. Don't paste them into chat, tickets, or a public repo, and don't commit them to version control.”
Step 8 — Install and Run Newt on the Internal Machine
On the machine that has the services you want to expose — a home server, a NAS, a box in a lab, anything without a public IP — install Newt from the fosrl/newt GitHub repository. It's distributed as both a small standalone binary and a Docker image; follow that repo's README for the exact current install command for your OS and architecture rather than assuming one, since it can differ by release.
Once installed, Newt needs the endpoint, ID, and secret from Step 7 to authenticate and open the tunnel — the fosrl/newt README documents the exact current way to supply them, as either command-line arguments or environment variables. The specific flag or variable names have changed between releases, so treat the README as authoritative rather than any specific syntax you might see reproduced elsewhere:
text
# Illustrative only — not literal syntax to copy-paste.
# Newt needs three pieces of connection info from Step 7:
# 1. the Pangolin server endpoint (e.g. https://pangolin.example.com)
# 2. the Site ID
# 3. the Site secret
# Check https://github.com/fosrl/newt for the current flag names or
# environment variable names it expects, then run it with those values.
# Never commit these values to a repo.
If you store these values in an environment file instead of passing them as flags, lock the file down so only the owner can read it:
bash
chmod 600 newt.env
Back in the dashboard, the Site should flip to a connected state within a few seconds. If it doesn't, see the troubleshooting note at the end.
Step 9 — Point DNS at Each Resource Subdomain
Public requests for an exposed service still arrive over the internet at your VPS — Pangolin then routes them through the tunnel to the internal machine. That means every subdomain you plan to expose needs its own DNS record pointing at the VPS, same as the dashboard domain did in Step 1. You can add these one at a time as you create Resources, or set up a wildcard record up front to cover all of them:
text
Type: A
Name: app.example.com
Value: 203.0.113.30
TTL: 300
Step 10 — Create a Resource
In the dashboard, create a Resource: pick the Site (your connected Newt client), give it a public subdomain — app.example.com — and point it at the internal host and port the service actually listens on, from the Newt machine's point of view. That's usually 127.0.0.1:8080 if the service runs on the same box as Newt, or a LAN IP:port if it's elsewhere on that internal network but still reachable from the Newt host.
Save it. Pangolin's Traefik layer requests a Let's Encrypt certificate for the new subdomain automatically and starts routing public HTTPS traffic on it through the tunnel to that internal address.
Step 11 — Verify
Once DNS has propagated and the Resource is active, requests to the subdomain should reach the internal service with a valid certificate, and you haven't touched a single inbound port on the internal network to make that happen.
bash
curl -I https://app.example.com
A 200 (or whatever status your internal app normally returns) with valid TLS headers confirms the whole path is working: DNS to VPS, VPS to Traefik, Traefik through the Newt tunnel, tunnel to the internal service, and back.
Beyond HTTP: Using Pangolin as a Lightweight VPN
Everything above proxies HTTP(S) traffic on a subdomain, which covers most web apps and dashboards. But the tunnel Newt establishes is a general-purpose WireGuard connection between the VPS and the internal network, not an HTTP-only pipe — Pangolin also supports defining Resources for raw TCP and UDP services, not just HTTP. That's the sense in which it functions as a site-to-site VPN rather than just a reverse proxy: a non-HTTP service on the internal network (a database, a game server, an SSH endpoint) can be reached through the same tunnel without being exposed directly to the internet. The exact configuration for TCP/UDP Resources is worth checking directly in Pangolin's docs, since the options there are more involved than the subdomain-and-port flow used for HTTP Resources above.
Security Notes
Use a strong, unique password for the dashboard admin account created in Step 6, and look at Pangolin's docs for enabling 2FA or SSO if you want additional protection on top of it
Guard each Site's endpoint/ID/secret the same way you'd guard a private key — anyone holding them can connect to your network as that Site
If you save those credentials to a file on the Newt host, chmod 600 it and keep it out of version control entirely
Keep the VPS firewall limited to SSH, 80/tcp, 443/tcp, and the specific WireGuard UDP port Pangolin publishes — resist the temptation to open ranges "just in case"
Troubleshooting: Site Shows Disconnected or a Resource Returns a 502
The most common failure is the Site never showing as connected in the dashboard, or a Resource resolving but returning a 502/504. Work through these in order:
Check Newt's own output on the internal machine for connection errors — an auth failure there usually means a typo in the endpoint, ID, or secret
Confirm the VPS firewall is actually allowing the WireGuard UDP port Newt connects to: sudo ufw status verbose
From the Pangolin server, check docker compose logs for the Traefik/proxy container for TLS or routing errors tied to the Resource's subdomain
If the Site connects fine but a specific Resource 502s, the problem is almost always that the internal host:port you pointed it at isn't actually reachable from the Newt machine — confirm with curl or nc from that machine directly against the target
If a certificate never issues for a subdomain, double-check that its DNS record (Step 9) was actually pointing at the VPS before the Resource was created — Let's Encrypt validation happens at creation time, not continuously