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
mkdir -p {{{DOCKER_PATH_VAR}}}/pangolin/config {{{DOCKER_PATH_VAR}}}/pangolin/traefik && cd {{{DOCKER_PATH_VAR}}}/pangolin
docker-compose.yml
nano 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
nano ./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
nano ./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 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
nano ./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
nano .env
CLOUDFLARE_DNS_API_TOKEN=YOUR_TOKENPANGOLIN_ENDPOINT=https://example.comNEWT_ID=YOUR_NEWT_IDNEWT_SECRET=YOUR_NEWT_SECRET
Start container
docker compose up -d