A lightweight Go web dashboard for monitoring Docker Compose services and systemd services across multiple hosts.
- Monitor Docker containers from Compose projects
- Monitor systemd units on local and remote hosts
- Monitor Home Assistant instances (with full HAOS addon support)
- Real-time log streaming via Server-Sent Events
- Real-time service state updates via WebSocket
- Dark theme web interface with sorting, filtering, and search
- Filter mode (hide non-matching) and Find mode (navigate between matches)
- Bang & Pipe query language for advanced filtering - readme on that
- Traefik integration for hostnames and external service discovery
- Log truncation for Docker containers
- Gotify push notifications for service state changes
- Go 1.21 or later
- Docker (for container monitoring)
- SSH access to remote hosts (for remote systemd monitoring)
- Traefik with API enabled (optional, for hostname discovery)
- Home Assistant with long-lived access token (optional, for HA monitoring)
- Copy the sample configuration and edit it for your environment:
cp sample.services.json services.json- Edit
services.jsonto define your hosts and services:
{
"port": 9001,
"hosts": [
{
"name": "myserver",
"address": "localhost",
"systemd_services": [
"docker.service",
"nginx.service",
"myuser:myapp.service"
],
"docker_compose_roots": ["/path/to/your/compose/projects/"]
}
]
}| Field | Description |
|---|---|
port |
HTTP server port (default: 9001) |
- Build and run:
# Install npm dependencies and build frontend (required once)
npm install
npm run build
# Then build and run Go binary
go build -o nas-dashboard && ./nas-dashboardAlternatively, use go generate to run both npm commands automatically:
go generate ./...
go build -o nas-dashboard && ./nas-dashboard- Open http://localhost:9001 in your browser.
Install as a systemd service:
./install.sh [username]The install script:
- Compiles and installs binary to
/usr/local/bin/nas-dashboard - Copies
services.json(orsample.services.json) to/etc/nas_dashboard/services.json - Auto-updates config: If your local
services.jsonis newer than the installed version, it will be copied automatically - Installs and enables the systemd service
- Generates polkit rules for local systemd service control
usernamedefaults to the current user
The script uses sudo internally only for commands that require it, so you don't need to run it with sudo. This however does mean you need sudoers access ;)
Uninstall:
./uninstall.shThe dashboard queries Docker containers via the Docker socket, systemd units via D-Bus (for localhost) or SSH (for remote hosts), and Home Assistant instances via the REST API. For HAOS installations, it additionally tunnels through SSH to access the Supervisor API for addon management. It serves a single-page web interface that fetches service status from /api/services and displays them in a sortable table. Clicking a service row opens an inline log viewer that streams logs in real-time using Server-Sent Events. The configuration file defines which hosts to monitor and which systemd units to track on each host. Docker Compose projects are auto-discovered by scanning the specified root directories.
Set address to localhost to use D-Bus for systemd queries. Any other address will use SSH with your default SSH key.
To display Traefik-exposed hostnames as clickable links next to services, enable Traefik in your host configuration:
{
"hosts": [
{
"name": "myserver",
"address": "localhost",
"traefik": {
"enabled": true,
"api_port": 8080
}
}
]
}The dashboard queries Traefik's /api/http/routers and /api/http/services endpoints to discover hostnames and external services:
Hostname Discovery: Services with Host() or HostRegexp() rules get green hostname badges. When both are present, exact Host() matches are preferred.
External Service Discovery: Traefik can expose services that aren't Docker containers or systemd units (e.g., reverse-proxied external hosts defined in file providers). These appear as "traefik" source services with health status based on Traefik's backend server status (UP/DOWN).
SSH Tunneling: For remote hosts, the dashboard automatically tunnels through SSH to reach the Traefik API.
The dashboard can monitor Home Assistant instances and display their health status. There are two levels of integration:
- Basic monitoring (any HA installation): Shows HA health status and allows restart
- Full HAOS integration (Home Assistant OS only): Shows all addons, streams logs, and allows addon control
For any Home Assistant installation, you can monitor the core HA service:
{
"hosts": [
{
"name": "homeassistant",
"address": "192.168.1.50",
"homeassistant": {
"port": 8123,
"use_https": true,
"ignore_https_errors": true,
"longlivedtoken": "your-long-lived-access-token"
},
"systemd_services": [],
"docker_compose_roots": []
}
]
}| Field | Description |
|---|---|
port |
Home Assistant API port (default: 8123) |
use_https |
Use HTTPS for API connection |
ignore_https_errors |
Skip TLS certificate verification (for self-signed certs) |
longlivedtoken |
Long-lived access token from HA (create in Profile → Security → Long-Lived Access Tokens) |
Features with basic monitoring:
- Health status (running/stopped) displayed in dashboard
- Restart HA Core via the dashboard (start/stop not supported without HAOS)
- Gotify notifications when HA becomes unreachable
For Home Assistant OS installations, you can get complete visibility including all addons, log streaming, and addon control. This requires the Advanced SSH & Web Terminal addon.
Prerequisites:
-
Install the Advanced SSH & Web Terminal addon from the Home Assistant Add-on Store (Community Add-ons repository)
-
Configure the SSH addon with atleast these options:
ssh: username: hassio authorized_keys: - >- (your public key instead, but the whole line just like an authorized_keys file, one per yaml element) allow_remote_port_forwarding: true allow_tcp_forwarding: true
-
Start the SSH addon and verify you can connect:
ssh hassio@192.168.1.50 -p 22
-
Configure the dashboard with HAOS settings:
{
"hosts": [
{
"name": "homeassistant",
"address": "192.168.1.50",
"homeassistant": {
"port": 8123,
"use_https": true,
"ignore_https_errors": true,
"longlivedtoken": "your-long-lived-access-token",
"is_homeassistant_operatingsystem": true,
"ssh_addon_port": 22
},
"systemd_services": [],
"docker_compose_roots": []
}
]
}| Additional Field | Description |
|---|---|
is_homeassistant_operatingsystem |
Enable HAOS addon discovery via Supervisor API |
ssh_addon_port |
SSH addon port (default: 22) |
How it works:
The dashboard connects to the SSH addon and creates a tunnel to the internal Supervisor API (http://supervisor). It automatically retrieves the SUPERVISOR_TOKEN from the SSH addon container, which rotates on each HAOS reboot.
Features with HAOS integration:
- All installed addons displayed as separate services
- Real-time log streaming for Core, Supervisor, Host, and individual addons
- Start/stop/restart HA Core via the Supervisor API
- Start/stop/restart addons via the dashboard
- Supervisor and Host OS status display
- Gotify notifications for addon state changes
Note: The SSH addon must remain running for the Supervisor API access to work. If you stop the SSH addon, the dashboard will fall back to basic monitoring.
The dashboard can send push notifications via Gotify when services change state or hosts become unreachable. This is useful for getting alerted when a service goes down.
{
"gotify": {
"enabled": true,
"hostname": "https://gotify.example.com",
"token": "your-app-token"
}
}| Field | Description |
|---|---|
enabled |
Enable or disable Gotify notifications |
hostname |
URL of your Gotify server |
token |
Application token from Gotify (create an app in Gotify's web UI) |
Events that trigger notifications:
| Event | Priority | Description |
|---|---|---|
| Service started | Normal (5) | 🟢 Service went from stopped to running |
| Service stopped | High (8) | 🔴 Service went from running to stopped |
| Host unreachable | Max (10) | 🚨 Cannot connect to a configured host |
| Host recovered | High (8) | ✅ Previously unreachable host is now reachable |
The monitor uses native event sources for efficient real-time detection:
- Docker: Uses the Docker Events API to receive container state changes instantly
- Local systemd: Uses D-Bus signals for immediate unit state notifications
- Remote systemd: Falls back to polling (every 60 seconds) since native events aren't available over SSH
- Home Assistant: Polls the HA API at regular intervals; for HAOS, also monitors addon states
Note: On startup, the monitor captures the current state of all services without sending notifications, so you won't receive a flood of alerts when the dashboard restarts.
The dashboard reads custom labels from Docker containers to customize visibility and display:
| Label | Description |
|---|---|
home.server.dashboard.description |
Custom description displayed below service name |
home.server.dashboard.hidden |
Set to true to hide service from dashboard |
home.server.dashboard.ports.hidden |
Comma-separated port numbers to hide (e.g., 8080,9000) |
home.server.dashboard.ports.<port>.label |
Custom label for a specific port |
home.server.dashboard.ports.<port>.hidden |
Set to true to hide a specific port |
home.server.dashboard.ports.<port>.protocol |
Set port link protocol to http or https (default: http) |
home.server.dashboard.remapport.<port> |
Remap a port to another service (for containers sharing network namespace) |
Protocol Override: By default, port links use http://. Set the protocol label to https for services with TLS/SSL enabled. Works with both direct ports and remapped ports:
services:
traefik:
labels:
home.server.dashboard.ports.8443.protocol: "https" # Opens https://host:8443
gluetun:
ports:
- "9091:9091"
labels:
home.server.dashboard.ports.9091.protocol: "https" # Protocol preserved when remapped
home.server.dashboard.remapport.9091: firefox
firefox:
network_mode: "service:gluetun" # Gets https://gluetun-ip:9091 linkPort Remapping: For containers that share a network namespace (e.g., services running through a VPN container like gluetun), use remapport to show the port on the correct service:
services:
gluetun:
image: qmcgaw/gluetun
ports:
- "8193:8193" # qbittorrent web UI exposed through VPN
labels:
home.server.dashboard.remapport.8193: qbittorrent
qbittorrent:
network_mode: "service:gluetun"
# Port 8193 will appear on qbittorrent with a link using gluetun's IPExample:
services:
myapp:
labels:
home.server.dashboard.description: "My application"
home.server.dashboard.ports.8080.label: "Admin Panel"
home.server.dashboard.ports.8080.protocol: "https"
home.server.dashboard.ports.9000.hidden: "true"The dashboard integrates with Watchtower to suppress false-positive "service stopped" notifications during container updates. When Watchtower updates a container, it briefly stops the old container before starting the new one. Without integration, this would trigger unwanted "service down" alerts.
{
"hosts": [
{
"name": "myserver",
"address": "localhost",
"watchtower": {
"port": 8080,
"token": "your-watchtower-api-token",
"update_timeout": 120
}
}
]
}| Field | Description |
|---|---|
port |
Watchtower metrics API port (default: 8080) |
token |
Metrics API token for authentication (can use WATCHTOWER_TOKEN env var) |
update_timeout |
Seconds to wait for container recovery before sending notification (default: 120) |
How it works:
- When a Docker container stops on a host with Watchtower configured, the notification is queued instead of being sent immediately
- The dashboard queries Watchtower's
/v1/metricsendpoint to check if containers are being scanned or were recently updated - If an update is detected as in-progress or recent (within 30 seconds), the container stop is assumed to be update-related
- The dashboard waits for the container to restart within
update_timeoutseconds - If the container restarts within the timeout, the notification is cancelled (no alert sent)
- If the timeout expires and the container is still down, the notification is sent normally
Requirements:
- Watchtower must have
--http-api-metricsenabled (for metrics endpoint) - Watchtower must have
--http-api-tokenset (for authentication) - The dashboard needs network access to Watchtower's metrics endpoint
Example Watchtower configuration:
services:
watchtower:
image: containrrr/watchtower
environment:
- WATCHTOWER_HTTP_API_TOKEN=your-secret-token
- WATCHTOWER_HTTP_API_METRICS=true
ports:
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sockEnvironment Variable: The WATCHTOWER_TOKEN environment variable takes precedence over the token in services.json, allowing you to keep secrets out of configuration files.
Benefits:
- Eliminates false-positive alerts during routine container updates
- Reduces notification noise while maintaining alert coverage for actual failures
- Configurable timeout allows adjustment based on typical update duration
- Read-only integration - the dashboard only monitors updates, it does not trigger them
Descriptions for systemd units are automatically fetched from the unit's Description field.
Systemd services can be marked as read-only to prevent start/stop/restart actions from all users (including admins). This is useful for critical services that should only be monitored, not controlled.
To mark a service as read-only, append :ro to the service name in your configuration:
{
"hosts": [
{
"name": "myserver",
"address": "localhost",
"systemd_services": [
"docker.service",
"nas-dashboard.service:ro"
]
}
]
}Behavior:
- Read-only services display a lock icon (🔒) instead of start/stop/restart buttons in the UI
- All control actions (start, stop, restart) are rejected with a 403 Forbidden response
- Applies to all users, including administrators
- Useful for monitoring the dashboard's own systemd service to prevent accidental self-termination
Example use case: Marking nas-dashboard.service:ro prevents users from stopping or restarting the dashboard through the web interface, which would cause the dashboard to become unavailable.
User-level systemd services (those in ~/.config/systemd/user/) can be monitored using the username:servicename.service notation:
{
"hosts": [
{
"name": "myserver",
"address": "localhost",
"systemd_services": [
"docker.service",
"xero:zunesync.service",
"alice:backup.timer:ro"
]
}
]
}Supported formats:
| Format | Description |
|---|---|
servicename.service |
System service |
servicename.service:ro |
System service, read-only |
username:servicename.service |
User service for specified user |
username:servicename.service:ro |
User service, read-only |
Behavior:
- User services are managed via
systemctl --userinstead of system D-Bus - They display with project name
systemd-userto distinguish from system services - Container name shows as
username@servicename.servicefor clarity - Supports the same
:rosuffix for read-only mode
Requirements:
- For local user services: The dashboard must run as the target user, or have permissions to use
machinectl - For remote user services: SSH user must have sudo access to run
systemctl --useras the target user - User services require lingering enabled:
sudo loginctl enable-linger username
The table search widget (below the filter cards) supports two modes:
- Filter mode (funnel icon): Hides rows that don't match the search term. Shows "X of Y" count.
- Find mode (search icon): Highlights matches and allows navigation between them with up/down buttons or Enter/Shift+Enter. Shows "1 of N" current position.
Toggle between modes by clicking the mode icon button. The search supports plain text, regex (case sensitivity toggle available), and Bang & Pipe expressions.
A dynamic row of host badges appears below the status/source filter cards, showing all configured hosts with service counts. Click a host badge to filter the table to that host only.
Click any service row to expand an inline log viewer with real-time streaming. The log search box supports:
- Plain text search (case-insensitive by default)
- Regex mode: Prefix with
!to invert matches (show lines NOT matching the pattern) - Bang & Pipe expressions for complex queries
For Docker containers, the dashboard tracks log file sizes and displays them in the Logs column. Administrators can truncate container logs to reclaim disk space:
- Click the log size badge in the Logs column
- Confirm the truncation in the modal dialog
Note: Log truncation requires the log-truncate-helper binary and appropriate permissions. The install script sets this up automatically with setcap capabilities.
The dashboard supports optional authentication via OIDC (for external access) and PAM-based local authentication (for direct/internal access).
For external access through a reverse proxy, configure OIDC to authenticate users against an identity provider (e.g., Authentik, Keycloak, Auth0):
{
"oidc": {
"service_url": "https://dashboard.example.com",
"callback": "/oidc/callback",
"config_url": "https://auth.example.com/application/o/myapp/.well-known/openid-configuration",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"groups_claim": "groups",
"admin_group": "admin"
}
}| Field | Description |
|---|---|
service_url |
The public URL where the dashboard is accessed |
callback |
OAuth2 callback path (typically /oidc/callback) |
config_url |
OIDC discovery endpoint URL |
client_id |
OAuth2 client ID from your identity provider |
client_secret |
OAuth2 client secret |
groups_claim |
Claim containing user groups (default: groups) |
admin_group |
Group name that grants full access (default: admin) |
Users must belong to the configured admin_group to have full access to all services.
For non-admin users, you can grant access to specific services based on OIDC group membership. This allows users to view and control only the services they're authorized for.
{
"oidc": {
"service_url": "https://dashboard.example.com",
"config_url": "https://auth.example.com/.well-known/openid-configuration",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"groups": {
"media-team": {
"services": {
"nas": ["plex", "jellyfin", "audiobookshelf"]
}
},
"devops": {
"services": {
"nas": ["docker.service", "traefik"],
"webserver": ["nginx.service"]
}
}
}
}
}- Each key under
groupsmust match an OIDC group name exactly servicesmaps host names to arrays of service names (Docker services or systemd units)- Permissions are additive: users in multiple groups get combined access from all groups
- Users in the
admin_groupalways have full access regardless of group configuration - Group filtering applies only to OIDC users; local/PAM users always have full access
For direct access (when the Host header doesn't match service_url), users are authenticated via PAM using their system credentials:
{
"local": {
"admins": "user1,user2"
}
}Only usernames listed in admins can authenticate locally. Passwords are validated against the system's PAM configuration (typically /etc/shadow).
Note: The systemd service requires CAP_DAC_READ_SEARCH capability for PAM authentication to read shadow passwords. This is configured automatically by the install script.
If neither oidc nor local sections are configured, the dashboard runs without authentication (not recommended for production).
The dashboard can start, stop, and restart services. This requires proper authorization setup depending on whether the host is local or remote.
For local systemd service control, the dashboard uses D-Bus to communicate with systemd. D-Bus requires polkit authorization to allow non-root users to manage services.
The install script automatically generates and installs polkit rules to /etc/polkit-1/rules.d/50-home-server-dashboard.rules. This grants the dashboard user permission to start, stop, and restart only the specific systemd services defined in your services.json.
To regenerate polkit rules manually (e.g., after adding new services):
./nas-dashboard -generate-polkit | sudo tee /etc/polkit-1/rules.d/50-home-server-dashboard.rulesWhy polkit instead of sudo? The systemd service runs with NoNewPrivileges=true for security hardening, which prevents using sudo. Polkit provides fine-grained authorization for D-Bus operations without requiring privilege escalation.
For remote systemd hosts, SSH key-based authentication must be configured:
# Generate an SSH key if you don't have one
ssh-keygen -t ed25519 -C "dashboard"
# Copy the key to remote hosts
ssh-copy-id user@192.168.1.9For remote hosts, systemctl commands are executed over SSH and require sudo privileges. Configure passwordless sudo for only the specific services you want to manage.
Generate sudoers configuration automatically from your services.json:
# Generate for current user
./dashboard -generate-sudoers
# Generate for a specific user
./dashboard -generate-sudoers -user myuserThis outputs a sudoers configuration based on your configured systemd services. Install it with:
./dashboard -generate-sudoers | sudo tee /etc/sudoers.d/home-server-dashboard
sudo chmod 440 /etc/sudoers.d/home-server-dashboardFor remote hosts, copy the relevant lines to each remote machine's /etc/sudoers.d/home-server-dashboard.
Manual configuration (if preferred):
On each host (local and remote), create a sudoers file:
sudo visudo -f /etc/sudoers.d/home-server-dashboardAdd rules for the specific services (replace youruser and service names):
# Allow dashboard user to manage specific systemd services without password
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl start ollama.service
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop ollama.service
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart ollama.service
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl start docker.service
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop docker.service
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart docker.service
Or use a pattern to allow all systemctl operations on specific services:
# Allow start/stop/restart for specific services
youruser ALL=(ALL) NOPASSWD: /usr/bin/systemctl start ollama.service, \
/usr/bin/systemctl stop ollama.service, \
/usr/bin/systemctl restart ollama.service
For Docker containers, the user running the dashboard needs to be in the docker group:
sudo usermod -aG docker youruser
# Log out and back in for group changes to take effect- Principle of Least Privilege: Only grant sudo access to the specific services listed in your
services.json - Avoid wildcards: Don't use
systemctl *patterns in sudoers - SSH hardening: Consider using a dedicated SSH key for the dashboard and restricting it with
ForceCommandif needed
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Dashboard HTML page (protected) |
/static/* |
GET | Static files (CSS/JS, public) |
/login |
GET | Initiate OIDC login flow |
/oidc/callback |
GET | OIDC callback handler |
/logout |
GET | Clear session, redirect to login |
/auth/status |
GET | Authentication status JSON |
/api/services |
GET | All services JSON array |
/api/logs?container=<name> |
GET | Docker container logs (SSE stream) |
/api/logs/systemd?unit=<name>&host=<host> |
GET | Systemd unit logs (SSE stream) |
/api/logs/traefik?service=<name>&host=<host> |
GET | Traefik service logs (stub) |
/api/logs/homeassistant?... |
GET | Home Assistant logs (SSE stream) |
/api/logs/flush |
POST | Truncate Docker container logs (admin) |
/api/services/start |
POST | Start a service (SSE status updates) |
/api/services/stop |
POST | Stop a service (SSE status updates) |
/api/services/restart |
POST | Restart a service (SSE status updates) |
/api/bangAndPipeToRegex?expr=<expr> |
GET | Compile Bang & Pipe expression to AST |
/api/docs/bangandpipe |
GET | Bang & Pipe documentation HTML |
/ws |
GET | WebSocket for real-time service updates |
GPLv3
