Skip to content

Front internal services with Pangolin — one name, no port — and codify the paired UniFi DNS record #314

Description

@ChrisonSimtian

Description

Internal services are currently reached as name:portmonitoring.homelab.chrison.internal:9091 for Prometheus, :3000 for Grafana, :9090 for Cockpit, :7655 for Pulse. That works, but the ports are load-bearing trivia: they collide (Cockpit and Prometheus both wanted 9090, #283 phase 3), they change, and every change means finding whatever hard-coded them.

We already run the thing that solves this — Pangolin. pulse.lab.chrison.dev is exactly the shape we want: one hostname, no port, proxied to host:7655. This issue is to do the same for internal-only services.

Important distinction, so the design starts honest: Pangolin is a reverse proxy, not a DNS server. It cannot serve *.homelab.chrison.internal — that resolution comes from UniFi's local DNS. What Pangolin provides is the name → service routing once the name resolves. So an internal service needs two things, and today they're maintained in different places by hand:

  1. a UniFi local-DNS record pointing the name at the proxy, and
  2. a Pangolin resource routing that hostname to target host:port.

Usage Example

Declare both from one place in the shape, so a service gets a portless internal name in a single edit:

# stacks/Core/pangolin.lxc.yaml
resources:
  - name: Prometheus
    subdomain: prometheus
    zone: internal                      # → prometheus.homelab.chrison.internal
    internalOnly: true                  # no Cloudflare record, no public exposure
    target: { ip: monitoring.homelab.chrison.internal, port: 9091, method: http }

Converge would then reconcile both sides: the Pangolin resource and a UniFi static-DNS A record for prometheus.homelab.chrison.internal → the Pangolin host. UniFi's standalone static-DNS API supports this — verified against the controller at /proxy/network/v2/api/site/default/static-dns, which is also how smarthome.homelab.chrison.internal was added.

Candidates once the mechanism exists: Grafana, Prometheus, Pulse (internal path), and Cockpit on each of the three podman hosts.

Points to settle in design

  • Internal zone naming. *.homelab.chrison.internal is the established internal namespace and deliberately separate from the public *.lab.chrison.dev / *.arr.chrison.dev (which are real Cloudflare records fronting private infra). An internalOnly resource must create no Cloudflare record.
  • TLS. Public resources get Let's Encrypt via DNS-01. .internal is not a real TLD, so LE can't issue for it — internal resources are either plain HTTP on the LAN or need an internal CA. Simplest first cut is HTTP.
  • Cockpit is per-host, so it needs three names (cockpit-monitoring, cockpit-media, cockpit-smarthome) rather than one.
  • Pangolin resource reconciler is add-only — it can't repoint an existing resource's target #309 blocks the useful half. The resource reconciler is add-only and cannot repoint an existing target, so a service that moves host or port silently keeps the old route. Worth fixing first, or this inherits the same trap.
  • SRV records are the only DNS type that carries a port, and UniFi does accept them (tested) — but browsers and curl ignore SRV, so they don't help here. Mentioned only to close off the obvious alternative.

Alternative

Keep using name:port. It costs nothing to run and every hostname already resolves. The downsides are the ones #283 phase 3 just hit: ports collide and get renumbered, and each renumbering is a hunt for whatever referenced the old one.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions