diff --git a/AGENTS.md b/AGENTS.md index 596a31b..c1be4e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ networks: These deliberately violate the conventions above — do not "fix" them: - `pihole` uses `network_mode: host` (it's a DNS server), so it has no `proxy` network +- `pangolin_dev` is itself an edge reverse proxy (Pangolin + gerbil + traefik), not a client of NPM; the `proxy` network is declared for convention but unused by its services, and `traefik` uses `network_mode: service:gerbil` instead of joining `internal` - `zammad_dev` `zammad-init` service hardcodes `restart: on-failure` (init container) - `nextcloud/compose-aio.yaml` follows the upstream AIO layout: fixed container/volume names, no env templating, no proxy network - `opencode` and `vscode` default `VOL_PROJECTS` to `${VOL_PATH:-/data}/projects` (nested default), not plain `/data` diff --git a/pangolin_dev/README.md b/pangolin_dev/README.md new file mode 100644 index 0000000..839f1bc --- /dev/null +++ b/pangolin_dev/README.md @@ -0,0 +1,98 @@ +# Pangolin + +## Overview + +Pangolin is a self-hosted tunneled reverse proxy with identity-aware access control. It exposes services running behind NAT or on a private LAN to the internet without port-forwarding, using its own WireGuard tunnel (`gerbil`) and a bundled Traefik instance for TLS termination and routing. + +Unlike most stacks in this repo, Pangolin **is itself the edge reverse proxy** — it does not sit behind Nginx Proxy Manager. It needs its own public IP and listens directly on ports 80/443/51820. + +## Project Details + +* Project Repository: https://github.com/fosrl/pangolin +* Container Image: https://hub.docker.com/r/fosrl/pangolin +* Compose Example: https://github.com/fosrl/pangolin/blob/main/docker-compose.example.yml +* Documentation: https://docs.fossorial.io/ +* Reverse Proxy Domain: pangolin.example.com +* Internal Service Port: 3000 (API), 3001 (internal API), 3002 (dashboard/next) + +## Getting Started + +1. Copy the starter config into place and edit it: + ```bash + mkdir -p /data/pangolin/config /data/pangolin/traefik /data/pangolin/letsencrypt /data/pangolin/gerbil + cp config/config.yml /data/pangolin/config/config.yml + cp config/traefik/traefik_config.yml config/traefik/dynamic_config.yml /data/pangolin/traefik/ + nano /data/pangolin/config/config.yml # set dashboard_url, base_domain, secret, admin email/password + nano /data/pangolin/traefik/*.yml # replace pangolin.example.com with your real domain + ``` +2. Copy env defaults: `cp sample.env .env` +3. Start the stack: `docker compose up -d` +4. Open `https://pangolin.example.com` and log in with the admin user set in `config.yml` +5. Follow the in-app wizard to create a site (installs a `newt` client on the machine you want to expose) and add resources + +## Environment Variable Notes + +``` +PANGOLIN_IMAGE – Docker image for the Pangolin server (default: fosrl/pangolin:latest) +PANGOLIN_RESTART – Restart policy + +GERBIL_IMAGE – Docker image for the gerbil WireGuard gateway (default: fosrl/gerbil:latest) +GERBIL_RESTART – Restart policy +GERBIL_WG_PORT – UDP port for the WireGuard tunnel (default: 51820) +GERBIL_HTTP_PORT – Public HTTP port, proxied through gerbil to traefik (default: 80) +GERBIL_HTTPS_PORT – Public HTTPS port, proxied through gerbil to traefik (default: 443) + +TRAEFIK_IMAGE – Docker image for Traefik (default: traefik:v3.3) +TRAEFIK_RESTART – Restart policy + +VOL_PATH – Base path for config/data volumes (default: /data) +TZ – Timezone (default: America/Vancouver) +``` + +## Volume Notes + +``` +${VOL_PATH}/pangolin/config – config.yml (required, see config/config.yml starter) +${VOL_PATH}/pangolin/gerbil – gerbil's generated WireGuard key/state +${VOL_PATH}/pangolin/traefik – traefik_config.yml and dynamic_config.yml (required, see config/traefik/) +${VOL_PATH}/pangolin/letsencrypt – ACME certificate storage (acme.json) +``` + +## Network Notes + +* **Does not sit behind NPM like other stacks in this repo** — Pangolin is its own edge reverse proxy. The `proxy` network is declared for convention but none of its services actually join it (see AGENTS.md "Known exceptions"). +* `traefik` runs with `network_mode: service:gerbil`, sharing gerbil's network namespace so it can bind the ports gerbil forwards from the WireGuard tunnel. +* Only `gerbil` publishes ports to the host: `51820/udp` (WireGuard), `80/tcp`, `443/tcp`. +* Requires a public IP with 80/443/51820 open inbound (or forwarded, if behind NAT). This is a different deployment model than the LAN-only, NPM-fronted stacks elsewhere in this repo. + +## Docker Run + +```bash +docker run -d \ + --name=pangolin \ + -v /data/pangolin/config:/app/config \ + fosrl/pangolin:latest +``` + +See compose.yaml for the full multi-container setup (`pangolin` + `gerbil` + `traefik`) — Pangolin does not function correctly as a single container. + +## Additional Notes / Gotchas + +* All three services (`pangolin`, `gerbil`, `traefik`) must run together; gerbil needs `NET_ADMIN` and `SYS_MODULE` capabilities for WireGuard. +* `dashboard_url`, `base_domain`, and the `pangolin.example.com` references in the Traefik dynamic config must all be updated to match your real domain before first start. +* Change `secret` and the admin `password` in `config.yml` before exposing this publicly — the repo default of `ChangeThisPassword` is not safe to leave in place, since this stack is internet-facing by design. +* After a site is registered in the dashboard, exposed services are reached by installing the lightweight `newt` client on the target host — see the Pangolin docs for `newt` setup. +* Certificates are issued automatically via Traefik's ACME HTTP challenge on port 80; keep port 80 open even if you only care about HTTPS. + +## Dockhand Stack, Deploy from Git + +Cookbooks Repository +stackname: pangolin_dev +Compose file path: pangolin_dev/compose.yaml +Additional env file (optional): pangolin_dev/sample.env + +Then "Load" pangolin_dev/sample.env into the Environmental variables in dockhand. + +Ensure `config.yml`, `traefik_config.yml`, and `dynamic_config.yml` exist under `${VOL_PATH}/pangolin/` before deploying. + +Create the Stack. diff --git a/pangolin_dev/compose.yaml b/pangolin_dev/compose.yaml new file mode 100644 index 0000000..36aff06 --- /dev/null +++ b/pangolin_dev/compose.yaml @@ -0,0 +1,50 @@ +services: + pangolin: + image: ${PANGOLIN_IMAGE:-fosrl/pangolin:latest} + restart: ${PANGOLIN_RESTART:-unless-stopped} + volumes: + - ${VOL_PATH:-/data}/pangolin/config:/app/config + networks: + - internal + + gerbil: + image: ${GERBIL_IMAGE:-fosrl/gerbil:latest} + restart: ${GERBIL_RESTART:-unless-stopped} + volumes: + - ${VOL_PATH:-/data}/pangolin/gerbil:/var/config + environment: + - TZ=${TZ:-America/Vancouver} + ports: + - ${GERBIL_WG_PORT:-51820}:51820/udp + - ${GERBIL_HTTP_PORT:-80}:80 + - ${GERBIL_HTTPS_PORT:-443}:443 + networks: + - internal + depends_on: + - pangolin + cap_add: + - NET_ADMIN + - SYS_MODULE + command: + - --reachableAt=http://gerbil:3003 + - --generateAndSaveKeyTo=/var/config/key + - --remoteConfig=http://pangolin:3001/api/v1/ + - --reportBandwidthTo=http://pangolin:3001/api/v1 + + traefik: + image: ${TRAEFIK_IMAGE:-traefik:v3.3} + restart: ${TRAEFIK_RESTART:-unless-stopped} + volumes: + - ${VOL_PATH:-/data}/pangolin/traefik:/etc/traefik:ro + - ${VOL_PATH:-/data}/pangolin/letsencrypt:/letsencrypt + depends_on: + - pangolin + - gerbil + network_mode: service:gerbil + command: + - --configFile=/etc/traefik/traefik_config.yml + +networks: + proxy: + external: true + internal: diff --git a/pangolin_dev/config/config.yml b/pangolin_dev/config/config.yml new file mode 100644 index 0000000..9a0c129 --- /dev/null +++ b/pangolin_dev/config/config.yml @@ -0,0 +1,44 @@ +app: + dashboard_url: "https://pangolin.example.com" + log_level: "info" + save_logs: false + +domains: + domain1: + base_domain: "example.com" + cert_resolver: "letsencrypt" + +server: + external_port: 3000 + internal_port: 3001 + next_port: 3002 + session_cookie_name: p_session_token + resource_session_cookie_name: p_session_token + secret: ChangeThisPassword + +traefik: + cert_resolver: letsencrypt + http_entrypoint: web + https_entrypoint: websecure + +gerbil: + start_port: 51820 + base_endpoint: "example.com" + use_subdomain: false + +rate_limits: + global: + window_minutes: 1 + max_requests: 500 + +users: + server_admin: + email: admin@example.com + password: ChangeThisPassword + +flags: + require_email_verification: false + disable_signup_without_invite: true + disable_user_create_org: true + allow_raw_resources: true + enable_integration_api: false diff --git a/pangolin_dev/config/traefik/dynamic_config.yml b/pangolin_dev/config/traefik/dynamic_config.yml new file mode 100644 index 0000000..bb7cf63 --- /dev/null +++ b/pangolin_dev/config/traefik/dynamic_config.yml @@ -0,0 +1,33 @@ +http: + middlewares: + redirect-to-https: + redirectScheme: + scheme: https + + routers: + main-app-router-redirect: + rule: "Host(`pangolin.example.com`)" + service: next-service + entryPoints: + - web + middlewares: + - redirect-to-https + + main-app-router: + rule: "Host(`pangolin.example.com`)" + service: next-service + entryPoints: + - websecure + tls: + certResolver: letsencrypt + + services: + next-service: + loadBalancer: + servers: + - url: "http://pangolin:3002" + + api-service: + loadBalancer: + servers: + - url: "http://pangolin:3000" diff --git a/pangolin_dev/config/traefik/traefik_config.yml b/pangolin_dev/config/traefik/traefik_config.yml new file mode 100644 index 0000000..0cd3c62 --- /dev/null +++ b/pangolin_dev/config/traefik/traefik_config.yml @@ -0,0 +1,34 @@ +api: + insecure: true + dashboard: true + +providers: + http: + endpoint: "http://pangolin:3001/api/v1/traefik-config" + pollInterval: "5s" + file: + filename: "/etc/traefik/dynamic_config.yml" + +log: + level: "INFO" + format: "common" + +certificatesResolvers: + letsencrypt: + acme: + httpChallenge: + entryPoint: web + email: admin@example.com + storage: "/letsencrypt/acme.json" + caServer: "https://acme-v02.api.letsencrypt.org/directory" + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + transport: + respondingTimeouts: + readTimeout: "30m" + metrics: + address: ":9101" diff --git a/pangolin_dev/sample.env b/pangolin_dev/sample.env new file mode 100644 index 0000000..705d998 --- /dev/null +++ b/pangolin_dev/sample.env @@ -0,0 +1,15 @@ +# Pangolin +VOL_PATH=/data +TZ=America/Vancouver + +PANGOLIN_IMAGE=fosrl/pangolin:latest +PANGOLIN_RESTART=unless-stopped + +GERBIL_IMAGE=fosrl/gerbil:latest +GERBIL_RESTART=unless-stopped +GERBIL_WG_PORT=51820 +GERBIL_HTTP_PORT=80 +GERBIL_HTTPS_PORT=443 + +TRAEFIK_IMAGE=traefik:v3.3 +TRAEFIK_RESTART=unless-stopped