Overview

I’ve come across Homer in my homelabbing adventures. It’s great for navigating the various services you can run in a homelab, and I plan on using it to organize the links I use for work and share it with my team.

The documented methods on github to get started running homer are:

  1. Use docker
  2. Unzip the prebuilt tarball and host the assets with an HTTP server

Running docker on a Mac just for this felt a bit heavy-handed, so I chose to serve the assets with an HTTP server instead. Of the documented HTTP server options, the GitHub docs suggest using pnpm http-server, python -m http.server, or any web server. Since I work with Go a lot and it’s lightweight, I decided to use a Go HTTP server.

Serving static assets in Go is straightforward. Most boilerplate examples suggest something like the following:

package main

import (
	"log"
	"net/http"
)

const (
	address = "localhost:1323"
	dir     = "./docs"
)

func main() {
	fs := http.FileServer(http.Dir(dir))
	http.Handle("/", fs)

	log.Printf("Listening on %s...\n", address)
	err := http.ListenAndServe(address, nil)
	if err != nil {
		log.Fatal(err)
	}
}

Troubleshooting

Running the Go HTTP server didn’t work out of the box. The dashboard kept redirecting to itself for a bit before stalling. Switching between python3 -m http.server --bind localhost --directory ./docs and running go run main.go, the difference seemed to stem from requests to /index.html.

The Homer dashboard relies on HEAD requests to /index.html with a timestamp query parameter for cache invalidation. As of Go 1.24.3, the HTTP File Server implementation redirects requests ending in index.html to / (docs link); truncating the timestamp and causing the Homer dashboard to load improperly.

Adding the following handler to override the default index.html behavior allows the Homer dashboard to load correctly:

http.HandleFunc("/index.html", func(w http.ResponseWriter, r *http.Request) {
    if r.Method == "HEAD" {
        f, err := os.Open(path.Join(dir, "index.html"))
        if err != nil {
            panic(err)
        }
        http.ServeContent(w, r, "index.html", time.Now(), f)
    } else {
        http.Redirect(w, r, "/", http.StatusMovedPermanently)
    }
})

Final solution

Putting it all together, here’s a complete Go HTTP server that can serve the Homer dashboard:

package main

import (
	"log"
	"net/http"
	"os"
	"path"
	"time"
)

const (
	address = "localhost:1323"
	dir     = "./docs"
)

func main() {
	fs := http.FileServer(http.Dir(dir))
	http.Handle("/", fs)
	http.HandleFunc("/index.html", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "HEAD" {
			f, err := os.Open(path.Join(dir, "index.html"))
			if err != nil {
				panic(err)
			}
			http.ServeContent(w, r, "index.html", time.Now(), f)
		} else {
			http.Redirect(w, r, "/", http.StatusMovedPermanently)
		}
	})

	log.Printf("Listening on %s...\n", address)
	err := http.ListenAndServe(address, nil)
	if err != nil {
		log.Fatal(err)
	}
}