Host Your Own AI Agent with OpenClaw - Free 1-Click Setup!

How to Self-Host Pangolin on a VPS (Replace Cloudflare Tunnels)

How to Self-Host Pangolin on a VPS (Head image)

In short. Pangolin is a potential Cloudflare Tunnel alternative or Tailscale alternative. Self-hosting Pangolin means running the Pangolin VPN server on a VPS with a public IP, deploying the Newt client on your private network, and using WireGuard tunnels to route traffic securely. No port forwarding is required on your home router. The setup takes under 30 minutes with Docker Compose. Pangolin handles SSL certificates, reverse proxying, and identity-aware access control from one dashboard.

Prerequisites

Before you start with the Pangolin install, make sure you have:

  • A VPS with a public IP. An entry-level Contabo VPS handles the full stack comfortably, with room to run additional services alongside it.
  • Ubuntu 22.04, 24.04 or 26.04 (fresh install recommended).
  • Docker and Docker Compose plugin installed.
  • A domain or subdomain with an A record pointed at the VPS IP. You must have access to the DNS zone management for your domain.
  • Ports 22/TCP, 80/TCP, 443/TCP, 51820/UDP, and 21820/UDP open in your firewall. You can easily set this up with a Contabo Firewall.

On the private-network side – your home server, office machine, or any remote device – you need Docker or the Newt binary. No open ports, no port forwarding, no public IP required on that machine.

Step 1: Prepare the VPS

SSH in as root and update the system before anything else.

  1. Connect: `ssh root@<your-vps-ip>`
  2. Update: `apt update &amp;& apt upgrade -y`
  3. Install Docker and the Compose plugin from the official Docker apt repository:
apt install -y ca-certificates curl gnupg

install -m 0755 -d /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg \

  -o /etc/apt/keyrings/docker.ascah 

chmod a+r /etc/apt/keyrings/docker.asc

echo \

  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \

  https://download.docker.com/linux/ubuntu \

  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \

  | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update

apt install -y docker-ce docker-ce-cli containerd.io \wai

  docker-buildx-plugin docker-compose-plugin

Verify with `docker compose version`. You should see a 2.x version string.

Next, open the ports Pangolin needs. Every Contabo VPS and VDS comes with a free built-in firewall you can manage directly from the Customer Control Panel, with no extra software to install. Open the Contabo Firewall guide and add inbound allow rules for: 22/TCP, 80/TCP, 443/TCP, 51820/UDP, and 21820/UDP

Step 2: Install Pangolin with Docker Compose

Pangolin’s server stack runs as three containers. Pangolin handles the dashboard, API, and access control. Gerbil manages the WireGuard interface on the VPS side and owns the exposed ports. Traefik runs inside Gerbil’s network namespace rather than directly on the host. Traefik terminates TLS, provisions Let’s Encrypt certificates automatically, and routes HTTP traffic to the right destination. CrowdSec can be added later as an optional Traefik plugin for reputation-based blocking.

Create the project directory and required subdirectories:

mkdir -p /opt/pangolin/config/db \

         /opt/pangolin/config/letsencrypt \

         /opt/pangolin/config/traefik/logs

cd /opt/pangolin

Generate a secret for Pangolin’s session signing. You will need the output in `config.yml`:

openssl rand -hex 32

Create the four configuration files below. Write each one to the path shown before moving on to the next.

`docker-compose.yml`

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

      - 22:22


  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

`config/traefik/traefik_config.yml` – replace `[email protected]` with your real email address:

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: "[email protected]"

      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"

`config/traefik/dynamic_config.yml` – replace every instance of `pangolin.example.com` with your dashboard hostname:

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

`config/config.yml` – replace all three placeholder domains and paste the secret you generated with `openssl`:

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: "paste-your-openssl-output-here"

    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: false

    disable_user_create_org: false

    allow_raw_resources: true

Step 3: Start Pangolin and Access the Dashboard

  1. Start the stack: docker compose up -d
  2. Check all three containers are running: docker compose ps. Gerbil and Traefik wait for Pangolin’s healthcheck to pass before starting, so give it about 30 seconds.
  3. Retrieve the one-time setup token from the Pangolin logs:
docker compose logs pangolin

Look for the block that reads === SETUP TOKEN GENERATED === and copy the token.

4. Open https://pangolin.example.com/auth/initial-setup in a browser. The first Let’s Encrypt certificate request can take up to 60 seconds. If you see a certificate warning on first load, wait a minute and refresh.

5. Paste the setup token, set your admin email and password, then create your first organization.

If Traefik cannot obtain a certificate, confirm your DNS A record is propagated and nothing else is bound to ports 80 or 443 on the host.

Step 4: Connect a Private Site with Newt over WireGuard

Newt is Pangolin’s site connector agent. It runs on your private network, makes an outbound WireGuard connection to the Pangolin hub, and proxies traffic to your local services. Because Pangolin Newt initiates the connection outbound, your home router needs no open ports, no port forwarding, and no public IP.

In the Pangolin dashboard, go to Sites, click Add Site, choose Newt Tunnel, and give the site a name. Pangolin shows you three credentials: the endpoint URL, the Newt ID, and the Newt secret. Copy all three. They are shown only once.

On your home server, deploy Newt with Docker Compose:

services:

  newt:

    image: fosrl/newt

    container_name: newt

    restart: unless-stopped

    environment:

      - PANGOLIN_ENDPOINT=https://pangolin.example.com

      - NEWT_ID=<your-newt-id>

      - NEWT_SECRET=<your-newt-secret>

Start it with docker compose up -d. Within seconds the site indicator in the dashboard of your self-hosted Pangolin turns green and shows Online.

Step 5: Expose Your First Service via Reverse Proxy

With the Newt tunnel online, exposing a service requires no config file edits. Everything is done from the Pangolin dashboard:

  1. Open your site and click Add Resource.
  2. Give the resource a name and choose a subdomain.
  3. Under Target Configuration, enter the internal address of the service as Newt sees it. For a service at 192.168.1.10:3000 on your home network, enter http://192.168.1.10:3000. For a service on the same Docker network as Newt, use its container name and port.
  4. Enable HTTPS. Traefik provisions the certificate automatically.
  5. Set an access policy: public, password-protected, or restricted to specific users or email domains.

The resource is live at its subdomain within seconds. If you add a wildcard A record (*.pangolin.example.com pointing at the VPS IP), each new resource is reachable immediately without a separate DNS entry per service.

Step 6: Add Browser-Based SSH / RDP Access (v1.19+)

Pangolin 1.19 added SSH, RDP, and VNC as first-class public resource types alongside HTTP. A user opens the resource URL, authenticates through Pangolin, and gets a full interactive session in the browser. No separate SSH client, remote desktop app, or VNC viewer is required.

To enable browser SSH for VPS, click Add Resource, select SSH as the protocol, and choose Pangolin SSH mode. This mode requires only that Newt runs as root on the target machine. No changes to sshd_config and no PAM configuration are needed. Assign a subdomain and an access policy and the terminal is live in the browser.

For RDP, select RDP as the protocol, point the target at the Windows host’s IP and port 3389, and assign a domain. The session renders in the browser with full clipboard and file transfer support.

Browser-based SSH, RDP, and VNC require Newt 1.13.0 or later. Run docker compose pull &&amp; docker compose up -d on your home server before enabling these resource types.

Hardening Checklist

Before exposing anything sensitive, work through this list:

  • Set a strong admin password and change it immediately if you used a weak one during initial setup.
  • Restrict dashboard access to known IP addresses via a Pangolin access policy.
  • Keep Pangolin, Gerbil, and Traefik images updated. Run `docker compose pull && docker compose up -d` from `/opt/pangolin` regularly. Pangolin 1.19 added optional automatic Newt updates you can enable per-site from the dashboard.
  • Confirm your Contabo Firewall rules allow only the ports you need.
  • Enable CrowdSec as an optional Traefik plugin if your resources face public traffic. It blocks known-bad IPs at the edge before requests reach your services.
  • Back up the `config/` directory on the VPS regularly. It holds the SQLite database, TLS certificates, and all resource configuration.

FAQ: Self-Hosting Pangolin

How much RAM does Pangolin need on a VPS?

The Pangolin, Gerbil, and Traefik containers together idle at a few hundred MB of RAM. A VPS with 4 GB RAM covers the stack comfortably with room for several active tunnels. If you plan to run additional services on the same VPS – a monitoring stack, a wiki, or a database – step up to 8 GB or more so the services do not compete under load.

Can I self-host Pangolin without a domain?

Not with the standard setup. Traefik uses the Let’s Encrypt HTTP-01 challenge to provision certificates, which requires a domain with a valid A record pointing at the VPS. A domain costs a few euros per year and is worth it for any persistent remote access setup. Pangolin Cloud is an alternative if you want the control plane managed for you without running a VPS at all.

What is the Newt client in Pangolin?

Newt is Pangolin’s site connector: a user-space WireGuard tunnel client that runs on your private network and establishes an outbound encrypted connection to the Pangolin server. Because Newt initiates the connection outbound, your home router needs no open ports, not even WireGuard’s default UDP 51820. Newt also acts as a TCP/UDP proxy, forwarding traffic from the Pangolin reverse proxy to services anywhere on your local network.

How do I update Pangolin?

From the `/opt/pangolin` directory, run `docker compose pull && docker compose up -d`. Always back up the `config/` directory first. It contains the SQLite database and TLS certificates, and there is no easy downgrade path once migrations have run. For Newt on your home server, the same `docker compose pull && docker compose up -d` applies. Pangolin 1.19 introduced optional automatic Newt updates: enable them in the dashboard at the organization or per-site level to keep edge devices current without manual intervention.

Scroll to Top