--- title: "Docker Compose" description: "Deploy Pangolin manually using Docker Compose without the automated installer" --- import PangolinCloudTocCta from "/snippets/pangolin-cloud-toc-cta.mdx"; 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](/self-host/quick-install) and [DNS & networking](/self-host/dns-and-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](/self-host/advanced/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: ```text . ├── 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](/self-host/advanced/enable-geoblocking). 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: ```bash mkdir -p config/db config/letsencrypt config/traefik/logs ``` ## Create the Configuration Files This file defines the Pangolin, Gerbil, and Traefik containers, their shared volumes, and the ports exposed on the host. ```yaml title="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. This file configures Traefik's providers, Badger plugin, Let's Encrypt resolver, entry points, logs, and health check endpoint. ```yaml title="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. This file defines the routers, middleware, and services that send dashboard, API, and WebSocket traffic to Pangolin. ```yaml title="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 ``` This file contains Pangolin's application settings, dashboard domain, base domain, CORS origin, and server secret. ```yaml title="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: ```bash 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](/self-host/advanced/container-cli-tool#rotate-server-secret). ### 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`: ```yaml title="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`: ```yaml title="config/config.yml" server: maxmind_db_path: "./config/GeoLite2-Country.mmdb" ``` See [Enable Geo-blocking](/self-host/advanced/enable-geoblocking) for the full process. ## Start the Stack ```bash sudo docker compose up -d ``` ```bash sudo docker compose logs -f pangolin traefik gerbil ``` ```bash sudo docker compose ps ``` `pangolin`, `traefik`, and `gerbil` should all report as running after the first startup finishes. Check the Pangolin container logs: ```bash sudo docker compose logs pangolin ``` Pangolin prints a setup token to stdout on first boot. Copy that token before continuing. Visit: ```text 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:///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](/self-host/advanced/without-tunneling).