Skip to content

How to Deploy Pangolin on a VPS

Updated Sep 2026

verified on Ubuntu 26.04 · Jul 2026
We earn commissions when you shop through the links below. Full disclosure →

Self-host Pangolin on your own VPS — a tunneled reverse proxy with built-in identity and automatic SSL that publishes private services to the internet over WireGuard, no Cloudflare account required.

Before you start
  • A VPS with 1 vCPU / 1–2 GB RAM and a public IPv4 address
  • A fresh Ubuntu 26.04 server with root/sudo SSH access, Docker + Compose installed
  • A domain with a wildcard DNS record (a single `*.example.com` A record) pointed at the server — every resource you publish gets its own subdomain
Need a box for this guide? Kamatera's free tier lets you spin one up now.Start free on Kamatera → (opens in new tab)

What Pangolin is

Pangolin is a self-hosted, tunneled reverse proxy — think of it as a self-hosted Cloudflare Tunnel. You run Pangolin on a VPS with a public IP, and it publishes services that don't have a public IP of their own (a home server, a lab box behind NAT, a container on an internal network) to the internet over an encrypted WireGuard tunnel. Nothing has to open a port on the private side; the private machine dials out to Pangolin, and Pangolin exposes it as a proper HTTPS subdomain.

That combination — reverse proxy plus tunnel — is bundled with two things most self-hosted proxies leave to you: identity and SSL are built in. Every published "Resource" can sit behind Pangolin's own auth (password, OAuth, or just an invite-only login) before traffic ever reaches the app, and TLS certificates are issued and renewed automatically. The stack is three containers working together: a TypeScript dashboard/API (pangolin) that owns configuration and auth, a small Go data-plane binary called Gerbil that terminates the WireGuard tunnels and the public ports, and Traefik as the actual reverse proxy, driven live by Pangolin's API via a badger auth-forwarding plugin. It's AGPL-3.0 (Community Edition — there's a paid Enterprise tier for larger orgs, not needed here).

This guide deploys the Pangolin server itself on your VPS — the piece that sits on the public internet. Once it's up, private machines connect to it with a lightweight client (Newt) to register resources; that side is out of scope here, but everything below gets the server itself running and reachable.

Server sizing — why 1–2 GB is enough

Pangolin's job is proxying and tunnel termination, not compute — the three containers (a Node dashboard, a small Go binary, and Traefik) are all lightweight, and none of them does anything CPU-heavy under normal use. 1 vCPU and 1–2 GB RAM is comfortable for a personal or small-team instance with a handful of published resources. The default datastore is SQLite, stored on a small volume, so disk needs are modest too — 10–20 GB leaves plenty of room.

Where it gets heavier is scale: a lot of concurrent tunnel clients or high proxied traffic volume benefits from more RAM and a dedicated Postgres datastore instead of SQLite, but that's a later problem. A 2 vCPU / 4 GB VPS has generous headroom for a first deploy.

Prepare the server

This guide assumes you've already worked through Docker & Compose on Ubuntu — a fresh Ubuntu 26.04 box with Docker Engine, the Compose plugin, and ufw allowing SSH plus 80 and 443. Pangolin needs two more ports open: 51820/udp (the main WireGuard tunnel) and 21820/udp (a secondary port Gerbil uses for client hole-punching). Unlike the reverse-proxy guides where everything binds to localhost behind a proxy, Pangolin's Traefik container shares Gerbil's network namespace and binds 80/443 directly — so the base guide's existing 80/443 rules already cover the web ports, and you only need to add the two UDP ports.

ufw allow 51820/udp
ufw allow 21820/udp
mkdir -p ~/pangolin/config/db ~/pangolin/config/letsencrypt ~/pangolin/config/traefik/logs
cd ~/pangolin

If your VPS provider has its own cloud firewall panel in addition to ufw (Hetzner Cloud Firewall, DigitalOcean Cloud Firewalls, etc.), open the same four ports there too — ufw alone won't help if the provider's firewall drops the traffic first.

Where to host itaffiliate disclosure
Hetzner Cloudrun it on
2 vCPU · 4 GB RAM · 80 GB SSD · $23.59/mo
Get Hetzner Cloud (opens in new tab)
Kamaterafree trial
1 vCPU · 1 GB RAM · 20 GB SSD · $4.00/mo
Start free on Kamatera → (opens in new tab)
DigitalOceanalso works on
1 vCPU · 1 GB RAM · 25 GB SSD · $6.00/mo
Deploy on DigitalOcean → (opens in new tab)

Paid link — we earn a commission if you shop through it.

Install Pangolin

Pangolin ships an interactive one-command installer (curl -fsSL https://static.pangolin.net/get-installer.sh | bash then sudo ./installer) that walks you through the setup with prompts. This guide uses Pangolin's other officially documented install path instead — manual Docker Compose — because every value is supplied up front in config files, which makes the whole install scriptable and reproducible rather than answered by hand.

First generate a random secret Pangolin uses to sign sessions — treat it like a password:

export PANGOLIN_SECRET=$(openssl rand -hex 32)
echo "$PANGOLIN_SECRET" > ~/pangolin/.pangolin-secret
chmod 600 ~/pangolin/.pangolin-secret

Create the Compose file:

cat > docker-compose.yml <<'EOF'
name: pangolin
services:
  pangolin:
    image: docker.io/fosrl/pangolin:latest
    container_name: pangolin
    restart: unless-stopped
    volumes:
      - ./config:/app/config
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
      interval: "10s"
      timeout: "10s"
      retries: 15

  gerbil:
    image: docker.io/fosrl/gerbil:latest
    container_name: gerbil
    restart: unless-stopped
    depends_on:
      pangolin:
        condition: service_healthy
    command:
      - --reachableAt=http://gerbil:3004
      - --generateAndSaveKeyTo=/var/config/key
      - --remoteConfig=http://pangolin:3001/api/v1/
    volumes:
      - ./config/:/var/config
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    ports:
      - 51820:51820/udp
      - 21820:21820/udp
      - 443:443
      - 80:80

  traefik:
    image: docker.io/traefik:v3.6
    container_name: traefik
    restart: unless-stopped
    network_mode: service:gerbil
    depends_on:
      pangolin:
        condition: service_healthy
    command:
      - --configFile=/etc/traefik/traefik_config.yml
    volumes:
      - ./config/traefik:/etc/traefik:ro
      - ./config/letsencrypt:/letsencrypt
      - ./config/traefik/logs:/var/log/traefik

networks:
  default:
    driver: bridge
    name: pangolin
EOF

Traefik's static config — this is where the Let's Encrypt email lives:

cat > config/traefik/traefik_config.yml <<'EOF'
api:
  insecure: true
  dashboard: true

providers:
  http:
    endpoint: "http://pangolin:3001/api/v1/traefik-config"
    pollInterval: "5s"
  file:
    filename: "/etc/traefik/dynamic_config.yml"

experimental:
  plugins:
    badger:
      moduleName: "github.com/fosrl/badger"
      version: "v1.4.0"

log:
  level: "INFO"
  format: "common"
  maxSize: 100
  maxBackups: 3
  maxAge: 3
  compress: true

certificatesResolvers:
  letsencrypt:
    acme:
      httpChallenge:
        entryPoint: web
      email: "admin@example.com"
      storage: "/letsencrypt/acme.json"
      caServer: "https://acme-v02.api.letsencrypt.org/directory"

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: "30m"
    http:
      tls:
        certResolver: "letsencrypt"
      encodedCharacters:
        allowEncodedSlash: true
        allowEncodedQuestionMark: true

serversTransport:
  insecureSkipVerify: true

ping:
  entryPoint: "web"
EOF

Traefik's dynamic config — the routers that get the dashboard itself online (every other route, for resources you publish later, Pangolin pushes to Traefik live via the http provider above):

cat > config/traefik/dynamic_config.yml <<'EOF'
http:
  middlewares:
    badger:
      plugin:
        badger:
          disableForwardAuth: true
    redirect-to-https:
      redirectScheme:
        scheme: https

  routers:
    main-app-router-redirect:
      rule: "Host(`pangolin.example.com`)"
      service: next-service
      entryPoints:
        - web
      middlewares:
        - redirect-to-https
        - badger

    next-router:
      rule: "Host(`pangolin.example.com`) && !PathPrefix(`/api/v1`)"
      service: next-service
      entryPoints:
        - websecure
      middlewares:
        - badger
      tls:
        certResolver: letsencrypt

    api-router:
      rule: "Host(`pangolin.example.com`) && PathPrefix(`/api/v1`)"
      service: api-service
      entryPoints:
        - websecure
      middlewares:
        - badger
      tls:
        certResolver: letsencrypt

    ws-router:
      rule: "Host(`pangolin.example.com`)"
      service: api-service
      entryPoints:
        - websecure
      middlewares:
        - badger
      tls:
        certResolver: letsencrypt

  services:
    next-service:
      loadBalancer:
        servers:
          - url: "http://pangolin:3002"

    api-service:
      loadBalancer:
        servers:
          - url: "http://pangolin:3000"

tcp:
  serversTransports:
    pp-transport-v1:
      proxyProtocol:
        version: 1
    pp-transport-v2:
      proxyProtocol:
        version: 2
EOF

And Pangolin's own config — the base domain, dashboard hostname, and the secret you generated:

cat > config/config.yml <<EOF
gerbil:
    start_port: 51820
    base_endpoint: "pangolin.example.com"

app:
    dashboard_url: "https://pangolin.example.com"
    log_level: "info"
    telemetry:
        anonymous_usage: true

domains:
    domain1:
        base_domain: "example.com"

server:
    secret: "${PANGOLIN_SECRET}"
    cors:
        origins: ["https://pangolin.example.com"]
        methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
        allowed_headers: ["X-CSRF-Token", "Content-Type"]
        credentials: false

flags:
    require_email_verification: false
    disable_signup_without_invite: true
    disable_user_create_org: false
    allow_raw_resources: true
EOF

Before starting, replace every example.com / pangolin.example.com / admin@example.com placeholder across the three config files with your real base domain, dashboard hostname, and email (see the wildcard DNS section below for what to use if you don't have a domain yet). Then bring the stack up:

docker compose up -d
docker compose logs --tail 60 pangolin traefik gerbil

Pangolin starts first, Gerbil waits for it to report healthy, and Traefik waits on Pangolin too since it needs the dashboard's live routing config. First boot takes a little longer while images are pulled and Gerbil generates its WireGuard keypair.

Domain + wildcard DNS

Pangolin gives every resource you publish its own subdomain under your base domain — app1.example.com, app2.example.com, and so on — generated on demand as you create them in the dashboard. Rather than adding a DNS record per resource, point a single wildcard A record at your server once: Type A, Name *, Value <your-server-ip>. From then on, every new subdomain resolves automatically and Traefik's HTTP-01 challenge (configured above) issues it a certificate the first time it's requested — no DNS-provider API keys or DNS-01 setup needed.

No domain yet? Use <your-server-ip>.sslip.io as a throwaway wildcard. sslip.io resolves any subdomain of <ip>.sslip.io to that IP with zero setup — pangolin.203.0.113.10.sslip.io, app1.203.0.113.10.sslip.io, and anything else you make up all resolve correctly, which is exactly the wildcard behavior Pangolin needs. Set your base domain to <your-server-ip>.sslip.io and your dashboard hostname to pangolin.<your-server-ip>.sslip.io in the config files above, and everything else works unchanged. It's fine for testing and evaluation; for anything real, register an actual domain and add the wildcard A record — a sslip.io address is public and guessable, which isn't what you want for production auth-gated resources.

First-run setup

On first boot Pangolin has no admin account yet. It prints a one-time setup token to its logs:

docker compose logs pangolin 2>&1 | grep -i "setup token" || true

Visit https://<your-dashboard-domain>/auth/initial-setup in a browser, paste the token, and set a strong password — this becomes the owner account with full access to every organization and resource. Like most self-hosted admin panels, that setup page is open to whoever gets there first, so do this immediately after the stack comes up.

Once you're in, create your first Site (a tunnel endpoint) and a test Resource pointing at a simple internal service to confirm routing works end to end — the dashboard walks you through installing the Newt client on the machine you're publishing from.

Backups

Everything that matters lives under ~/pangolin/config: the SQLite database (config/db/db.sqlite), the Let's Encrypt certificates (config/letsencrypt/acme.json), Gerbil's WireGuard key (config/key), and the config files themselves — including the secret in config/config.yml, which signs every session token. Back up the whole directory:

tar -czf ~/pangolin-backup-$(date +%F).tar.gz -C ~/pangolin config

Copy that archive off the box on a schedule — S3-compatible storage or another machine — and periodically restore it into a throwaway stack to confirm it actually works. A backup you haven't restored is a guess. If you're running more than a handful of resources, consider migrating the datastore to Postgres for better durability under load; that's a config change documented in Pangolin's docs, not something this guide's SQLite default handles for you.

Upgrades

docker compose pull
docker compose up -d

Read the release notes before upgrading, more than with most self-hosted tools — Pangolin's config schema has changed across versions in ways that can break an unattended pull && up -d if you skip straight over a breaking release. Take the backup above first, then upgrade one version at a time rather than jumping several releases, and pin specific image tags (fosrl/pangolin:1.x.x instead of :latest) once you're past initial setup so an upgrade is always a deliberate, single-version step.

Troubleshooting

Certificate never issues / dashboard shows a self-signed or invalid cert. Almost always DNS. Confirm your wildcard A record (or sslip.io hostname) actually resolves to the server's IP from outside (dig pangolin.example.com from your laptop), and that ports 80 and 443 are open both in ufw and any provider-level cloud firewall — the HTTP-01 challenge needs port 80 reachable from the internet at the exact moment the cert is requested.

502 / Bad Gateway from Traefik. Check docker compose ps — Traefik depends on Pangolin being healthy, and if Pangolin is still starting or crash-looping, Traefik has nowhere to send the request. Check docker compose logs pangolin for the actual error.

Tunnel clients (Newt) can't connect. WireGuard traffic on 51820/udp or 21820/udp is being dropped somewhere between the client and the server — check the provider's cloud firewall in addition to ufw, since many VPS panels ship with UDP blocked by default even when ufw allows it.

Setup page 404s or the dashboard is unreachable at your domain. The Host() rules in dynamic_config.yml and dashboard_url in config.yml must exactly match the hostname you're actually visiting — if you edited one placeholder and missed another, Traefik has no route for the request.

Verification + next steps

You're done when all three containers show healthy in docker compose ps, the dashboard loads over HTTPS at your real domain with a valid certificate, you've created the owner account, and a test Resource routes traffic from a connected Newt client through to an internal service. From here, invite teammates as users on specific organizations, and layer in an identity provider if you want SSO rather than Pangolin-native logins. A 2 vCPU / 4 GB box comfortably runs Pangolin alongside whatever it's tunneling to; see Best VPS for self-hosting for the ranked picks.

Next steps

How to self-host Pangolin →More self-hosted vpn & mesh networking tools →Best VPS for a VPN (WireGuard) →Automatic HTTPS with Caddy →Run Claude Code with Ollama on Your Own VPS →Deploy Coolify on a VPS →How to Deploy Actual Budget on a VPS →How to Deploy AnythingLLM on a VPS →How to Deploy Appwrite on a VPS →How to Deploy Audiobookshelf on a VPS →How to Deploy Authelia on a VPS →How to Deploy authentik on a VPS →How to Deploy Baserow on a VPS →How to Deploy Beszel on a VPS →How to Deploy Bitwarden on a VPS →How to Deploy BookStack on a VPS →How to Deploy CapRover on a VPS →How to Deploy Checkmate on a VPS →How to Deploy Directus on a VPS →How to Deploy docker-mailserver on a VPS →How to Deploy Docmost on a VPS →How to Deploy Dokku on a VPS →How to Deploy Dokploy on a VPS →How to Deploy Firefly III on a VPS →How to Deploy Forgejo on a VPS →How to Deploy Gatus on a VPS →How to Deploy Ghostfolio on a VPS →How to Deploy Gitea on a VPS →How to Deploy GitLab on a VPS →How to Deploy GlitchTip on a VPS →How to Deploy Grafana on a VPS →How to Deploy Graylog on a VPS →How to Deploy Headscale on a VPS →How to Deploy Healthchecks on a VPS →How to Deploy Home Assistant on a VPS →How to Deploy Immich on a VPS →How to Deploy Jan on a VPS →How to Deploy Jellyfin on a VPS →How to Deploy Karakeep on a VPS →How to Deploy Keycloak on a VPS →How to Deploy Leantime on a VPS →How to Deploy LibreChat on a VPS →How to Deploy Linkwarden on a VPS →How to Deploy LocalAI on a VPS →How to Deploy Mailcow on a VPS →How to Deploy Mailu on a VPS →How to Deploy Matomo on a VPS →How to Deploy Mattermost on a VPS →How to Deploy Meilisearch on a VPS →How to Deploy Memos on a VPS →How to Deploy n8n on a VPS →How to Deploy Navidrome on a VPS →How to Deploy NetBird on a VPS →How to Deploy Netdata on a VPS →How to Deploy Nextcloud on a VPS →How to Deploy Next.js to a VPS →How to Deploy Nginx Proxy Manager on a VPS →How to Deploy NocoDB on a VPS →How to Deploy ntfy on a VPS →How to Deploy Ollama on a VPS →How to Deploy Open WebUI on a VPS →How to Deploy OpenHands on a VPS →How to Deploy OpenObserve on a VPS →How to Deploy OpenProject on a VPS →How to Deploy Outline on a VPS →How to Deploy Paperless-ngx on a VPS →How to Deploy Passbolt on a VPS →How to Deploy Plane on a VPS →How to Deploy Plausible Analytics on a VPS →How to Deploy Pocket ID on a VPS →How to Deploy PocketBase on a VPS →How to Deploy Prometheus on a VPS →How to Deploy Psono on a VPS →How to Deploy Radarr on a VPS →How to Deploy Rocket.Chat on a VPS →How to Deploy SigNoz on a VPS →How to Deploy Sonarr on a VPS →How to Deploy Stalwart on a VPS →How to Deploy Stirling-PDF on a VPS →How to Deploy Supabase on a VPS →How to Deploy Synapse on a VPS →How to Deploy Taiga on a VPS →How to Deploy TeamPass on a VPS →How to Deploy Tinyauth on a VPS →How to Deploy Traefik on a VPS →How to Deploy Trilium on a VPS →How to Deploy Twenty CRM on a VPS →How to Deploy Umami on a VPS →How to Deploy Uptime Kuma on a VPS →How to Deploy Vaultwarden on a VPS →How to Deploy Vikunja on a VPS →How to Deploy wg-easy on a VPS →How to Deploy Wiki.js on a VPS →How to Deploy Zabbix on a VPS →How to Deploy Zitadel on a VPS →How to Deploy Zulip on a VPS →Docker & Compose on Ubuntu 26.04 →Building AI Workflows with n8n →Install Open WebUI with Ollama →Adding AI-Powered Insights to Plausible Analytics →Building AI-Powered Apps with Supabase and pgvector →

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.