axboard docs Widgets GitLab Releases

Documentation · v0.5.3

axboard

A self-hosted dashboard for your homelab: the front door to every service you run. One Go binary, an embedded React app, and a YAML file you own.

Overview

What it is

axboard is a grid of widgets, most importantly clickable service tiles with health checks, that sits in front of everything you self-host. One binary with the app embedded, no database, no accounts required, LAN-bound by default.

Edit it in the browser or in your editor; both stay in sync. Arrange widgets and manage services, connections, alerts and status pages in the app, or edit config.yaml by hand. Every change hot-reloads.

The interface is in seven languages and follows your system's light or dark theme.

It is deliberately not a metrics collector (health checks answer "is it up"; use Grafana for observability), not a plugin platform, and not multi-tenant.

Getting started

Quick start

axboard ships as one multi-arch image (amd64 + arm64, Raspberry Pi included). You need Docker or Podman with compose.

1 · Make a folder and grab a starter config

mkdir -p axboard/config && cd axboard
curl -o config/config.yaml \
  https://gitlab.com/axel-labs.cloud/axboard/-/raw/main/config.example.yaml

The starter has two dashboards with sample services, weather, feeds and host widgets. Edit it or start from scratch.

2 · Create docker-compose.yml

services:
  axboard:
    image: registry.gitlab.com/axel-labs.cloud/axboard:v0.5.3   # amd64 + arm64; :latest tracks releases
    container_name: axboard
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./config:/etc/axboard:Z            # your config.yaml (bind the directory)
      - axboard-state:/var/lib/axboard:Z   # layouts, uptime history, icons
volumes:
  axboard-state:

3 · Bring it up

docker compose up -d      # or: podman compose up -d

4 · Open it

Go to http://<server-ip>:8080. The status dots fill in as axboard checks each service.

5 · Make it yours

Click Edit in the top bar (⌘/Ctrl E), then an empty cell to add a widget there. Add services in Settings → Services and service credentials in Settings → Connections. Or edit config/config.yaml; the server reloads on save.

i
Host widgets that need more access (real network I/O, host processes, filesystems, Wake-on-LAN, container discovery) are covered by the annotated compose under Deployment. To require a login, see Authentication.

From source (optional)

git clone https://gitlab.com/axel-labs.cloud/axboard.git && cd axboard
make build      # needs Go 1.26 + Node 22
cp config.example.yaml config.yaml
./bin/axboard --config ./config.yaml --state ./state.yaml --addr :8080

Flags: --config (default config.yaml), --state (default state.yaml), --addr (overrides server.bind, default :8080). Subcommand: axboard passwd [username], see Authentication.

Getting started

Configuration

Two files: one is yours, the other is axboard's, so dragging widgets never rewrites the file you edit.

FileOwnerHolds
config.yamlYou, through the app or your editorServices, groups, connections, dashboards and their widgets, the top bar, alerts, status pages. Hot-reloads on change.
state.yamlaxboardGrid layouts, widget settings changed in the app (layered over config.yaml), the last open dashboard, profiles. Don't edit by hand.

On a YAML error the app shows a banner with the line and keeps serving the last good config. Saves from the app reformat config.yaml and drop comments; the raw editor (Settings → config.yaml) keeps them. Theme, font, language and widget style are per browser.

A minimal config

apps:
  - id: jellyfin
    name: Jellyfin
    url: https://jellyfin.lan
    icon: jellyfin              # selfh.st slug, si:<simple-icons slug>, URL, or upload
    group: media
    health: { type: http, url: https://jellyfin.lan/health, interval: 60s }

groups:
  - { id: media, name: Media, color: "#8b5cf6" }

dashboards:
  - id: home
    name: Home
    default: true
    widgets:
      - { id: apps-1, type: apps, title: Media, config: { appIds: [jellyfin] } }

See config.example.yaml in the repo for more, including topBar and dashboard backgrounds. Top-level keys: server, apps, groups, connections, dashboards, topBar, alerts, status_pages, discovery, icon_sources.

Getting started

Settings pages

Account menu (top right) → Settings, or ⌘/Ctrl K. Each page has its own URL under /settings/.

PageWhat it does
ServicesAdd, edit, duplicate and delete services and groups: URL, icon, description, health check, criticality, Wake-on-LAN. Discover lists containers with published ports; Import takes a list of names and URLs. The icon picker can browse extra sources (icon_sources: a public bucket or web folder, or a private S3 / R2 bucket with a read-only key).
ConnectionsCredentials for the services widgets call. See Connections.
AlertsChannels, timing, muted services, maintenance pause. See Alerts.
Status pagesPublic pages with a live preview. See Status pages.
config.yamlThe whole file as YAML, validated before saving, comments kept. ⌘/Ctrl S saves.

Getting started

Connections

A connection is a service axboard calls for your widgets: its address and key, set up once. Widgets name it (config: { connection: <id> }) instead of carrying credentials. The secret stays on the server: the API never returns it, and axboard only sends it to that connection's own host.

Add them in Settings → Connections (with a Test button and where to find the key), or from a widget's settings when you set it up.

connections:
  - id: ha
    kind: homeassistant
    name: Home Assistant              # optional; how widgets list it
    url: http://homeassistant.lan:8123
    secret: "eyJ…"                     # API key, token or password
  - id: pve
    kind: proxmox
    url: https://pve.lan:8006
    username: "root@pam!axboard"       # login name, or a Proxmox / PBS token id
    secret: "…"

Kinds:

homeassistantproxmoxpbssonarrradarrjellyfinembyplexpiholeadguardtechnitiumqbittorrenttransmissionportainerscrutinyimmichnextcloudoverseerrpaperlesstraefiktailscalegotifyntfyunifispeedtesttrackerprometheusconcentus

Widgets set up before connections, with their own baseUrl and key, keep working; their settings card offers to save them as a connection. Deleting a connection asks the widgets that use it to be set up again.

Monitoring

Health checks & monitors

Each service can carry a health block. axboard runs one checker per service and shows a coloured status dot on its tile. Five check types:

httptcppingdnspush
  • http: GET a URL (the service URL when empty) and compare against expect_status (default 200). Optional headers, body_contains, insecure.
  • tcp: dial host:port.
  • ping: ICMP (needs NET_RAW).
  • dns: resolve a host; optional body_contains match on the answers.
  • push: a heartbeat. Your job calls /api/push/<id> (GET or POST) on a schedule; a missed beat marks the service down.

Each check polls on its health.interval; server.default_interval sets the default (60s when unset). Timeout defaults to 5s. retries tolerates N failed checks (shown as degraded) before "down".

axboard keeps uptime on disk and shows 24h / 7d / 30d in the service popover and on status pages. Mark a service critical: true so its outage counts as a major incident (see Status pages).

Monitoring

Alerts

axboard notifies you when a health-checked service goes down or recovers. Set it up in Settings → Alerts, with a Test per channel and Send a test to all, or in the alerts block. Every configured channel fires:

ChannelNeeds
ntfyA topic on ntfy.sh or your own ntfy server; optional access token.
TelegramA bot token (from @BotFather) and a chat id.
EmailAn SMTP server (host, port, credentials, from, to).
WebhookA URL that takes a JSON POST (Discord, Slack, your own).
alerts:
  ntfy: { server: https://ntfy.sh, topic: my-homelab-alerts }
  cert_expiry_days: 14     # 0 = off
  resend_minutes: 60       # repeat while down; 0 = once
  muted: [test-vm]          # service ids: still checked, no alerts
server:
  locale: de                # language of alert messages (en es de fr it pt nl)

Maintenance pauses every alert for a few hours (paused_until); checks and status pages keep running. Certificate expiry is checked on every HTTPS service. Desktop alerts in the account menu turns on browser notifications too.

Monitoring

Status pages

Server-rendered pages that need no JavaScript: the default at /status and any number at /status/<slug>. Each shows the chosen services with a status dot, history strip, 24h/7d/30d uptime, response time and certificate warnings. An SVG badge per service is at /status/badge/<id>.

Build them in Settings → Status pages with a live preview. Per page:

  • Page: title (with its own colour), address (slug), intro text, footer, on/off.
  • Look: theme (auto / dark / light), width (narrow / wide / full), banner style (tint · minimal · strip · outline · solid), "turn red at", language (the visitor's, or fixed).
  • Background: colour, gradient or image with blur and dim.
  • Services: narrow by group, untick services to hide them.
  • Notices: banners for incidents and planned work (info, warning, critical, maintenance).
  • Access: public without sign-in; hide "Powered by axboard".

Severity by criticality

A critical service down turns the banner red. Non-critical outages are amber and turn red once N are down at once (critical_threshold; 0 = never).

all operational degraded / partial critical disruption
i
With sign-in on, status pages need a login by default. Tick Public (public: true) to share one while the dashboard stays private. With sign-in off, every page is public.

Building the board

Editing the board

  • Edit in the top bar (⌘/Ctrl E) turns on edit mode; Done or Esc leaves it.
  • Click an empty cell to add a widget there; the picker has categories, topics and search.
  • Drag a widget by the whole card; resize from its corner. Arrow keys nudge the selected widget.
  • Configure, Duplicate and Remove float above the widget. Remove has an Undo. Settings open in a card beside the widget and apply as you type; every widget can hide its title.
  • A board laid out to about one screen stretches its rows to end on the bottom edge. Below about 900 px wide the board becomes a read-only stack.

The grid is 24 columns wide. Sizes below are columns × rows.

Building the board

Widgets

Every widget fits its tile at every size: number widgets pick their layout from the tile's shape, lists share the height, and feeds show whole rows (set Show to all that fit, or a fixed count). A widget lives in a dashboard's widgets: list; its options go under config:.

Expand a widget for what it does, its settings and a config.yaml example. Fields are optional unless noted.

Productivity

ClockclockTime and date, extra timezones
Grid2×2resizes 2×1 – 4×4 · cols×rows

Local time and date in one of four styles (digital, modern, classic, analog), with up to five extra timezones underneath.

UI24-hour toggle · style · date format (25/12/2026 by default, US, ISO, short, long, browser) · other timezones.
- { id: clock-1, type: clock, title: Clock,
    config: { use24h: true, style: digital, dateFormat: dmy, timezones: [America/New_York, UTC] } }
BookmarksshortcutList of quick links
Grid2×2resizes 1×1 – 10×10 · cols×rows

A list of links to anything, internal or external: an icon and a label per row, from the top. The icon follows the URL's favicon until you pick one.

UILinks (icon · label · address), reorderable, up to 50.
- { id: bm-1, type: shortcut, title: Links,
    config: { shortcuts: [ { label: Router, url: https://192.168.1.1, icon: ubiquiti } ] } }
ChecklistchecklistTodo list with a progress bar
Grid2×3resizes 2×2 – 10×10 · cols×rows

A checklist you tick off in place, with a progress bar. Items are saved with the widget.

UIAdd / edit / check / delete items on the widget.
- { id: todo-1, type: checklist, title: Todo,
    config: { checklist: [ { text: Renew certs, done: false } ] } }
NotesnotesFree-text scratchpad, optional Markdown
Grid3×3resizes 2×1 – 12×12 · cols×rows

A free-text scratchpad, optionally rendered as Markdown (click to edit). URLs auto-link.

UIRender as Markdown · edit on the widget.
- { id: note-1, type: notes, title: Notes, config: { markdown: true, text: "## TODO" } }
CountdowncountdownTime until (or since) a date
Grid3×2resizes 2×1 – 6×3 · cols×rows

A live countdown to, or count-up since, a date and time.

UILabel · target date and time.
- { id: cd-1, type: countdown, config: { target: "2026-12-31T23:59:00", label: New Year } }
CalendarcalendariCal (.ics) agenda or month grid
Grid3×4resizes 2×2 – 12×16 · cols×rows

Upcoming events from an iCal address (fetched by the server), as an agenda or a month grid. Month view needs at least 3×3 and works without a calendar.

UICalendar address · view (upcoming / month) · events · refresh minutes.
- { id: cal-1, type: calendar, title: Calendar,
    config: { url: "https://example.com/calendar.ics", view: agenda, count: 6, refreshMin: 30 } }
ImageimageA static image or banner
Grid3×2resizes 1×1 – 12×12 · cols×rows

An image by URL, optionally a link.

UIImage address · fit (fill / whole image) · link.
- { id: img-1, type: image, config: { url: https://example.com/logo.png, fit: contain } }
Section labelsectionFrameless heading to divide the board
Grid4×1resizes 2×1 – 24×1 · cols×rows

A frameless heading that divides a busy board.

UIHeading · alignment (left / centre).
- { id: sec-1, type: section, config: { text: Infrastructure, align: left } }
PomodoropomodoroFocus / break timer
Grid2×2resizes 2×2 – 4×3 · cols×rows

A focus/break timer with start, pause and reset.

UIFocus minutes · break minutes.
- { id: pom-1, type: pomodoro, config: { work: 25, break: 5 } }
Tabbed grouptabgroupSeveral widgets in one card, switched by tabs
Grid3×3resizes 2×2 – 16×16 · cols×rows

Stacks several widgets in one card, switched by tabs. Each tab is a full widget with its own config, picked and set up inline.

UIAdd / remove / reorder tabs · each tab's title, type and settings.
- { id: tabs-1, type: tabgroup, title: Infra,
    config: { tabs: [ { title: CPU, type: gauge, config: { metric: cpu } },
                      { title: Net, type: netgraph } ] } }

System & network — need host access; see the compose grants

Host statshostCPU / RAM / disk / network / load / uptime
Grid3×3resizes 2×2 – 8×8 · cols×rows

CPU, memory, disk, disk and network I/O, swap, load and uptime for the machine axboard runs on. Rows that do not fit the tile are dropped. Real network I/O needs host networking.

UIShow the load average.
- { id: host-1, type: host, title: Host, config: { showLoad: true } }
Resource gaugegaugeOne or two host metrics as ring / bar / sparkline
Grid2×2resizes 1×1 – 6×6 · cols×rows

One host metric (or two, splitting the tile) as a ring, bar or sparkline.

UIMetric and second metric (cpu / ram / disk / swap) · style · time window · colour (by level or one colour) · compact · glow · track · label.
- { id: cpu-1, type: gauge, title: CPU,
    config: { metric: cpu, style: ring, colorScale: levels, warn: 70, crit: 90 } }
Per-core CPUpercpuOne bar per logical core
Grid4×2resizes 2×2 – 12×6 · cols×rows

Live load of every logical CPU core as a strip of bars.

UIColour (by level with warn / crit breakpoints, or one colour).
- { id: cores, type: percpu, title: Cores, config: { colorScale: levels, warn: 70, crit: 90 } }
TemperaturestempsHardware sensors from hwmon
Grid3×3resizes 2×2 – 8×8 · cols×rows

Temperature sensors (CPU, NVMe, chipset) from /sys/class/hwmon. Pick which to show and rename them.

UIPick sensors · rename them · colour + warn / crit (°C).
- { id: temps-1, type: temps, title: Temps,
    config: { warn: 70, crit: 85, names: { "Package id 0": CPU } } }
Top processestopprocHighest CPU / memory processes
Grid3×3resizes 2×2 – 8×10 · cols×rows

The busiest processes by CPU or memory. Host processes need pid: host on the container.

UISort by CPU or memory · at most N rows.
- { id: top-1, type: topproc, title: Top, config: { count: 8, sort: cpu } }
FilesystemsdisksUsage bar per mounted filesystem
Grid3×3resizes 2×2 – 10×8 · cols×rows

A usage bar for every real filesystem you pick. The host's mounts need the read-only /:/host bind.

UIPick mounts · colour + warn / crit.
- { id: fs-1, type: disks, title: Disks, config: { mounts: [/, /mnt/tank] } }
Network graphnetgraphLive download / upload chart
Grid4×2resizes 3×1 – 12×6 · cols×rows

Live download and upload throughput as an area chart. Real host traffic needs host networking.

UITime window (1m / 5m / 15m / 1h) · style (from the bottom / mirrored) · download and upload colours · fixed scale (Mbit/s).
- { id: net-1, type: netgraph, title: Network, config: { style: mirror, window: 5m } }
ContainerscontainersDocker / Podman containers, one per row
Grid3×4resizes 2×2 – 6×12 · cols×rows

Containers from the mounted socket, one per row with its image, ports and volumes, optionally CPU and memory. Restart one from its row.

UIOnly names containing · running only · show CPU and memory.
- { id: ctr-1, type: containers, title: Containers,
    config: { runningOnly: true, stats: true, filter: media } }
Battery / UPSbatteryBatteries and UPS units
Grid3×2resizes 2×1 – 6×5 · cols×rows

Charge and status of batteries and UPS units under /sys/class/power_supply. No settings.

- { id: bat-1, type: battery, title: UPS }
Uptime monitormonitorUp / down and latency for a list of URLs
Grid3×4resizes 2×1 – 6×12 · cols×rows

Up/down and latency for an ad-hoc list of URLs, separate from your services.

UIAddresses to watch (name · address) · check every N seconds.
- { id: mon-1, type: monitor, title: Endpoints,
    config: { refreshSec: 30, targets: [ { name: Router, url: https://192.168.1.1 } ] } }
Public IPpublicipWAN IP, location and provider, VPN indicator
Grid3×2resizes 2×1 – 6×4 · cols×rows

Your WAN IP with location and provider. Shows “VPN on” when the provider's name contains what you set.

UIVPN provider name.
- { id: ip-1, type: publicip, title: WAN, config: { expectIsp: Mullvad } }
Speed testspeedtestDownload / upload / latency, run in the browser
Grid3×2resizes 2×2 – 6×4 · cols×rows

An internet speed test run in your browser against Cloudflare (about 50 MB per run). It measures the viewing device's connection.

UIRun when the dashboard opens · download and upload colours.
- { id: spd-1, type: speedtest, title: Speed, config: { auto: false } }
Speedtest TrackerspeedtesttrackerLatest result and history from Speedtest Tracker
Grid3×2resizes 2×2 – 6×8 · cols×rows

Speedtest Tracker: latest download, upload and ping, plus recent history.

UIConnection (Speedtest Tracker).
- { id: stt-1, type: speedtesttracker, title: Speedtest, config: { connection: speedtest } }
UniFiunifiConnected clients and live WAN throughput
Grid3×2resizes 2×1 – 6×4 · cols×rows

A UniFi console: connected clients and live WAN download and upload.

UIConnection (UniFi: a local admin account) · site · hide the public IP.
- { id: unifi-1, type: unifi, title: UniFi, config: { connection: unifi, site: default } }
Wake-on-LANwolWake-on-LAN buttons
Grid3×2resizes 2×1 – 8×6 · cols×rows

Buttons that send Wake-on-LAN magic packets. Needs host networking to reach the LAN broadcast. For a wake button on one service, give the service a wol block and use the App widget.

UIDevices (name · MAC address · optional broadcast).
- { id: wol-1, type: wol, title: Wake,
    config: { targets: [ { name: NAS, mac: "AA:BB:CC:DD:EE:FF", broadcast: 192.168.1.255 } ] } }

Services & self-hosted apps

Panels for the tools you run. Most call the tool's API through a connection: config: { connection: <id> }.

Apps gridappsIcon grid of picked services with status dots
Grid2×2resizes 1×1 – 24×24 · cols×rows

Clickable tiles for the services you pick, each with a live status dot, sized to show every one in the tile. Optionally grouped under folding headers. Right-click a tile to check it now.

UIPick and order services · names under the icons · group by category · open in the same tab · open inside the board (in a panel, for services that allow embedding).
- { id: apps-1, type: apps, title: Media,
    config: { appIds: [jellyfin, sonarr, radarr], showNames: true, grouped: false } }
AppappOne service as a tile, layout by size
Grid1×1resizes 1×1 – 3×2 · cols×rows

One service as a tile that grows from an icon to a card with name, description, status, response time and last check. Shows a Wake-on-LAN button while the service is down if it has a wol block.

UIService · description override · show description / status / response time / last checked · open in this tab.
- { id: app-nas, type: app, title: NAS,
    config: { appId: nas, showResponseTime: true, showLastChecked: true } }
Status summarystatusEvery service: history bars down to coloured squares
Grid5×4resizes 2×1 – 12×14 · cols×rows

Every health-checked service, from full uptime-history bars at large sizes down to coloured squares, optionally broken down by group.

UIHistory bars · by group · legend · only these groups · only these services.
- { id: sum-1, type: status, title: Overview,
    config: { byGroup: true, bars: true, groups: [infra, media] } }
ProxmoxproxmoxPVE nodes, VMs / LXC and storage
Grid3×4resizes 2×2 – 8×10 · cols×rows

Proxmox VE nodes, guests and storage with live CPU, RAM and disk, a capacity summary, warning borders and backup age. Several servers sit side by side; click a guest to open its console.

UIServers (a connection + label each) · capacity summary · VMs and containers · storage · last backup · compact · CPU and memory as bars / % / both · warning thresholds.
- { id: pve-1, type: proxmox, title: Proxmox,
    config: { servers: [ { connection: pve, name: pve-01 } ], showBackups: true } }
Proxmox Backup ServerpbsDatastore usage per store
Grid3×2resizes 2×2 – 6×8 · cols×rows

Datastore usage (used / total) per store on a Proxmox Backup Server.

UIConnection (Proxmox Backup Server: token id + secret).
- { id: pbs-1, type: pbs, title: PBS, config: { connection: pbs } }
Jellyfin / PlexmediaJellyfin / Emby / Plex sessions and libraries
Grid3×3resizes 2×2 – 6×8 · cols×rows

Jellyfin, Emby or Plex: now-playing sessions with progress, plus library counts.

UIConnection (Jellyfin, Emby or Plex).
- { id: jf-1, type: media, title: Jellyfin, config: { connection: jellyfin } }
Sonarr / RadarrarrDownload queue and upcoming calendar
Grid3×3resizes 2×2 – 8×8 · cols×rows

The download queue and upcoming releases from Sonarr or Radarr.

UIConnection (Sonarr or Radarr) · upcoming days.
- { id: sonarr-1, type: arr, title: Sonarr, config: { connection: sonarr, days: 7 } }
Overseerr / JellyseerrseerrPending / approved / available requests
Grid3×2resizes 2×2 – 6×6 · cols×rows

Overseerr / Jellyseerr: pending, approved and available media requests.

UIConnection (Overseerr / Jellyseerr).
- { id: seerr-1, type: seerr, title: Requests, config: { connection: overseerr } }
TransmissiontransmissionTorrents: progress, rates, ratio
Grid3×3resizes 2×2 – 8×8 · cols×rows

Transmission torrents with progress, up/down rates and ratio, plus live totals.

UIConnection (Transmission) · torrents shown.
- { id: tr-1, type: transmission, title: Transmission, config: { connection: transmission, max: 8 } }
qBittorrentqbittorrentTorrents: progress, rates, ratio
Grid3×3resizes 2×2 – 8×8 · cols×rows

qBittorrent torrents with progress, up/down rates and ratio, plus live totals.

UIConnection (qBittorrent) · torrents shown.
- { id: qb-1, type: qbittorrent, title: qBittorrent, config: { connection: qbittorrent, max: 8 } }
ImmichimmichPhoto / video counts and storage
Grid3×2resizes 2×2 – 6×4 · cols×rows

Immich: photo and video counts and storage used.

UIConnection (Immich).
- { id: immich-1, type: immich, title: Immich, config: { connection: immich } }
NextcloudnextcloudFree space, users, files, shares
Grid3×2resizes 2×2 – 6×6 · cols×rows

Nextcloud: free space, users, files and shares from the serverinfo API.

UIConnection (Nextcloud: an NC-Token, or a username + app password).
- { id: nc-1, type: nextcloud, title: Nextcloud, config: { connection: nextcloud } }
Paperless-ngxpaperlessTotal documents and inbox
Grid2×2resizes 2×1 – 6×4 · cols×rows

Paperless-ngx: total documents and how many wait in the inbox.

UIConnection (Paperless-ngx).
- { id: ppl-1, type: paperless, title: Paperless, config: { connection: paperless } }
PortainerportainerContainers / images / volumes / stacks
Grid3×2resizes 2×2 – 6×6 · cols×rows

Portainer: running, stopped and total containers, images, volumes and stacks for an environment.

UIConnection (Portainer) · environment id.
- { id: ptn-1, type: portainer, title: Portainer, config: { connection: portainer, env: 1 } }
ScrutinyscrutinyDisk SMART health
Grid3×3resizes 2×2 – 6×8 · cols×rows

Scrutiny: passed / failed / unknown drives, with temperature per drive.

UIConnection (Scrutiny).
- { id: scr-1, type: scrutiny, title: SMART, config: { connection: scrutiny } }
DNS SinkholednsPi-hole / AdGuard / Technitium stats
Grid3×3resizes 2×2 – 6×8 · cols×rows

Pi-hole, AdGuard Home or Technitium: queries, block rate, blocklist size and top blocked domains.

UIConnection (Pi-hole, AdGuard Home or Technitium).
- { id: dns-1, type: dns, title: Pi-hole, config: { connection: pihole } }
TraefiktraefikRouter / service / middleware counts
Grid3×2resizes 2×1 – 6×4 · cols×rows

Traefik: router, service and middleware counts with error and warning flags.

UIConnection (Traefik API).
- { id: trf-1, type: traefik, title: Traefik, config: { connection: traefik } }
PrometheusprometheusFiring and pending alerts
Grid3×2resizes 2×1 – 6×8 · cols×rows

Prometheus: firing and pending alerts, ranked by severity.

UIConnection (Prometheus).
- { id: prom-1, type: prometheus, title: Alerts, config: { connection: prometheus } }
TailscaletailscaleDevices, online, pending updates
Grid3×3resizes 2×2 – 6×8 · cols×rows

Your tailnet: device count, how many are online, and pending client updates.

UIConnection (Tailscale API key) · tailnet.
- { id: ts-1, type: tailscale, title: Tailscale, config: { connection: tailscale } }
NotificationsnotifyGotify or ntfy messages
Grid3×3resizes 2×2 – 12×16 · cols×rows

Recent messages from Gotify (with app and client counts) or an ntfy topic, newest first.

UIConnection (Gotify or ntfy) · topic (ntfy).
- { id: ntfy-1, type: notify, title: Alerts, config: { connection: ntfy, topic: my-alerts } }
CameracameraFrigate camera or any MJPEG / JPEG stream
Grid4×3resizes 2×2 – 12×10 · cols×rows

A live camera: a Frigate camera by name, or any MJPEG / JPEG stream address.

UISource (Frigate / stream address) · Frigate address and camera, or stream address · live or snapshots + interval · fit · title colour · link.
- { id: cam-1, type: camera, title: Door,
    config: { source: frigate, baseUrl: http://frigate.lan:5000, camera: door } }
Grafana panelgrafanaA Grafana panel or dashboard
Grid5×4resizes 3×2 – 12×12 · cols×rows

A Grafana panel (Share → Embed, d-solo/…) or dashboard in a frame. Grafana needs allow_embedding = true.

UIPanel address · refresh every N seconds · match the dashboard's theme · hide the title bar · kiosk mode.
- { id: graf-1, type: grafana, title: Latency,
    config: { url: "https://grafana.lan/d-solo/abc/net?panelId=2", refreshSec: 30, kiosk: true } }
axwaveconcentusNow playing from an axwave server
Grid2×3resizes 2×3 – 8×6 · cols×rows

Album art, title, artist and progress from an axwave server's active session, with play/pause and skip between the art and the track. Saved as type concentus (its former name).

UIConnection (axwave: address, username, password).
- { id: np-1, type: concentus, title: Now playing, config: { connection: axwave } }

Home Assistant

Every HA widget uses a homeassistant connection (address + long-lived access token). Entities are picked from a searchable live list. Controls update at once and reconcile with HA a moment later.

Home AssistanthomeassistantOverview: people home, lights on, pinned entities
Grid3×1resizes 2×1 – 6×8 · cols×rows

How many people are home and lights and switches on, plus entities you pin, with toggles and run buttons for them.

UIConnection · pinned entities.
- { id: ha-1, type: homeassistant, title: Home,
    config: { connection: ha, entities: [switch.coffee, script.goodnight] } }
HA LightshalightsToggle and dim several lights
Grid3×1resizes 2×1 – 6×8 · cols×rows

On/off and brightness for the lights (and switches) you pick. Sliders take the light's colour.

UIConnection · lights.
- { id: lights-1, type: halights, title: Lights,
    config: { connection: ha, entities: [light.kitchen, light.hall] } }
HA LighthalightOne light: toggle, brightness, warmth, hue
Grid2×1resizes 2×1 – 4×4 · cols×rows

One light: toggle and brightness, warmth over the light's own kelvin range, and a hue bar for RGB lights.

UIConnection · light.
- { id: light-1, type: halight, config: { connection: ha, entity: light.desk } }
HA FanshafansSeveral fans, stepped speed
Grid3×2resizes 3×1 – 6×8 · cols×rows

Several fans, each an Off / 1 / 2 / 3 speed slider.

UIConnection · fans.
- { id: fans-1, type: hafans, title: Fans,
    config: { connection: ha, entities: [fan.bedroom, fan.office] } }
HA FanhafanOne fan in one row
Grid2×1resizes 2×1 – 4×1 · cols×rows

One fan as an Off / 1 / 2 / 3 speed slider in a single row.

UIConnection · fan.
- { id: fan-1, type: hafan, config: { connection: ha, entity: fan.bedroom } }
HA ThermostathaclimateCurrent temperature, target, HVAC mode
Grid3×1resizes 2×1 – 5×4 · cols×rows

A thermostat: current temperature, target +/- and HVAC mode.

UIConnection · climate entity.
- { id: therm-1, type: haclimate, config: { connection: ha, entity: climate.living_room } }
HA CovershacoverOpen / stop / close several covers
Grid3×1resizes 2×1 – 6×8 · cols×rows

Open / stop / close blinds, garage doors and curtains, with position.

UIConnection · covers.
- { id: covers-1, type: hacover, title: Blinds,
    config: { connection: ha, entities: [cover.living, cover.garage] } }
HA CoverhacoveroneOne cover: open / stop / close
Grid2×1resizes 2×1 – 4×3 · cols×rows

One blind, garage door or curtain: open / stop / close.

UIConnection · cover.
- { id: cover-1, type: hacoverone, config: { connection: ha, entity: cover.garage } }
HA MediahamediaNow playing, transport, volume
Grid3×2resizes 2×2 – 5×4 · cols×rows

A media player: now playing, previous / play-pause / next and volume.

UIConnection · media player.
- { id: hmedia-1, type: hamedia, config: { connection: ha, entity: media_player.living_room } }
HA LockhalockLock / unlock a door
Grid2×1resizes 2×1 – 4×3 · cols×rows

Lock or unlock a door, with its state.

UIConnection · lock.
- { id: lock-1, type: halock, config: { connection: ha, entity: lock.front_door } }
HA VacuumhavacuumStart / pause / dock, state, battery
Grid3×2resizes 2×2 – 4×4 · cols×rows

A vacuum: start / pause / dock, with state and battery.

UIConnection · vacuum.
- { id: vac-1, type: havacuum, config: { connection: ha, entity: vacuum.robot } }
HA SceneshascenesOne-tap scenes / scripts / automations
Grid3×1resizes 2×1 – 6×8 · cols×rows

One-tap buttons for scenes, scripts, automations and buttons; the right service is called per domain.

UIConnection · scenes, scripts, buttons.
- { id: scenes-1, type: hascenes, title: Scenes,
    config: { connection: ha, entities: [scene.movie, script.goodnight] } }
HA SensorshasensorsValues for the sensors you pick
Grid3×1resizes 2×1 – 6×8 · cols×rows

Read-only rows for any sensor / binary_sensor; binary sensors show a state dot.

UIConnection · sensors.
- { id: sensors-1, type: hasensors, title: Sensors,
    config: { connection: ha, entities: [sensor.living_temp, binary_sensor.front_door] } }
HA SensorhasensorOne sensor as a big value
Grid2×1resizes 2×1 – 4×3 · cols×rows

One sensor as a big value (temperature, humidity, a door, …).

UIConnection · sensor.
- { id: sensor-1, type: hasensor, config: { connection: ha, entity: sensor.outdoor_temp } }
HA PowerhapowerLive draw per power sensor, plus total
Grid3×1resizes 2×1 – 6×8 · cols×rows

Live draw for the power sensors you pick, each a bar scaled to max, with the total in the header.

UIConnection · power sensors · full bar at (W).
- { id: power-1, type: hapower, title: Power,
    config: { connection: ha, entities: [sensor.house_power], max: 3000 } }
HA PresencehapresenceWho's home, with zones
Grid3×2resizes 2×1 – 5×8 · cols×rows

Who's home, with avatars and zones: every person.* Home Assistant tracks. No entity picker.

UIConnection.
- { id: presence-1, type: hapresence, title: Presence, config: { connection: ha } }
HA BatterieshabatteryEvery device battery, lowest first
Grid3×2resizes 2×1 – 6×8 · cols×rows

Every battery sensor, lowest first, with low-battery flags. Found automatically.

UIConnection.
- { id: batteries-1, type: habattery, title: Batteries, config: { connection: ha } }

Web & feeds

WeatherweatherCurrent conditions and forecast (Open-Meteo)
Grid4×4resizes 2×2 – 6×4 · cols×rows

Current conditions and forecast from Open-Meteo (no API key). Grows from compact to detailed to an hourly forecast.

UICity (searched, stored as name + lat/lon) · units (°C / °F) · hourly forecast.
- { id: wx-1, type: weather, title: Weather,
    config: { city: Lisbon, lat: 38.72, lon: -9.14, units: celsius, hourly: true } }
SunsunSunrise / sunset / UV / daylight arc
Grid3×2resizes 2×2 – 5×4 · cols×rows

Sunrise, sunset, UV index and a daylight arc for a city.

UICity.
- { id: sun-1, type: sun, config: { city: Lisbon, lat: 38.72, lon: -9.14 } }
MarketsmarketsCrypto and stocks with charts
Grid3×3resizes 2×2 – 8×12 · cols×rows

Prices for crypto (CoinGecko ids) and stocks / ETFs (Yahoo tickers). Taller tiles switch from a price list to one chart per symbol.

UICoins and tickers · chart period (1d / 1w / 1m / 3m / 1y / 5y) · coin currency.
- { id: mkt-1, type: markets, title: Markets,
    config: { ids: [bitcoin, ethereum], stocks: [AAPL, VWCE.DE], vs: eur, period: 1m } }
ReleasesreleasesLatest GitHub / GitLab releases
Grid3×4resizes 2×2 – 6×10 · cols×rows

Latest releases of the projects you watch, on GitHub or GitLab.

UIProjects as gh:owner/repo or gl:group/project · optional API tokens (kept in this browser).
- { id: rel-1, type: releases, title: Releases,
    config: { repos: [ "gh:go-chi/chi", "gl:gitlab-org/gitlab" ] } }
RSS feedrssRSS / Atom feed
Grid3×4resizes 2×2 – 12×16 · cols×rows

Recent items from an RSS or Atom feed, fetched by the server.

UIFeed address · show (all that fit, 5, 10, 20, 30, 50) · refresh minutes.
- { id: rss-1, type: rss, title: News, config: { url: https://example.com/feed.xml, count: 10 } }
RedditredditPosts from a subreddit
Grid3×4resizes 2×2 – 12×16 · cols×rows

Posts from a subreddit with score, comments and age. Reddit blocks anonymous access from most networks: add a read-only script app's id and secret.

UISubreddit · sort (hot / new / top / rising) · app id and secret · show.
- { id: rdt-1, type: reddit, title: r/selfhosted,
    config: { subreddit: selfhosted, sort: hot, clientId: "…", clientSecret: "…" } }
Hacker NewshackernewsFront page, Ask HN or Show HN
Grid3×4resizes 2×2 – 12×16 · cols×rows

Hacker News front page, Ask HN or Show HN, with points, comments and age.

UIFeed (front page / Ask HN / Show HN) · show.
- { id: hn-1, type: hackernews, title: HN, config: { kind: front_page, max: 10 } }
LobsterslobstersLobste.rs hottest stories
Grid3×4resizes 2×2 – 12×16 · cols×rows

Lobste.rs hottest stories with score, comments, tags and age.

UIShow.
- { id: lob-1, type: lobsters, title: Lobsters }
YouTube channelyoutubeRecent uploads with thumbnails
Grid3×4resizes 2×2 – 12×16 · cols×rows

Recent uploads from a YouTube channel, via its RSS feed (no API key).

UIChannel id (UC…) · show (all that fit, 5, 10, 15).
- { id: yt-1, type: youtube, title: Uploads, config: { channelId: "UC…" } }
XKCDxkcdThe latest XKCD comic
Grid3×4resizes 2×3 – 6×8 · cols×rows

The latest XKCD comic, alt text on hover. No settings.

- { id: xkcd-1, type: xkcd }
Custom APIcustomapiAny JSON endpoint as labelled values
Grid3×2resizes 2×2 – 8×8 · cols×rows

Fetches a JSON endpoint (through the server) and shows chosen values by path (data.queue, items[0].name).

UIJSON address · values (label + path). refreshMin (default 5) is YAML only.
- { id: api-1, type: customapi, title: Queue,
    config: { url: "http://sonarr.lan:8989/api/v3/queue?apikey=…", refreshMin: 5,
              fields: [ { label: Queue, path: totalRecords } ] } }
Template (advanced)templateJSON from APIs, rendered by your own JS
Grid3×4resizes 2×2 – 8×12 · cols×rows

Fetches one or more JSON APIs and renders them with a short JavaScript template that returns HTML, run in your browser. Helpers h.esc, h.num, h.relTime, h.get, h.bar; classes row, muted, big, label, grid.

UIRequests (address · name · GET / POST · headers · body) · template · check every N seconds.
- { id: tpl-1, type: template, title: Custom,
    config: { requests: [ { url: "https://api.example.com/x" } ],
              template: "return `<div class='big'>${h.esc(data.value)}</div>`" } }
EmbediframeAny URL in a frame
Grid4×4resizes 2×2 – 24×24 · cols×rows

Any URL in a frame. Sites that send X-Frame-Options / frame-ancestors stay blank; the address bar has an open-in-new-tab link.

UIPage address · reload (never … every hour) · hide the address bar.
- { id: emb-1, type: iframe, title: Wiki, config: { url: https://wiki.lan, refreshSec: 0 } }
i
Wake-on-LAN per service. Give a service a wol block (mac + optional broadcast) and the App widget shows a wake button while that service is down.

Building the board

Appearance

Account menu → Appearance, a live-preview panel with four tabs. Each tab has Reset to defaults.

TabSavedOptions
Generalthis browserTheme (dark / light / system) · palette (neutral / blue) · accent (violet, blue, teal, amber, rose, slate, stone) · font (Inter, System, Rounded, Humanist, Serif, Mono) · language.
This dashboardconfig.yamlBackground: colour, gradient (presets, or a builder: linear / radial, direction, colours, or CSS) or image (URL or upload; fill / fit / tile, blur, dim), plus opacity. Density: compact / cozy / spacious.
Top barconfig.yaml, all dashboardsBar style (glass / solid / contrast / transparent) · branding (logo, name, browser tab icon) · in the bar: search field, services up / total, clock, weather · bar bookmarks (services as icon launchers).
Widgetsthis browserOpacity, blur, border · shadow (off / soft / medium / strong, optional accent tint) · three shapes, each from square to rounded: Corners (widgets, dialogs, panels: square / soft / default / round), Bars (usage bars, progress, sliders: segmented / square / rounded / pill), Buttons (buttons, fields, menus, toggles: square / soft / default / round) · custom CSS (unsupported; may break on upgrades).

Usage-bar colours are set per widget (gauges, cores, temperatures, filesystems): by level (warning and critical breakpoints, each level's colour editable, optional blending) or one colour.

topBar:
  barStyle: default            # default (glass) | solid | contrast | transparent
  header: { clock: true, appsUp: true, brandText: Homelab, links: [jellyfin] }
dashboards:
  - id: home
    name: Home
    density: cozy               # compact | cozy | spacious
    background: { type: gradient, gradient: "linear-gradient(135deg, #0f172a, #0e7490)" }

The built-in themes and the custom-theme creator of v0.3 are gone; a dashboard's old accent and theme fields are ignored.

Building the board

Languages

English, Spanish, German, French, Italian, Portuguese and Dutch (en es de fr it pt nl).

  • The app follows the browser's language until you pick one in Appearance → General (saved per browser).
  • Alert messages use server.locale (Settings → Alerts → language of alert messages). Empty is English.
  • Status pages use the page's locale if set, else the visitor's browser, else server.locale.

Content from your services is shown as it comes.

Building the board

Multiple dashboards & deep links

The top bar has Home (the default: true dashboard), a tab per favourite (favorite: true, reorder by dragging) and a dashboard switcher. Home lives at /, every other dashboard at /<slug> from its name: Dev opens at /dev. Back and forward work.

The switcher creates dashboards (blank or from a template: Overview, Monitoring, Personal), sets name and icon, makes one Home, favourites it, exports, copies to share, imports, enters kiosk mode and deletes. Up to 12 dashboards. Back up everything downloads config and state as JSON; Restore from backup puts it back.

Kiosk mode hides all chrome and locks the board for a wall display. ?kiosk=1 in the URL opens it; Esc leaves.

Building the board

Keyboard shortcuts

Press ? for the cheat sheet.

KeyAction
⌘/Ctrl KSearch: services, bookmarks, dashboards, every setting and appearance option, web search (bangs: g …, !yt …, gh …, w …)
?Show the shortcut list
EscClose a panel or leave edit mode
⌘/Ctrl EEdit mode on / off
⌘/Ctrl Z / ⌘/Ctrl ⇧ ZUndo / redo
Del / ⌫Remove the selected widget (edit mode)
Arrow keysMove the selected widget (edit mode)
⌘/Ctrl 1…9Go to dashboard N

Operating

Authentication

axboard is open by default: LAN-bound, no login, meant to sit behind a reverse proxy. An opt-in built-in login is there for when there is no proxy. It stays off until you add a user.

1 · Generate a password hash

# argon2id; prompts without echo, or reads stdin when piped (8+ characters)
docker exec -it axboard axboard passwd admin
echo 'your-password' | ./bin/axboard passwd admin

2 · Paste the snippet into config.yaml

server:
  bind: 0.0.0.0:8080
  auth:
    session_ttl: 168h        # optional, default 7 days
    users:
      - username: admin
        password_hash: "$argon2id$v=19$m=65536,t=3,p=4$…"

With a user configured, the app shows a sign-in screen and /api needs a session cookie, except heartbeat ingest (/api/push/*) and /api/version. /healthz and /metrics stay open. Sign out from the account menu.

Hashes are never sent to the browser and the app's structured saves can't change them; manage users in config.yaml with axboard passwd. The session key is generated into session.key next to state.yaml; deleting it signs everyone out.

i
The built-in login is a lightweight fallback, not an identity provider. For WAN exposure, prefer a reverse proxy with forward-auth (Authentik, Authelia, oauth2-proxy). Either way, bind server.bind to a LAN address.

Operating

Deployment

One multi-arch (amd64 / arm64) image: registry.gitlab.com/axel-labs.cloud/axboard. Tags: latest and versions like v0.5.0. Pull it, add a compose file and a config.yaml, bring it up.

1 · Bare minimum

The image, a port and two volumes, bridged. CPU, memory and disk widgets still report the host; real host network I/O and Wake-on-LAN need the grants in the next file. (docker-compose.min.yml)

services:
  axboard:
    image: registry.gitlab.com/axel-labs.cloud/axboard:v0.5.3
    container_name: axboard
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./config:/etc/axboard:Z       # your config.yaml (bind the directory)
      - axboard-state:/var/lib/axboard:Z

volumes:
  axboard-state:

2 · Full (all host access)

The same service with every optional grant, each labelled with what it unlocks. Remove any you don't want. (docker-compose.example.yml)

services:
  axboard:
    image: registry.gitlab.com/axel-labs.cloud/axboard:v0.5.3      # a release tag; :latest tracks releases
    container_name: axboard
    restart: unless-stopped

    # --- networking -------------------------------------------------------
    # Host networking lets the Host stats / Resource gauge / Network graph
    # widgets read REAL host network I/O, and lets Wake-on-LAN reach the LAN
    # broadcast. With it, the app binds the host port directly (no ports:).
    network_mode: host
    # Prefer bridged instead? Drop network_mode above and map the port —
    # CPU/RAM/disk still report the host, but network I/O shows only the
    # container's own traffic:
    # ports:
    #   - "8080:8080"

    # --- optional host access (remove any you don't want) -----------------
    pid: host                        # Top processes widget sees host processes
    cap_add:
      - NET_RAW                       # ICMP "ping" health-check type
    security_opt:
      - label=disable                 # SELinux: reach the container socket

    volumes:
      # Required: your config dir (bind the DIRECTORY, not the file, so hot
      # reload sees create/rename events) + a named volume for machine state.
      - ./config:/etc/axboard:Z
      - axboard-state:/var/lib/axboard:Z
      # Optional: read-only container socket → Containers widget and service
      # discovery. (Docker: /var/run/docker.sock)
      - ${XDG_RUNTIME_DIR:-/run/user/1000}/podman/podman.sock:/var/run/docker.sock:ro
      # Optional: read-only host root → Filesystems widget sees real mounts.
      - /:/host:ro,rslave

    environment:
      - AXBOARD_HOST_ROOT=/host        # pair with the /host bind above

    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:8080/healthz || exit 1"]
      interval: 30s
      timeout: 3s
      retries: 3

volumes:
  axboard-state:

3 · Your config

mkdir -p config
curl -o config/config.yaml \
  https://gitlab.com/axel-labs.cloud/axboard/-/raw/main/config.example.yaml   # or write your own
docker compose up -d

Open http://<host>:8080. The server reloads config/config.yaml on save. Layouts, uptime history, uploaded icons and the session key live in the axboard-state volume.

Updating

docker compose pull && docker compose up -d

Coming from an older release? Images before v0.5.0 were published elsewhere: change the image: line.

Backup & restore

Back up both:

  • config.yaml: services, groups, connections (with their secrets), dashboards, widgets, alerts, status pages. The one that matters; keep it in version control if you like (mind the secrets).
  • the axboard-state volume: layouts and widget settings (state.yaml), uptime history, uploaded icons, the session key.
# back up
cp config/config.yaml config.yaml.bak
podman volume export axboard-state -o axboard-state.tar
# docker equivalent for the volume:
# docker run --rm -v axboard-state:/v -v "$PWD":/b alpine tar czf /b/state.tar -C /v .

# restore: put config.yaml back, re-import the volume, restart
podman volume import axboard-state axboard-state.tar
docker compose up -d
i
No shell? The dashboard switcher's Back up everything downloads config and state as JSON, without connection secrets or password hashes. Settings → config.yaml shows the whole file to copy or paste back. Appearance settings kept per browser are not in either.
!
Exposure. axboard is open by default: bind it to a LAN address. Anyone who can reach the port can read and change the config unless you turn on the built-in login or put a forward-auth proxy in front.

Operating

Development

make dev-go     # Go API on :8080
make dev-web    # Vite dev server on :5173, proxies /api/* → :8080 (HMR)

make build bundles the app into the binary (//go:embed). go test ./... runs the Go tests. @axel-labs/ui installs from its public registry, so no token is needed.

Stack

Go 1.26 · chi · yaml.v3 · fsnotify. React 19 · Vite · TypeScript · Tailwind 4 · TanStack Query · react-grid-layout · i18next · the Axel Labs design system (@axel-labs/ui). No codegen: a small hand-written client talks to a small HTTP API.