Skip to main content

Try free on Pangolin Cloud

Fastest way to get started with Pangolin using the hosted control plane. No credit card required.
This guide walks through a manual deployment using the same file layout the installer generates from install/config/* in the Pangolin source tree. Use it if you want the installer’s defaults, but you want to create and maintain the files yourself. This guide assumes you already have a Linux server with Docker and Docker Compose installed, plus root or sudo access.

Prerequisites

Review the quick install guide and DNS & networking first. At minimum you need:
  • A public Linux server
  • A base domain such as example.com
  • A dashboard hostname such as pangolin.example.com
  • An email address for Let’s Encrypt
  • TCP ports 80 and 443 open
  • UDP ports 51820 and 21820 open if you are using tunneling
If you do not want tunneling, see Without Tunneling. In that mode you will skip the gerbil service and expose Traefik directly.
base domain is the parent domain you will attach resources to, such as example.com. dashboard hostname is the specific hostname for the Pangolin UI and API, such as pangolin.example.com.

File Layout

Create the following project structure:
.
├── docker-compose.yml
└── config/
    ├── config.yml
    ├── db/
    ├── letsencrypt/
    └── traefik/
        ├── dynamic_config.yml
        ├── logs/
        └── traefik_config.yml
The following files are created later by the running services or added only when you enable optional features:
  • config/db/db.sqlite is created by Pangolin on first startup.
  • config/key is created by Gerbil when tunneling is enabled.
  • config/GeoLite2-Country.mmdb is optional and only needed for geo-blocking. It is not downloaded by the running services in a manual install; download it manually before enabling geo-blocking.

Create the Directories

Create the project folders:
mkdir -p config/db config/letsencrypt config/traefik/logs

Create the Configuration Files

1

Create docker-compose.yml

This file defines the Pangolin, Gerbil, and Traefik containers, their shared volumes, and the ports exposed on the host.
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
      # - 443:443/udp # Uncomment if you enable HTTP/3 in Traefik.
      - 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
    # enable_ipv6: true
This is the installer’s default community layout with Gerbil enabled. If you want to pin releases instead of using latest, replace the image tags with the versions you intend to run.
2

Create config/traefik/traefik_config.yml

This file configures Traefik’s providers, Badger plugin, Let’s Encrypt resolver, entry points, logs, and health check endpoint.
config/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.4.0" # Check github.com/fosrl/badger for the latest release.

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

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

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: "30m"
    # Uncomment to enable HTTP/3. You must also expose 443/udp in docker-compose.yml.
    # http3:
    #   advertisedPort: 443
    http:
      tls:
        certResolver: "letsencrypt"
      encodedCharacters:
        allowEncodedSlash: true
        allowEncodedQuestionMark: true

serversTransport:
  insecureSkipVerify: true

ping:
  entryPoint: "web"
Traefik stores Let’s Encrypt certificates at /letsencrypt/acme.json inside the container. The Compose file mounts that path from ./config/letsencrypt, so Traefik will create config/letsencrypt/acme.json when it needs certificate storage.
3

Create config/traefik/dynamic_config.yml

This file defines the routers, middleware, and services that send dashboard, API, and WebSocket traffic to Pangolin.
config/traefik/dynamic_config.yml
http:
  middlewares:
    badger:
      plugin:
        badger:
          disableForwardAuth: true
    redirect-to-https:
      redirectScheme:
        scheme: https

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

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

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

    ws-router:
      rule: "Host(`pangolin.example.com`)" # REPLACE
      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
4

Create config/config.yml

This file contains Pangolin’s application settings, dashboard domain, base domain, CORS origin, and server secret.
config/config.yml
# To see all available options, please visit the docs:
# https://docs.pangolin.net/

gerbil:
    start_port: 51820
    base_endpoint: "pangolin.example.com" # REPLACE WITH YOUR DASHBOARD DOMAIN

app:
    dashboard_url: "https://pangolin.example.com" # REPLACE WITH YOUR DASHBOARD DOMAIN
    log_level: "info"
    telemetry:
        anonymous_usage: true

domains:
    domain1:
        base_domain: "example.com" # REPLACE WITH YOUR BASE DOMAIN

server:
    secret: "replace-with-a-long-random-secret" # REPLACE WITH SECURE SECRET
    cors:
        origins: ["https://pangolin.example.com"] # REPLACE WITH YOUR DASHBOARD DOMAIN
        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
Replace these values before starting the stack:
  • pangolin.example.com with your dashboard hostname
  • example.com with your base domain
  • replace-with-a-long-random-secret with a strong random secret
  • admin@example.com in traefik_config.yml with your Let’s Encrypt email
Generate a secret with:
openssl rand -hex 32
Do not reuse a weak or short server.secret. If you need to rotate it later, use pangctl rotate-server-secret. See the container CLI tool guide.

Optional Email Configuration

If you want Pangolin to send email, add this block to config/config.yml and set flags.require_email_verification to true:
config/config.yml
email:
    smtp_host: "smtp.example.com"
    smtp_port: 587
    smtp_user: "smtp-user"
    smtp_pass: "smtp-password"
    no_reply: "noreply@example.com"

Optional Geo-blocking Configuration

If you want geo-blocking, download the MaxMind database and add this line under server:
config/config.yml
server:
    maxmind_db_path: "./config/GeoLite2-Country.mmdb"
See Enable Geo-blocking for the full process.

Start the Stack

1

Start the services

sudo docker compose up -d
2

Watch the logs

sudo docker compose logs -f pangolin traefik gerbil
3

Verify the containers are healthy

sudo docker compose ps
pangolin, traefik, and gerbil should all report as running after the first startup finishes.
4

Get the setup token from the Pangolin logs

Check the Pangolin container logs:
sudo docker compose logs pangolin
Pangolin prints a setup token to stdout on first boot. Copy that token before continuing.
5

Open the initial setup page

Visit:
https://pangolin.example.com/auth/initial-setup
Replace the hostname with your real dashboard domain, then use the setup token from the Pangolin logs to register the first admin account.

Verify the Setup

You should expect the following on a healthy first install:
  • docker compose ps shows pangolin, traefik, and gerbil as running.
  • docker compose logs pangolin includes the one-time setup token for the first admin account.
  • Visiting https://<your-dashboard-domain>/auth/initial-setup loads the setup page.
  • config/db/db.sqlite exists after Pangolin starts.
  • config/key exists after Gerbil starts.
The first Let’s Encrypt certificate request can take a short while. If the page initially shows a certificate warning, wait a minute and refresh.

If Something Fails

  • If the setup page does not load, confirm your DNS record points to the server and ports 80 and 443 are reachable.
  • If you cannot complete first-time signup, check sudo docker compose logs pangolin and copy the setup token printed by Pangolin.
  • If certificates are not issued, confirm admin@example.com was replaced and that nothing else is already bound to ports 80 or 443.
  • If pangolin never becomes healthy, inspect sudo docker compose logs -f pangolin.
  • If tunneling does not work, inspect sudo docker compose logs -f gerbil and confirm UDP ports 51820 and 21820 are open.
  • If Traefik serves the wrong host, re-check every pangolin.example.com replacement in both Traefik files and config/config.yml.

Without Tunneling

If you do not want Gerbil:
  • Remove the gerbil service.
  • Remove network_mode: service:gerbil from traefik.
  • Add ports 80:80 and 443:443 directly to traefik.
  • Remove the gerbil block from config/config.yml.
That mode is covered in more detail in Without Tunneling.