Skip to content

Pangolin

Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.

References


Directory structure

  • Directorypangolin
    • Directoryconfig
      • config.yml (*)
      • Directorydb
        • db.sqlite
      • key
      • Directoryletsencrypt
        • acme.json
      • Directorylogs/
      • Directorytraefik
        • traefik_config.yml (*)
        • dynamic_config.yml (*)
    • docker-compose.yml (*)

Make directory

Terminal window
mkdir -p {{{DOCKER_PATH_VAR}}}/pangolin/config {{{DOCKER_PATH_VAR}}}/pangolin/traefik && cd {{{DOCKER_PATH_VAR}}}/pangolin

docker-compose.yml

Terminal window
nano docker-compose.yml
docker-compose.yml
services:
pangolin:
image: fosrl/pangolin:1.0.1
container_name: pangolin
restart: unless-stopped
volumes:
- ./config:/app/config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "3s"
timeout: "3s"
retries: 5
gerbil:
image: fosrl/gerbil:1.0.0
container_name: gerbil
restart: unless-stopped
depends_on:
pangolin:
condition: service_healthy
command:
- --reachableAt=http://gerbil:3003
- --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
volumes:
- ./config/:/var/config
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- 51820:51820/udp
- 443:443
- 80:80
traefik:
image: traefik:v3.3.3
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil
depends_on:
pangolin:
condition: service_healthy
command:
- --configFile=/etc/traefik/traefik_config.yml
environment:
CLOUDFLARE_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN}
volumes:
- ./config/traefik:/etc/traefik:ro
- ./config/letsencrypt:/letsencrypt
# newt:
# image: fosrl/newt
# container_name: newt
# restart: unless-stopped
# environment:
# PANGOLIN_ENDPOINT: ${PANGOLIN_ENDPOINT}
# NEWT_ID: ${NEWT_ID}
# NEWT_SECRET: ${NEWT_SECRET}
networks:
default:
driver: bridge
name: pangolin

config.yml

Terminal window
nano ./config/config.yml
./config/config.yml
app:
dashboard_url: "https://example.com" # REPLACE THIS WITH YOUR DASHBOARD URL
log_level: "info"
save_logs: true
log_failed_attempts: true
domains:
domain1:
base_domain: "example.com" # REPLACE THIS WITH YOUR DOMAIN
cert_resolver: "letsencrypt"
prefer_wildcard_cert: true
server:
external_port: 3000
internal_port: 3001
next_port: 3002
internal_hostname: "pangolin"
session_cookie_name: "p_session_token"
resource_access_token_param: "p_token"
resource_session_request_param: "p_session_request"
traefik:
cert_resolver: "letsencrypt"
http_entrypoint: "web"
https_entrypoint: "websecure"
gerbil:
start_port: 51820
base_endpoint: "example.com"
use_subdomain: false
block_size: 24
site_block_size: 30
subnet_group: 100.89.137.0/20
rate_limits:
global:
window_minutes: 1
max_requests: 100
# optional
# email:
# smtp_host: "host.hoster.net"
# smtp_port: 587
# smtp_user: "[email protected]"
# smtp_pass: "aaaaaaaaaaaaaaaaaa"
# no_reply: "[email protected]"
users:
server_admin:
password: "Password123$"
flags:
# require_email_verification: true # email smtp must be enabled to use
disable_signup_without_invite: true
disable_user_create_org: true
allow_raw_resources: true
allow_base_domain_resources: true

traefik_config.yml

Terminal window
nano ./traefik/traefik_config.yml
./traefik/traefik_config.yml
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.0.0"
log:
level: "INFO"
format: "common"
certificatesResolvers:
letsencrypt:
acme:
# httpChallenge: # use httpChallenge or dnsChallenge
# entryPoint: web
dnsChallenge:
provider: "cloudflare" # REPLACE THIS WITH YOUR DNS PROVIDER
email: [email protected] # REPLACE THIS WITH YOUR EMAIL
storage: "/letsencrypt/acme.json"
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" # staging
# caServer: "https://acme-v02.api.letsencrypt.org/directory" # production
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
serversTransport:
insecureSkipVerify: true

dynamic_config.yml

Terminal window
nano ./traefik/dynamic_config.yml
./traefik/dynamic_config.yml
http:
middlewares:
redirect-to-https:
redirectScheme:
scheme: https
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`pangolin.example.com`)" # REPLACE THIS WITH YOUR DOMAIN
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`pangolin.example.com`) && !PathPrefix(`/api/v1`)" # REPLACE THIS WITH YOUR DOMAIN
service: next-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# Only if using dnsChallenge
domains:
- main: "example.com" # REPLACE THIS WITH YOUR DOMAIN
sans:
- "*.example.com" # REPLACE THIS WITH YOUR DOMAIN
# API router (handles /api/v1 paths)
api-router:
rule: "Host(`pangolin.example.com`) && PathPrefix(`/api/v1`)" # REPLACE THIS WITH YOUR DOMAIN
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
# WebSocket router
ws-router:
rule: "Host(`proxy.example.com`)" # REPLACE THIS WITH YOUR DOMAIN
service: api-service
entryPoints:
- websecure
tls:
certResolver: letsencrypt
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server

Cloudflare DNS API token

.env

Terminal window
nano .env
.env
CLOUDFLARE_DNS_API_TOKEN=YOUR_TOKEN
PANGOLIN_ENDPOINT=https://example.com
NEWT_ID=YOUR_NEWT_ID
NEWT_SECRET=YOUR_NEWT_SECRET

Start container

Terminal window
docker compose up -d

Open web ui

http://localhost:3000 or http://{{{IP_ADDRESS_VAR}}}:3000