Switches traefik.pivoine.art routing from a standalone file-provider
router (dynamic/dashboard.yaml) to docker-compose labels on traefik's
own service - the same pattern every other stack already uses for its
own routing, rather than a one-off exception. Traefik discovers itself
just like any other container on falcon_network. Both routers point
at the built-in api@internal service via .service= instead of a
loadbalancer target, since there's no backend container port to route
to. Also dropped the (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
condition - the whole traefik.pivoine.art host is dedicated to this,
so a bare Host() match is simpler and no less correct.
Also fixes why VPN access still failed: traefik.pivoine.art was never
added to Headscale's MagicDNS extra_records, so even a tailnet-
connected browser resolved it via public DNS and hit vpn-only from a
non-tailnet source IP - same root cause as the earlier
pulsenode.pivoine.art fix, just missed for this hostname.
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.
--api.dashboard=true alone wasn't enough - the API returned 404 on
every path after enabling it. Traefik apparently doesn't reliably
auto-create the default "traefik" entrypoint the way older docs
describe; defining it explicitly is the documented, reliable way to
get the API/dashboard router actually listening. Still not in ports:,
so still only reachable from other containers on falcon_network.
- 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.
v0.7.1 caused 403 on all routes. v0.9.0 renamed whitelist→allowlist,
changed ip to a comma-separated string, and added enabled/statuscode fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Registers tomMoulard/fail2ban v0.7.1 as an experimental plugin and
applies it globally on the web-secure entrypoint alongside security-headers.
Bans IPs after 5 failures within 10 minutes for 3 hours; whitelists
RFC1918 ranges to protect internal/Docker traffic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrated 11 services from monolithic docker-compose project into independent stacks,
each with dedicated databases, minimal .env configuration, and bind-mount data volumes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>