valknar fa73750a28
CI / Static checks (push) Successful in 34s
CI / Build and push image (push) Skipped
chore: gitignore local .claude session state
2026-08-17 16:05:34 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00
2026-08-17 14:39:44 +02:00

PulseNode

A self-hosted infrastructure dashboard: system stats, Docker containers, databases, and HTTP/Traefik health, configured entirely through a YAML file and pushed to the browser live over WebSockets. No database of its own - config.yml is the source of truth, metrics are ephemeral.

Features

  • YAML-configured widgets and layout, hot-reloaded - edit config.yml and connected browsers update without a page refresh
  • Secrets via .env, interpolated into config.yml at load time (${VAR} / ${VAR:-default}), never sent to the browser
  • Live updates over WebSocket - each widget subscribes to its own topic; the server only serializes results for widgets someone is actually viewing
  • Widget types: Docker containers, Postgres/Redis instances, system resources, HTTP health checks, Traefik router status, bookmarks, search
  • Optional label-based auto-discovery - containers carrying traefik.enable=true can be turned into dashboard widgets automatically; manual config.yml entries always take precedence
  • CSS-variable theming, light/dark toggle, theme.customCssPath for a fully custom stylesheet
  • Ships as a single Docker image, designed to run non-root with a read-only root filesystem

Quick start (local development)

pnpm install
cp config/.env.example config/.env   # fill in real values
cp .env.example .env                 # only needed for docker compose
pnpm dev

Open http://localhost:3000. pnpm dev runs the custom server (server.ts) via tsx watch, which boots Next.js, the WebSocket server, and the collector scheduler together, and restarts on server-side code changes. Editing config.yml itself does not restart the process - it's picked up live via the config watcher.

Other scripts: pnpm build (production build), pnpm start (run the production build via the same custom server), pnpm lint, pnpm exec tsc --noEmit.

Configuration

config/config.yml

Everything is organized into groups, each a list of widgets:

settings:
  title: PulseNode

theme:
  mode: dark              # dark | light | auto
  variables:
    --pn-accent: "#38bdf8"
  # customCssPath: custom.css   # relative to config/, served at /api/theme/custom-css

discovery:
  docker:
    enabled: false          # set true to auto-discover traefik-labeled containers
    network: falcon_network # only include containers on this docker network

groups:
  - name: Core Infra
    widgets:
      - type: docker
        name: Traefik
        containerName: traefik
        href: https://traefik.example.com
        interval: 10s

Config errors are non-destructive: if config.yml fails validation after a live edit, PulseNode logs the error and keeps serving the last valid config instead of crashing.

config/.env

Secrets and per-deployment values referenced from config.yml via ${VAR} or ${VAR:-default}. Loaded once at startup and on every config reload; never exposed to the client. See config/.env.example.

Widget types

type Required fields Notes
bookmark name, href Static link card. Optional description.
search - Client-side search box. Optional engines (duckduckgo/google/bing, default [duckduckgo]), defaultEngine.
docker name, containerName Status, uptime, CPU/mem, restart count via dockerode. Optional href, showStats (default true), interval (default 5s).
database name, containerName, engine Thin skin over the same Docker collector - engine is postgres or redis, used for icon/label only. interval default 10s.
system - Host CPU/mem/disk/network via systeminformation. Optional name (default System), interval (default 5s).
http name, url HTTP health check with latency and consecutive-failure tracking. Optional method (default GET), timeout (default 5s), interval (default 30s), expect.status (default 200).
traefik apiUrl Router/entrypoint/middleware status from Traefik's own API (--api.dashboard=true must be enabled). Optional name (default Traefik), interval (default 15s).

interval/timeout values are duration strings: 500ms, 5s, 1m, 1h.

Auto-discovery

With discovery.docker.enabled: true, PulseNode scans running containers every 60s (and immediately on any config.yml change) for traefik.enable=true labels, parses the router's Host(...) rule for a link, and adds anything not already listed manually to a synthetic "Discovered" group. discovery.docker.excludeLabels and .network narrow what qualifies; .labelPrefix (default traefik) controls which label namespace is read.

Deployment

cp .env.example .env
cp config/.env.example config/.env
# edit both, then:
docker compose up -d --build
  • .env (repo root) holds TRAEFIK_HOST, NETWORK_NAME, and DOCKER_GID - compose-level values, interpolated into docker-compose.yml's Traefik labels and used to join the container to the docker group. Find your host's docker group id with getent group docker | cut -d: -f3; without it the non-root container gets EACCES on /var/run/docker.sock.
  • config/.env holds values referenced from config.yml (see above).
  • The compose file joins the container to an external falcon_network so collectors can reach services directly by container name (http://umami:3000/...) instead of only through Traefik.
  • /var/run/docker.sock is mounted read-only; /app/config is mounted read-only from ./config.
  • The container runs as a non-root user with a read-only root filesystem, dropped capabilities, and tini as PID 1.
  • /api/health backs the container HEALTHCHECK.

Project structure

config/                config.yml + .env (gitignored) live here
server.ts              custom server: http + WebSocket (/ws) + collector scheduler
app/                   Next.js App Router pages and API routes
components/widgets/    one folder per widget type (Widget.tsx + shared Skeleton/StatusDot)
components/layout/     Dashboard shell, theme toggle, brand mark
lib/config/            schema (zod), loader (parse/interpolate/watch), effective (+ discovery merge)
lib/collectors/        docker, system, http, traefik collectors + the scheduler
lib/discovery/         traefik-label auto-discovery
lib/ws/                WebSocket server (topics) and the browser-side subscription hooks

Known limitations

  • No authentication yet. PulseNode assumes it's reachable only on a private network (e.g. behind Traefik with no public route, or on a VPN). A minimal session-based auth gate is a planned follow-up, not yet built.
  • system widget metrics reflect whatever cgroup/namespace the container runs in - for true host-level stats rather than the container's own view, mount /proc and /sys from the host and run with pid: host (not configured by default).
S
Description
A self-hosted infrastructure dashboard: system stats, Docker containers, databases, and HTTP/Traefik health, configured entirely through a YAML file and pushed to the browser live over WebSockets.
Readme MIT
415 KiB
Languages
TypeScript 93%
CSS 3.5%
Python 1.5%
Dockerfile 1.2%
JavaScript 0.8%