Publish apps through Caddy over a NetBird mesh — approval-gated, no LAN exposure, no hand-edited upstreams.
The coordinator lets any app on any Proxmox host be published through Caddy without the host being reachable on the LAN, without Caddy knowing the host’s address, and with every published app approved and auditable. It replaces per-host Caddy upstream entries with a registry that drives NetBird policies and Caddy configuration from one place.
Four problems with the current setup motivate it:
Project name: Forecaddie — the caddie who walks ahead and reports where everything landed. The product is the coordination layer only; NetBird is the transport and Caddy is the edge, both unmodified.
In scope
Out of scope
| Term | Meaning |
|---|---|
| Peer | A host enrolled in NetBird. Has a stable WireGuard key and overlay IP. One peer per LXC or VM. |
| Peer group | A NetBird group. Every enrolled app host gets its own group, assigned by its setup key. |
| App | One service on a peer: protocol + port + public hostname. A peer may host many apps (Docker-host VM). |
| Binding | The approved relationship app → peer → Caddy node(s). Moving an app means destroying one binding and creating another. |
| Caddy node | One Caddy instance (in the reference deployment, a single Caddy LXC). Also a NetBird peer, in group caddy. |
| Fragment | The one Caddy config file per node that the coordinator owns and Caddy imports. |
| Policy | A NetBird access-control rule: source group → destination group, protocol, ports. The coordinator owns all policies touching caddy. |
| Reconciler | The per-Caddy-node process that turns registry state into a fragment and policies and applies them. |
| Management | The NetBird management server (self-hosted) and its API. |
| IdP | The external OIDC provider (Pocket ID, Authentik, Keycloak, or similar). |
| Actor | Role in the system |
|---|---|
| Admin | Full control: approve, destroy, manage Caddy nodes, manage roles. |
| Operator | Register apps, enable/disable approved apps, view everything. Cannot approve or destroy. Intended for agents. |
| Viewer | Read-only access to status and audit. |
| Automation agents | Machine clients of the API. Act as Operator by default; Admin only by explicit grant. Authenticate as OIDC clients, never with shared passwords. |
| App host | A peer. Runs the app and the NetBird client. Has no coordinator software unless the optional enrollment helper is used. |
| Caddy node | Runs Caddy and the reconciler. Reads registry state, writes its fragment, reloads Caddy. |
| NetBird management | Source of truth for peers, groups, policies and peer status. The coordinator writes groups and policies through its API. |
| IdP | Sole source of human and agent identity for the coordinator. |
An approved app is reachable only from its Caddy node(s), only on its declared port, and only while approved. Everything below serves that sentence.
Enrollment
netbird up with the setup key, apply the host firewall rule (FR-05).wt0. The coordinator documents and, where it has a path to do so, verifies this.0.0.0.0. The UI shows a warning for apps on peers flagged as Docker hosts.App registry
http, https, tcp, udp), peer-side port, public hostname(s), L4 mode (SNI or dedicated public port), Caddy node(s), health-check definition, owner, notes.Approval lifecycle
pending → approved → disabled ↔ approved; any state → destroyed. Only Admin may approve or destroy; Operator may create, enable, disable.caddy → peer group, protocol, port), write the Caddy fragment(s), validate, reload. If any step fails the app stays pending and the failure is recorded.Caddy reconciliation
caddy validate on the full merged config before caddy reload; a failed validation leaves the previous fragment in place and the app in its previous state.reverse_proxy to <overlay-ip>:<port> with X-Forwarded-* headers set, active health checks enabled, and a 503 error page served via handle_errors when the upstream is unhealthy.L4 passthrough
Health and status
Multiple Caddy nodes and sites
caddy; policies are written per node group where per-node scoping is wanted (caddy-site-a, caddy-site-b).Management surface
API for agents
Audit
Security
All → All policy is removed during setup and the coordinator refuses to run while it exists.Availability and degraded modes
Performance
Operability
wt0 briefly.Observability
Portability
caddy-l4 is added for this project. The hand-maintained Caddyfile, auth portal and JWKS setup stay as they are.| Alternative | Why not |
|---|---|
| Pangolin | Closest existing match: outbound WireGuard tunnels (Newt), dashboard, OIDC, Integration API, clustering. But it is built on Traefik and brings its own request-auth layer (Badger), which would duplicate caddy-security and the external IdP and require abandoning the existing Caddy build. Dual AGPLv3 / commercial license. |
| frp | Solid transport with client IDs, OIDC client-credentials auth and a dashboard, but it is a tunnel, not a mesh: no per-peer firewall policy, no group model, and a coordinator would still have to be written on top. NetBird gives identity, policy and enrollment for free. |
| Gopher | Caddy + rathole in one binary with a web UI and host agent. Pre-1.0, no OIDC, owns Caddy rather than cooperating with an existing build. |
| Cloudflare Tunnel | Previously evaluated. Hands TLS termination and access control to a third party; the point here is self-hosting. |
| Split-horizon DNS to app hosts | Removes hairpinning but bypasses Caddy, so no central TLS, no auth gate, and apps back on the LAN. Rejected per the purpose section. |
| NetBird subnet routes as inter-site glue | The configuration that caused trouble before. Out of scope; the existing site-to-site VPN keeps that role. |
| Building the tunnel and agent from scratch | Most control, most work, and reproduces what NetBird already does well. |
caddy-site-a, caddy-site-b) from day one, or one caddy group until a second node exists?