From 93ee0f9ec6c47c8eaec34ddbdf843aee5c45e5e6 Mon Sep 17 00:00:00 2001
From: Tediore <49540618+Tediore@users.noreply.github.com>
Date: Thu, 18 Jun 2026 23:45:42 -0500
Subject: [PATCH 1/3] add manual installation section for Podman
---
docs.json | 3 +-
self-host/manual/podman-quadlets.mdx | 489 +++++++++++++++++++++++++++
2 files changed, 491 insertions(+), 1 deletion(-)
create mode 100644 self-host/manual/podman-quadlets.mdx
diff --git a/docs.json b/docs.json
index 7bc6e57..4a4f429 100644
--- a/docs.json
+++ b/docs.json
@@ -231,7 +231,8 @@
]
}
]
- }
+ },
+ "self-host/manual/podman-quadlets"
]
},
"self-host/dns-and-networking",
diff --git a/self-host/manual/podman-quadlets.mdx b/self-host/manual/podman-quadlets.mdx
new file mode 100644
index 0000000..6108991
--- /dev/null
+++ b/self-host/manual/podman-quadlets.mdx
@@ -0,0 +1,489 @@
+---
+title: "Podman Quadlets (Rootless)"
+description: "Deploy Pangolin manually using Podman Quadlets (rootless) 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 Podman installed.
+
+## 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`.
+
+
+
+## Note about ports 80 and 443
+By default, unprivileged users cannot bind to privileged ports (< 1024). Some workarounds for this include:
+1. Changing the unprivileged start port to 80
+2. Using `iptables` / `nftables` to redirect 80 and 443 to ports above 1023 (such as 8080 and 8443, respectively)
+
+Configuring this is out of the scope of this guide, but many guides exist online for this exact situation.
+
+**This guide assumes you use option 1.**
+
+
+
+## File Layout
+
+Create the following project structure:
+
+```text
+~/.config/
+ └── containers/
+ └── systemd
+ ├── gerbil.container
+ ├── pangolin.container
+ ├── traefik.container
+ ├── services.pod
+ └── 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/containers/systemd # only needed if this is the first time you're running rootless Podman containers
+cd ~/.config/containers/systemd
+mkdir -p config/db config/letsencrypt config/traefik/logs
+```
+
+## Create the Configuration Files
+
+
+
+ This section defines the Pangolin, Gerbil, and Traefik containers, the pod, their shared volumes, and the ports exposed on the host.
+
+```ini title="pangolin.container"
+[Container]
+ContainerName=pangolin
+Image=docker.io/fosrl/pangolin:ee-latest
+
+Pod=services.pod
+
+HealthCmd=["curl","-f","http://localhost:3001/api/v1/"]
+HealthInterval=10s
+HealthRetries=15
+HealthTimeout=10s
+Notify=healthy
+
+Volume=./config:/app/config
+Volume=./config/letsencrypt:/app/config/letsencrypt:ro
+
+[Service]
+Restart=always
+
+[Install]
+WantedBy=default.target
+```
+
+```ini title="gerbil.container"
+[Unit]
+After=pangolin.service
+Requires=pangolin.service
+
+[Container]
+ContainerName=gerbil
+Image=docker.io/fosrl/gerbil:latest
+
+Pod=services.pod
+
+AddCapability=NET_ADMIN SYS_MODULE
+Exec='--reachableAt=http://localhost:3004' '--generateAndSaveKeyTo=/var/config/key' '--remoteConfig=http://localhost:3001/api/v1/'
+
+Volume=./config/:/var/config
+
+[Service]
+Restart=always
+
+[Install]
+WantedBy=default.target
+```
+
+```ini title="traefik.container"
+[Unit]
+After=pangolin.service
+Requires=pangolin.service
+
+[Container]
+ContainerName=traefik
+Image=docker.io/traefik:latest
+
+Pod=services.pod
+
+Exec='--configFile=/etc/traefik/traefik_config.yml'
+
+Volume=./config/traefik:/etc/traefik:ro
+Volume=./config/letsencrypt:/letsencrypt
+Volume=./config/traefik/logs:/var/log/traefik
+
+[Service]
+Restart=always
+
+[Install]
+WantedBy=default.target
+```
+
+```ini title="services.pod"
+[Unit]
+Description=Pangolin Pod
+Wants=network-online.target
+After=network-online.target
+
+[Pod]
+PodName=services
+
+Network=pasta
+
+PublishPort=51820:51820/udp
+PublishPort=21820:21820/udp
+PublishPort=443:443
+# Uncomment the line below if you enable HTTP/3 in Traefik.
+# PublishPort=443:443/udp
+PublishPort=80:80
+
+[Service]
+Restart=always
+
+[Install]
+WantedBy=default.target
+```
+
+
+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://localhost: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 services.pod.
+ # 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 container 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://localhost:3002"
+
+ api-service:
+ loadBalancer:
+ servers:
+ - url: "http://localhost: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).
+Please note you will need to run `podman exec ...` instead of `docker exec ...`.
+
+
+
+
+
+### 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
+ systemctl --user daemon-reload
+ systemctl --user start services-pod
+ ```
+
+
+
+ ```bash
+ podman logs -f pangolin traefik gerbil
+ ```
+
+
+
+ ```bash
+ podman ps -a
+ ```
+
+ `pangolin`, `traefik`, and `gerbil` should all report as running after the first startup finishes.
+
+
+
+ Check the Pangolin container logs:
+
+ ```bash
+ podman 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:
+
+- `podman ps -a` shows `pangolin`, `traefik`, and `gerbil` as running.
+- `podman 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 `podman 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` (or whatever alternate ports you selected on the host).
+- If `pangolin` never becomes healthy, inspect `podman logs -f pangolin`.
+- If tunneling does not work, inspect `podman 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 the `gerbil` block from `config/config.yml`.
+
+That mode is covered in more detail in [Without Tunneling](/self-host/advanced/without-tunneling).
From 080cd514bba52bba33ae41d3f5c6716c6ef0099d Mon Sep 17 00:00:00 2001
From: Tediore <49540618+Tediore@users.noreply.github.com>
Date: Thu, 18 Jun 2026 23:58:26 -0500
Subject: [PATCH 2/3] add missing section about lingering
---
self-host/manual/podman-quadlets.mdx | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/self-host/manual/podman-quadlets.mdx b/self-host/manual/podman-quadlets.mdx
index 6108991..8bde705 100644
--- a/self-host/manual/podman-quadlets.mdx
+++ b/self-host/manual/podman-quadlets.mdx
@@ -421,6 +421,12 @@ See [Enable Geo-blocking](/self-host/advanced/enable-geoblocking) for the full p
```
+
+ ```bash
+ loginctl enable-linger $USER
+ ```
+
+
```bash
podman logs -f pangolin traefik gerbil
From 2748ccc1b47e4958faafee41d4923e9cef30d8a2 Mon Sep 17 00:00:00 2001
From: Tediore <49540618+Tediore@users.noreply.github.com>
Date: Fri, 19 Jun 2026 07:55:41 -0500
Subject: [PATCH 3/3] remove unneeded letsencrypt email and add note about
Linux environment
---
self-host/manual/podman-quadlets.mdx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/self-host/manual/podman-quadlets.mdx b/self-host/manual/podman-quadlets.mdx
index 8bde705..29c3e7e 100644
--- a/self-host/manual/podman-quadlets.mdx
+++ b/self-host/manual/podman-quadlets.mdx
@@ -9,7 +9,7 @@ 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 Podman installed.
+This guide assumes you already have a Linux server with Podman installed and has been tested on Debian 13.5 with Podman version 5.4.2.
## Prerequisites
@@ -220,7 +220,6 @@ certificatesResolvers:
acme:
httpChallenge:
entryPoint: web
- email: "admin@example.com" # REPLACE
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"