Separates system/traefik (host+proxy infra) from the per-service
widgets into their own group, and gives the traefik widget an icon
and public dashboard link now that pulsenode's traefik widget
supports both.
Uses the new combined docker+API widget type shipped in pulsenode
v0.4.0. Umami and Passbolt stay as plain docker widgets - Umami has no
static API key for self-hosted instances (needs a login flow, skipped
for now), Passbolt has no widget-relevant API to poll.
The public traefik.pivoine.art path round-trips through the host's
published port, and Docker's hairpin NAT there rewrites the source IP
before it reaches vpn-only's allowlist, so pulsenode's own request was
getting a 403 despite falcon_network being allowlisted. Port 8080 is
not published to the host, so it's reachable only from other
falcon_network containers - no TLS/vpn-only needed, and it's not
exposed externally.
Previous attempt tried a separate internal-only :8080 entrypoint for
pulsenode to reach the API directly - that turned out to 404 no
matter what (--api.dashboard=true alone didn't register a working
router there, and adding --api=true plus an explicit entrypoint still
didn't produce a matching router; verified against a local traefik:v3
container before giving up on that path rather than keep guessing
against the live one).
Cleaner approach: expose it through the same web/web-secure
entrypoints everything else already uses, via a new file-provider
router (traefik/dynamic/dashboard.yaml) matching Traefik's own
documented self-referencing pattern - Host(`traefik.pivoine.art`) &&
(PathPrefix(`/api`) || PathPrefix(`/dashboard`)) routed to the built-in
api@internal service, gated by vpn-only same as pulsenode's own router.
vpn-only's sourceRange now also includes falcon_network's own subnet
(172.18.0.0/16) alongside the tailnet ranges - a container on our own
internal docker network is as trusted as a tailnet peer, and without
this pulsenode's own request to the API (a container-to-container
call, not a VPN-sourced one) would get the same 403 a random internet
visitor would. pulsenode's traefik widget now points at
https://traefik.pivoine.art/api instead of the internal :8080 attempt.
- traefik/compose.yml: flip --api.dashboard=true so Traefik's internal
API is reachable. No entrypoint named "traefik" is defined, so
Traefik auto-creates its default one on :8080 - not published to the
host (only 80/443 are), so it's reachable only from other containers
on falcon_network, i.e. exactly (and only) pulsenode.
- pulsenode/config.yml: adds a system widget and swaps the plain
Traefik docker card for the real traefik widget (router/entrypoint/
middleware view via that now-enabled API), both at the top of
Services.
Verified locally before deploying: a plain container (no pid: host,
no /proc,/sys mounts) already reports host-accurate CPU count, memory
total, and root disk usage via systeminformation, since Docker doesn't
namespace /proc/stat, /proc/meminfo, or overlayfs df by default. No
privilege escalation needed for the system widget after all - tested
side by side against the real host's nproc/free/df output. Also
confirmed the merged config.yml validates against the app's own zod
schema, not just as YAML.
Trims the deployed config down to what's actually useful day-to-day:
a single Services group with the 10 stacks as plain docker widgets
(traefik, coolify, gitea, code, immich, n8n, passbolt, umami,
headscale, mailpit). Drops the Data Services group (8 database
widgets), the Search widget, and auto-discovery (and the synthetic
Discovered group it produced) - all still fully supported by the app
itself and demonstrated in the pulsenode repo's own dogfood config,
just more than this deployment needs.
Mirrors the pulsenode app repo's updated config.yml (v0.3.0): removes
the leftover --pn-accent: #38bdf8 override that was masking the new
neon-green default, and adds icon: entries for every service that has
a brand match in lib/brand-icons.ts.
falcon-vpn-only was defined inline in falcon.yaml even though it's a
generic ipAllowList restriction with nothing falcon-specific about it
- security.yaml is where the other reusable middlewares (security-
headers, no-index, rate-limit) already live. Renamed to vpn-only to
match their bare naming style now that it's not tied to one file, and
updated falcon.yaml's two router references to vpn-only@file (same
cross-file pattern already used there for security-headers@file).
Also applies it to the pulsenode stack: it's a dashboard surfacing
container names/health for every service here, and there's no auth in
front of it yet, so gating it to the tailnet is a real mitigation for
that gap rather than leaving it open on the public hostname.
Infrastructure dashboard monitoring all the stacks in this repo, built
via stacks new pulsenode and then hand-adjusted for what a generic
scaffold can't know: pulls the published image
(dev.pivoine.art/valknar/pulsenode:latest) instead of building, mounts
/var/run/docker.sock:ro plus its own config/ directory read-only, runs
read-only/non-root/cap-dropped (group_add on DOCKER_GID to get socket
access without running as root), and uses its own healthcheck.mjs
rather than curl (not present in its minimal image).
config/config.yml is real, not a placeholder: docker/database widgets
for every container across all ten stacks here, discovery enabled
against falcon_network as a safety net for anything added later.