I have had a homelab running for a while. A few servers, a handful of self-hosted services, the usual setup. For a long time I did what most people do and kept a browser bookmarks folder with links to everything. Portainer here, Jenkins there, GitLab somewhere else. It worked. It also felt like living out of a junk drawer.
What I actually wanted was one page. Open a new tab, see everything, click anything. No login, no cloud, no subscription.
"The goal was something that felt considered. Not a tool I tolerated, but a page I actually liked opening."
So on a Saturday I sat down and built it.
What It Does
Two pages. The main page shows a service grid, one card per self-hosted service with a name, icon, and host address. The bookmarks page is a full bookmark manager with search, tag filtering, and personal bookmarks that persist in localStorage.
Both pages share a single config file. You edit that one file and everything updates. Add a service and a new card appears. Add a bookmark with a tag and a new filter chip shows up automatically. No build step, no bundler, no package manager involved.
There is also live weather from Open-Meteo, a live clock, and time-of-day greetings. Small things that make the page feel alive.
What You Need
The barrier is about as low as it gets. Here is everything required:
How It Works
One config file
Everything lives in config.js. Your name, location for weather, services, bookmarks, and greeting messages. Both pages load this file at runtime. You never touch index.html or bookmarks.html.
A service entry looks like this:
{ name: "Portainer", icon: "🐳", url: "http://your-server:9000", meta: "your-server:9000" }
Add an object to the array, get a new card on the dashboard. Remove one, it disappears. The grid reflows automatically. Same pattern for bookmarks. Tags are optional but they build the filter chips automatically from whatever values exist.
Weather
Weather reads your coordinates and timezone from config and hits the Open-Meteo API. No account required, no API key, no rate limits. It refreshes every ten minutes and fails silently if the fetch fails.
location: {
label: "City, ST",
lat: 44.9778,
lon: -93.2650,
tz: "America/Chicago",
}
Bookmarks
Built-in bookmarks come from config.js and are the same across every browser that opens the page. Personal bookmarks are added through the UI and stored in localStorage. They live in your browser, not on the server, and never touch the network. Both types are searchable and support tags.
Running It
One Docker command:
docker run -d \
--name dashboard \
-p 8080:80 \
-v $(pwd):/usr/share/nginx/html:ro \
nginx:alpine
Open http://localhost:8080 and you are done. For something permanent alongside other services, a Compose entry handles it:
services:
dashboard:
image: nginx:alpine
container_name: dashboard
restart: unless-stopped
ports:
- "8080:80"
volumes:
- ./self-host-dashboard:/usr/share/nginx/html:ro
What I Would Do Differently
localStorage bookmarks are per-browser. If you open the dashboard on a different machine your personal bookmarks are not there. For a single-person homelab that is probably fine. For anything shared it is not. A small backend that reads and writes a JSON file on the server would fix it. A few lines of Python or Go. I did not build it because I did not need it, but it is the obvious next step.
The other thing I thought about is service status indicators. A dot on each card showing whether the service is actually reachable. CORS makes direct health checks from the browser unreliable so you would need a small proxy to do it cleanly. Maybe version two.
Get the Code
Free and open source. Edit config.js, run the Docker command, have a dashboard.
github.com/MichaelCoughlinAN/hiimmichael
The README has the full quickstart. If you run it or build on top of it I would like to see it.