Docker Compose
Deploy Pangolin manually using Docker Compose without the automated installer
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
80and443open - UDP ports
51820and21820open 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.ymlThe following files are created later by the running services or added only when you enable optional features:
config/db/db.sqliteis created by Pangolin on first startup.config/keyis created by Gerbil when tunneling is enabled.config/GeoLite2-Country.mmdbis 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/logsCreate the Configuration Files
Create docker-compose.yml
This file defines the Pangolin, Gerbil, and Traefik containers, their shared volumes, and the ports exposed on the host.
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.7
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: trueThis 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.
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.
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.
Create config/traefik/dynamic_config.yml
This file defines the routers, middleware, and services that send dashboard, API, and WebSocket traffic to Pangolin.
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: 2Create config/config.yml
This file contains Pangolin's application settings, dashboard domain, base domain, CORS origin, and server secret.
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: trueReplace these values before starting the stack:
pangolin.example.comwith your dashboard hostnameexample.comwith your base domainreplace-with-a-long-random-secretwith a strong random secretadmin@example.comintraefik_config.ymlwith your Let's Encrypt email
Generate a secret with:
openssl rand -hex 32Do 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:
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:
server:
maxmind_db_path: "./config/GeoLite2-Country.mmdb"See Enable Geo-blocking for the full process.
Start the Stack
Start the services
sudo docker compose up -dWatch the logs
sudo docker compose logs -f pangolin traefik gerbilVerify the containers are healthy
sudo docker compose pspangolin, traefik, and gerbil should all report as running after the first startup finishes.
Get the setup token from the Pangolin logs
Check the Pangolin container logs:
sudo docker compose logs pangolinPangolin prints a setup token to stdout on first boot. Copy that token before continuing.
Open the initial setup page
Visit:
https://pangolin.example.com/auth/initial-setupReplace 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 psshowspangolin,traefik, andgerbilas running.docker compose logs pangolinincludes the one-time setup token for the first admin account.- Visiting
https://<your-dashboard-domain>/auth/initial-setuploads the setup page. config/db/db.sqliteexists after Pangolin starts.config/keyexists 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
80and443are reachable. - If you cannot complete first-time signup, check
sudo docker compose logs pangolinand copy the setup token printed by Pangolin. - If certificates are not issued, confirm
admin@example.comwas replaced and that nothing else is already bound to ports80or443. - If
pangolinnever becomes healthy, inspectsudo docker compose logs -f pangolin. - If tunneling does not work, inspect
sudo docker compose logs -f gerbiland confirm UDP ports51820and21820are open. - If Traefik serves the wrong host, re-check every
pangolin.example.comreplacement in both Traefik files andconfig/config.yml.
Without Tunneling
If you do not want Gerbil:
- Remove the
gerbilservice. - Remove
network_mode: service:gerbilfromtraefik. - Add ports
80:80and443:443directly totraefik. - Remove the
gerbilblock fromconfig/config.yml.
That mode is covered in more detail in Without Tunneling.
Was this page helpful?

