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.
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.
| File | Owner | Holds |
|---|---|---|
config.yaml | You, through the app or your editor | Services, groups, connections, dashboards and their widgets, the top bar, alerts, status pages. Hot-reloads on change. |
state.yaml | axboard | Grid 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/.
| Page | What it does |
|---|---|
| Services | Add, 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). |
| Connections | Credentials for the services widgets call. See Connections. |
| Alerts | Channels, timing, muted services, maintenance pause. See Alerts. |
| Status pages | Public pages with a live preview. See Status pages. |
| config.yaml | The 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:
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:
- http: GET a URL (the service URL when empty) and compare against
expect_status(default 200). Optionalheaders,body_contains,insecure. - tcp: dial
host:port. - ping: ICMP (needs
NET_RAW). - dns: resolve a host; optional
body_containsmatch 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:
| Channel | Needs |
|---|---|
| ntfy | A topic on ntfy.sh or your own ntfy server; optional access token. |
| Telegram | A bot token (from @BotFather) and a chat id. |
| An SMTP server (host, port, credentials, from, to). | |
| Webhook | A 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).
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
Local time and date in one of four styles (digital, modern, classic, analog), with up to five extra timezones underneath.
- { id: clock-1, type: clock, title: Clock,
config: { use24h: true, style: digital, dateFormat: dmy, timezones: [America/New_York, UTC] } }BookmarksshortcutList of quick links
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.
- { id: bm-1, type: shortcut, title: Links,
config: { shortcuts: [ { label: Router, url: https://192.168.1.1, icon: ubiquiti } ] } }SearchsearchSearch box for a chosen engine
A search box that submits to DuckDuckGo, Google, Bing or a custom address with {q}. Bangs (g, gh, yt, w, gm, npm, aw …) jump to one site; a taller tile shows them as chips.
- { id: q-1, type: search, config: { engine: custom, customUrl: "https://searx.lan/search?q={q}" } }ChecklistchecklistTodo list with a progress bar
A checklist you tick off in place, with a progress bar. Items are saved with the widget.
- { id: todo-1, type: checklist, title: Todo,
config: { checklist: [ { text: Renew certs, done: false } ] } }NotesnotesFree-text scratchpad, optional Markdown
A free-text scratchpad, optionally rendered as Markdown (click to edit). URLs auto-link.
- { id: note-1, type: notes, title: Notes, config: { markdown: true, text: "## TODO" } }CountdowncountdownTime until (or since) a date
A live countdown to, or count-up since, a date and time.
- { id: cd-1, type: countdown, config: { target: "2026-12-31T23:59:00", label: New Year } }CalendarcalendariCal (.ics) agenda or month grid
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.
- { 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
An image by URL, optionally a link.
- { id: img-1, type: image, config: { url: https://example.com/logo.png, fit: contain } }Section labelsectionFrameless heading to divide the board
A frameless heading that divides a busy board.
- { id: sec-1, type: section, config: { text: Infrastructure, align: left } }PomodoropomodoroFocus / break timer
A focus/break timer with start, pause and reset.
- { id: pom-1, type: pomodoro, config: { work: 25, break: 5 } }Tabbed grouptabgroupSeveral widgets in one card, switched by tabs
Stacks several widgets in one card, switched by tabs. Each tab is a full widget with its own config, picked and set up inline.
- { 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
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.
- { id: host-1, type: host, title: Host, config: { showLoad: true } }Resource gaugegaugeOne or two host metrics as ring / bar / sparkline
One host metric (or two, splitting the tile) as a ring, bar or sparkline.
- { 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
Live load of every logical CPU core as a strip of bars.
- { id: cores, type: percpu, title: Cores, config: { colorScale: levels, warn: 70, crit: 90 } }TemperaturestempsHardware sensors from hwmon
Temperature sensors (CPU, NVMe, chipset) from /sys/class/hwmon. Pick which to show and rename them.
- { id: temps-1, type: temps, title: Temps,
config: { warn: 70, crit: 85, names: { "Package id 0": CPU } } }Top processestopprocHighest CPU / memory processes
The busiest processes by CPU or memory. Host processes need pid: host on the container.
- { id: top-1, type: topproc, title: Top, config: { count: 8, sort: cpu } }FilesystemsdisksUsage bar per mounted filesystem
A usage bar for every real filesystem you pick. The host's mounts need the read-only /:/host bind.
- { id: fs-1, type: disks, title: Disks, config: { mounts: [/, /mnt/tank] } }Network graphnetgraphLive download / upload chart
Live download and upload throughput as an area chart. Real host traffic needs host networking.
- { id: net-1, type: netgraph, title: Network, config: { style: mirror, window: 5m } }ContainerscontainersDocker / Podman containers, one per row
Containers from the mounted socket, one per row with its image, ports and volumes, optionally CPU and memory. Restart one from its row.
- { id: ctr-1, type: containers, title: Containers,
config: { runningOnly: true, stats: true, filter: media } }Battery / UPSbatteryBatteries and UPS units
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
Up/down and latency for an ad-hoc list of URLs, separate from your services.
- { 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
Your WAN IP with location and provider. Shows “VPN on” when the provider's name contains what you set.
- { id: ip-1, type: publicip, title: WAN, config: { expectIsp: Mullvad } }Speed testspeedtestDownload / upload / latency, run in the browser
An internet speed test run in your browser against Cloudflare (about 50 MB per run). It measures the viewing device's connection.
- { id: spd-1, type: speedtest, title: Speed, config: { auto: false } }Speedtest TrackerspeedtesttrackerLatest result and history from Speedtest Tracker
Speedtest Tracker: latest download, upload and ping, plus recent history.
- { id: stt-1, type: speedtesttracker, title: Speedtest, config: { connection: speedtest } }UniFiunifiConnected clients and live WAN throughput
A UniFi console: connected clients and live WAN download and upload.
- { id: unifi-1, type: unifi, title: UniFi, config: { connection: unifi, site: default } }Wake-on-LANwolWake-on-LAN buttons
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.
- { 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
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.
- { id: apps-1, type: apps, title: Media,
config: { appIds: [jellyfin, sonarr, radarr], showNames: true, grouped: false } }AppappOne service as a tile, layout by size
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.
- { id: app-nas, type: app, title: NAS,
config: { appId: nas, showResponseTime: true, showLastChecked: true } }Status summarystatusEvery service: history bars down to coloured squares
Every health-checked service, from full uptime-history bars at large sizes down to coloured squares, optionally broken down by group.
- { id: sum-1, type: status, title: Overview,
config: { byGroup: true, bars: true, groups: [infra, media] } }ProxmoxproxmoxPVE nodes, VMs / LXC and storage
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.
- { id: pve-1, type: proxmox, title: Proxmox,
config: { servers: [ { connection: pve, name: pve-01 } ], showBackups: true } }Proxmox Backup ServerpbsDatastore usage per store
Datastore usage (used / total) per store on a Proxmox Backup Server.
- { id: pbs-1, type: pbs, title: PBS, config: { connection: pbs } }Jellyfin / PlexmediaJellyfin / Emby / Plex sessions and libraries
Jellyfin, Emby or Plex: now-playing sessions with progress, plus library counts.
- { id: jf-1, type: media, title: Jellyfin, config: { connection: jellyfin } }Sonarr / RadarrarrDownload queue and upcoming calendar
The download queue and upcoming releases from Sonarr or Radarr.
- { id: sonarr-1, type: arr, title: Sonarr, config: { connection: sonarr, days: 7 } }Overseerr / JellyseerrseerrPending / approved / available requests
Overseerr / Jellyseerr: pending, approved and available media requests.
- { id: seerr-1, type: seerr, title: Requests, config: { connection: overseerr } }TransmissiontransmissionTorrents: progress, rates, ratio
Transmission torrents with progress, up/down rates and ratio, plus live totals.
- { id: tr-1, type: transmission, title: Transmission, config: { connection: transmission, max: 8 } }qBittorrentqbittorrentTorrents: progress, rates, ratio
qBittorrent torrents with progress, up/down rates and ratio, plus live totals.
- { id: qb-1, type: qbittorrent, title: qBittorrent, config: { connection: qbittorrent, max: 8 } }ImmichimmichPhoto / video counts and storage
Immich: photo and video counts and storage used.
- { id: immich-1, type: immich, title: Immich, config: { connection: immich } }NextcloudnextcloudFree space, users, files, shares
Nextcloud: free space, users, files and shares from the serverinfo API.
- { id: nc-1, type: nextcloud, title: Nextcloud, config: { connection: nextcloud } }Paperless-ngxpaperlessTotal documents and inbox
Paperless-ngx: total documents and how many wait in the inbox.
- { id: ppl-1, type: paperless, title: Paperless, config: { connection: paperless } }PortainerportainerContainers / images / volumes / stacks
Portainer: running, stopped and total containers, images, volumes and stacks for an environment.
- { id: ptn-1, type: portainer, title: Portainer, config: { connection: portainer, env: 1 } }ScrutinyscrutinyDisk SMART health
Scrutiny: passed / failed / unknown drives, with temperature per drive.
- { id: scr-1, type: scrutiny, title: SMART, config: { connection: scrutiny } }DNS SinkholednsPi-hole / AdGuard / Technitium stats
Pi-hole, AdGuard Home or Technitium: queries, block rate, blocklist size and top blocked domains.
- { id: dns-1, type: dns, title: Pi-hole, config: { connection: pihole } }TraefiktraefikRouter / service / middleware counts
Traefik: router, service and middleware counts with error and warning flags.
- { id: trf-1, type: traefik, title: Traefik, config: { connection: traefik } }PrometheusprometheusFiring and pending alerts
Prometheus: firing and pending alerts, ranked by severity.
- { id: prom-1, type: prometheus, title: Alerts, config: { connection: prometheus } }TailscaletailscaleDevices, online, pending updates
Your tailnet: device count, how many are online, and pending client updates.
- { id: ts-1, type: tailscale, title: Tailscale, config: { connection: tailscale } }NotificationsnotifyGotify or ntfy messages
Recent messages from Gotify (with app and client counts) or an ntfy topic, newest first.
- { id: ntfy-1, type: notify, title: Alerts, config: { connection: ntfy, topic: my-alerts } }CameracameraFrigate camera or any MJPEG / JPEG stream
A live camera: a Frigate camera by name, or any MJPEG / JPEG stream address.
- { id: cam-1, type: camera, title: Door,
config: { source: frigate, baseUrl: http://frigate.lan:5000, camera: door } }Grafana panelgrafanaA Grafana panel or dashboard
A Grafana panel (Share → Embed, d-solo/…) or dashboard in a frame. Grafana needs allow_embedding = true.
- { 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
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).
- { 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
How many people are home and lights and switches on, plus entities you pin, with toggles and run buttons for them.
- { id: ha-1, type: homeassistant, title: Home,
config: { connection: ha, entities: [switch.coffee, script.goodnight] } }HA LightshalightsToggle and dim several lights
On/off and brightness for the lights (and switches) you pick. Sliders take the light's colour.
- { id: lights-1, type: halights, title: Lights,
config: { connection: ha, entities: [light.kitchen, light.hall] } }HA LighthalightOne light: toggle, brightness, warmth, hue
One light: toggle and brightness, warmth over the light's own kelvin range, and a hue bar for RGB lights.
- { id: light-1, type: halight, config: { connection: ha, entity: light.desk } }HA FanshafansSeveral fans, stepped speed
Several fans, each an Off / 1 / 2 / 3 speed slider.
- { id: fans-1, type: hafans, title: Fans,
config: { connection: ha, entities: [fan.bedroom, fan.office] } }HA FanhafanOne fan in one row
One fan as an Off / 1 / 2 / 3 speed slider in a single row.
- { id: fan-1, type: hafan, config: { connection: ha, entity: fan.bedroom } }HA ThermostathaclimateCurrent temperature, target, HVAC mode
A thermostat: current temperature, target +/- and HVAC mode.
- { id: therm-1, type: haclimate, config: { connection: ha, entity: climate.living_room } }HA CovershacoverOpen / stop / close several covers
Open / stop / close blinds, garage doors and curtains, with position.
- { id: covers-1, type: hacover, title: Blinds,
config: { connection: ha, entities: [cover.living, cover.garage] } }HA CoverhacoveroneOne cover: open / stop / close
One blind, garage door or curtain: open / stop / close.
- { id: cover-1, type: hacoverone, config: { connection: ha, entity: cover.garage } }HA MediahamediaNow playing, transport, volume
A media player: now playing, previous / play-pause / next and volume.
- { id: hmedia-1, type: hamedia, config: { connection: ha, entity: media_player.living_room } }HA LockhalockLock / unlock a door
Lock or unlock a door, with its state.
- { id: lock-1, type: halock, config: { connection: ha, entity: lock.front_door } }HA VacuumhavacuumStart / pause / dock, state, battery
A vacuum: start / pause / dock, with state and battery.
- { id: vac-1, type: havacuum, config: { connection: ha, entity: vacuum.robot } }HA SceneshascenesOne-tap scenes / scripts / automations
One-tap buttons for scenes, scripts, automations and buttons; the right service is called per domain.
- { id: scenes-1, type: hascenes, title: Scenes,
config: { connection: ha, entities: [scene.movie, script.goodnight] } }HA SensorshasensorsValues for the sensors you pick
Read-only rows for any sensor / binary_sensor; binary sensors show a state dot.
- { 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
One sensor as a big value (temperature, humidity, a door, …).
- { id: sensor-1, type: hasensor, config: { connection: ha, entity: sensor.outdoor_temp } }HA PowerhapowerLive draw per power sensor, plus total
Live draw for the power sensors you pick, each a bar scaled to max, with the total in the header.
- { id: power-1, type: hapower, title: Power,
config: { connection: ha, entities: [sensor.house_power], max: 3000 } }HA PresencehapresenceWho's home, with zones
Who's home, with avatars and zones: every person.* Home Assistant tracks. No entity picker.
- { id: presence-1, type: hapresence, title: Presence, config: { connection: ha } }HA BatterieshabatteryEvery device battery, lowest first
Every battery sensor, lowest first, with low-battery flags. Found automatically.
- { id: batteries-1, type: habattery, title: Batteries, config: { connection: ha } }Web & feeds
WeatherweatherCurrent conditions and forecast (Open-Meteo)
Current conditions and forecast from Open-Meteo (no API key). Grows from compact to detailed to an 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
Sunrise, sunset, UV index and a daylight arc for a city.
- { id: sun-1, type: sun, config: { city: Lisbon, lat: 38.72, lon: -9.14 } }MarketsmarketsCrypto and stocks with charts
Prices for crypto (CoinGecko ids) and stocks / ETFs (Yahoo tickers). Taller tiles switch from a price list to one chart per symbol.
- { id: mkt-1, type: markets, title: Markets,
config: { ids: [bitcoin, ethereum], stocks: [AAPL, VWCE.DE], vs: eur, period: 1m } }ReleasesreleasesLatest GitHub / GitLab releases
Latest releases of the projects you watch, on GitHub or GitLab.
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
Recent items from an RSS or Atom feed, fetched by the server.
- { id: rss-1, type: rss, title: News, config: { url: https://example.com/feed.xml, count: 10 } }RedditredditPosts from a subreddit
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.
- { id: rdt-1, type: reddit, title: r/selfhosted,
config: { subreddit: selfhosted, sort: hot, clientId: "…", clientSecret: "…" } }Hacker NewshackernewsFront page, Ask HN or Show HN
Hacker News front page, Ask HN or Show HN, with points, comments and age.
- { id: hn-1, type: hackernews, title: HN, config: { kind: front_page, max: 10 } }LobsterslobstersLobste.rs hottest stories
Lobste.rs hottest stories with score, comments, tags and age.
- { id: lob-1, type: lobsters, title: Lobsters }YouTube channelyoutubeRecent uploads with thumbnails
Recent uploads from a YouTube channel, via its RSS feed (no API key).
UC…) · show (all that fit, 5, 10, 15).- { id: yt-1, type: youtube, title: Uploads, config: { channelId: "UC…" } }XKCDxkcdThe latest XKCD comic
The latest XKCD comic, alt text on hover. No settings.
- { id: xkcd-1, type: xkcd }Custom APIcustomapiAny JSON endpoint as labelled values
Fetches a JSON endpoint (through the server) and shows chosen values by path (data.queue, items[0].name).
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
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.
- { 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
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.
- { id: emb-1, type: iframe, title: Wiki, config: { url: https://wiki.lan, refreshSec: 0 } }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.
| Tab | Saved | Options |
|---|---|---|
| General | this browser | Theme (dark / light / system) · palette (neutral / blue) · accent (violet, blue, teal, amber, rose, slate, stone) · font (Inter, System, Rounded, Humanist, Serif, Mono) · language. |
| This dashboard | config.yaml | Background: 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 bar | config.yaml, all dashboards | Bar 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). |
| Widgets | this browser | Opacity, 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
localeif set, else the visitor's browser, elseserver.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.
| Key | Action |
|---|---|
| ⌘/Ctrl K | Search: services, bookmarks, dashboards, every setting and appearance option, web search (bangs: g …, !yt …, gh …, w …) |
| ? | Show the shortcut list |
| Esc | Close a panel or leave edit mode |
| ⌘/Ctrl E | Edit mode on / off |
| ⌘/Ctrl Z / ⌘/Ctrl ⇧ Z | Undo / redo |
| Del / ⌫ | Remove the selected widget (edit mode) |
| Arrow keys | Move the selected widget (edit mode) |
| ⌘/Ctrl 1…9 | Go 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.
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-statevolume: 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
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.