DEV Community

Cover image for I Built a Dashboard to Monitor My Self-Hosted Docker Services
Cyber
Cyber

Posted on

I Built a Dashboard to Monitor My Self-Hosted Docker Services

I run a bunch of self-hosted services on my home server. Vaultwarden, Linkwarden, Nextcloud, Anynote, AFFiNE, Vikunja - the list keeps growing. At some point I had over 30 containers running and I realized I had a problem.

I couldn't remember what was running where.

Which port was Vikunja on again? Is Nextcloud on .local or the IP? Did Anynote go down after that update? Let me SSH in and check...

I wanted one place where I could:

  • See all my services at a glance
  • Know instantly if something is down
  • Access my DevOps tutorials and notes
  • Not need to bookmark 40 different URLs

So I built Cyberboard - a self-hosted homelab dashboard that runs in Docker alongside everything else.

The Problem

If you run a homelab, you probably know the feeling. You start with one or two containers. Then you add Vaultwarden because managing passwords yourself is the right thing to do. Then Nextcloud because Google Drive feels wrong now. Then a media stack. Then monitoring. Then a reverse proxy. Then DNS.

Before you know it, you're managing a small data center from your closet.

My pain points were:

  • Scattered bookmarks - services spread across browser profiles and devices
  • No health visibility - I'd only find out something crashed when I tried to use it
  • Wasted time - looking up IPs and ports in Docker Compose files
  • Lost notes - network configs and IPs scattered across random text files

Existing dashboards like Homer and Heimdall looked nice but felt static. I wanted something I could edit live, that checked if my services were actually responding, and where I could keep my learning resources next to my infrastructure.

What I Built

Cyberboard is a single-page dashboard with:

  • Service cards with live health status (green/red dots)
  • Tutorial cards for my DevOps notes (Terraform, K8s, Docker, CI/CD, etc.)
  • Quick links to GitHub, Docker Hub, K8s docs, and other references
  • A scratchpad for quick notes and network info
  • Built-in editor to add, edit, remove, and reorder everything
  • Search across all sections
  • Category filters (Productivity, Media, Infrastructure, Security, etc.)
  • Dark/light theme

Everything is stored in a single data.json file. No database, no accounts, no complexity.

The Health Check Problem

This was the most interesting part to build. My first attempt used browser-side fetch with mode: 'no-cors' to ping each service. It sort of worked - but had major issues:

  • HTTPS services with self-signed certs always showed as down (browser blocks them)
  • Auth-protected services (like ones behind Authentik) showed as down
  • no-cors responses are opaque - you can't tell if a service actually responded or not

The fix was to move health checks server-side. The Python server pings each service URL directly using HEAD requests, with:

  • Self-signed cert support (common in homelabs)
  • Any HTTP response = online (even 401/403 - the service is running, it just wants auth)
  • Parallel checks (20 threads, all services pinged simultaneously)
  • 5-second timeout per service

Now a service behind HTTPS with a self-signed cert and basic auth shows as green. Because it is up - it just has security in front of it.

def check_url(url):
    try:
        req = urllib.request.Request(url, method="HEAD")
        urllib.request.urlopen(req, timeout=5, context=SSL_CTX)
        return True
    except urllib.error.HTTPError:
        # 401, 403, 500 — service is running
        return True
    except Exception:
        # Connection refused, timeout, DNS failure — actually down
        return False
Enter fullscreen mode Exit fullscreen mode

The Tech Stack (No Build Step)

I wanted to keep it simple. No npm install, no webpack, no build pipeline. Just files that a browser can run directly.

  • Preact - 3KB React alternative, loaded from CDN
  • HTM - tagged template literals instead of JSX (no transpiler needed)
  • Lucide - SVG icons for the UI
  • Python stdlib - the entire server is one file with zero pip dependencies
  • Import Maps - browser-native module resolution, no bundler

The import map in the HTML tells the browser where to find each library:

<script type="importmap">
{
  "imports": {
    "preact": "https://esm.sh/preact@10",
    "preact/hooks": "https://esm.sh/preact@10/hooks",
    "htm": "https://esm.sh/htm@3",
    "lucide-preact": "https://esm.sh/lucide-preact@0.474.0?external=preact"
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Every component uses clean imports like import { useState } from 'preact/hooks' - same as any React project, just without the 200MB node_modules folder.

For Docker deployments, I have two Dockerfiles. The default one uses CDN imports (smaller image). The offline one (Dockerfile.offline) downloads and vendors all JS dependencies at build time - so the container runs on isolated networks with zero internet access. You pick which one fits your setup:

# Default - uses CDN
docker compose up -d

# Offline - fully self-contained
docker compose --profile offline up -d cyberboard-offline
Enter fullscreen mode Exit fullscreen mode

How It's Structured

The whole project is ~20 files:

cyberboard/
├── index.html              # HTML shell + import map
├── style.css               # All styles
├── app.js                  # Root component
├── lib/                    # Shared modules
│   ├── preact.js           # Preact/HTM re-exports
│   ├── icons.js            # Lucide icons
│   ├── context.js          # App state context
│   ├── constants.js        # Emojis, colors, categories
│   └── data.js             # Load/save helpers
├── components/             # 14 Preact components
├── data.json               # Your data (gitignored)
├── data.json.default       # Factory defaults
├── server.py               # The entire backend
├── Dockerfile
└── docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

The server is 100 lines of Python. It serves static files, exposes GET /api/health for health checks, and POST /api/save to write changes back to data.json. That's it.

The Editor

One thing I didn't want was to edit a JSON file every time I added a new service. So I built an editor mode directly into the dashboard.

Press E (or click the pencil icon) and:

  • Delete buttons appear on every card
  • Edit buttons let you modify existing entries
  • "Add Service / Add Tutorial / Add Link" cards appear
  • Drag and drop to reorder cards
  • Export/import JSON backups

The add/edit modal has a searchable emoji picker with 250+ emojis organized by category (Tech, Security, Cloud, Media, etc.). Search "docker" and the whale emoji pops up. Search "lock" and you get all the security emojis.

All changes save to data.json on the server automatically (debounced 400ms). No save button needed.

Running It

Local:

git clone https://github.com/ppaczk0wsk1/cyberboard
cd cyberboard
python3 server.py
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8900 and you're done.

Docker:

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Mount data.json as a volume so your customizations persist:

services:
  cyberboard:
    build: .
    ports:
      - "8900:8900"
    volumes:
      - ./data.json:/app/data.json
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

On first run, the server copies data.json.default into place so you start with a full set of example services and tutorials. Then you customize from there.

What I Learned

Building this was a good exercise in restraint. It would have been easy to add user accounts, uptime history graphs, Docker API integration, notification systems... but none of that is needed for a personal dashboard.

The things that actually mattered:

  • Health checks that work with real homelab setups (self-signed certs, auth, weird ports)
  • An editor that's fast enough to not be annoying
  • Search that finds things instantly
  • A single JSON file that's easy to back up and version control

If you run a homelab and you're tired of forgetting which port Sonarr is on, give it a try. The repo is at github.com/ppaczk0wsk1/cyberboard.

What's Next

Some things I'm considering:

  • Response time display on service cards (the health check already pings them - just need to time it)
  • Favorites pinned to top for the services I use most
  • Custom service groups beyond already existing categories

But honestly, it does everything I need right now. And that's kind of the point.


If you're interested in self-hosting, check out my other post: My Top 5 Self-Hosted Tools Running on My Home Server via Docker

Cyberboard is open source and MIT licensed. PRs welcome.

Top comments (0)