Homedex: Inventaris Homelab Otomatis dari Docker sampai Sertifikat Kadaluarsa
Ada rasa tidak enak yang khas pada homelab: semua service dulu ada, lalu tahun berjalan ada yang mati, ada domain yang hampir kedaluwarsa, dan tidak ada satu pun daftar yang bisa dipercaya. Dua tahun kemudian, yang tersisa hanya halaman dashboard yang kalau dibuka bikin sesak napas.
Homedex adalah jawaban repo orang lain untuk masalah itu. Ini review terhadap proyek pihak ketiga milik Harsh Shah (github.com/HarshShah0203), bukan produk kami. Yang saya lakukan di sini adalah membaca source-nya lalu melaporkan apa yang benar-benar terjadi di dalam.
Ringkasnya: satu aplikasi Go yang memindai homelab Anda dari sebelas sumber, menyimpan hasilnya ke SQLite, lalu menampilkan lewat web UI. Klaim “read-only” di README bukan sekadar slogan. Di level kode, itu pilihan yang bisa diperiksa.
Sumber kodenya cukup serius untuk dibaca
Kondisi repo saat artikel ini ditulis: 297 file, 105 file .go (59 di antaranya file test), 19 komponen .svelte, dan 38 file TypeScript. Bahasa utama Go dengan go 1.23.0. Lisensi MIT, repo dipublikasikan 16 Juli 2026, commit terakhir 27 September 2026.
Rasio test yang tinggi itu bukan kebetulan. Dua file test terbesar justru milik konektor yang paling rawan gagal diam-diam: internal/connectors/proxmox/proxmox_test.go (36 KB) dan internal/connectors/tailscale/tailscale_test.go (34 KB).
Tiga dependensi di go.mod sudah menandai arsitekturnya:
github.com/docker/docker v27.5.1+incompatible
github.com/go-chi/chi/v5 v5.2.1
modernc.org/sqlite v1.36.1
modernc.org/sqlite adalah SQLite murni Go tanpa cgo, artinya image Docker bisa multi-arch tanpa compiler C. Detail kecil dengan dampak besar buat orang yang deploy di Raspberry Pi atau Odroid, yang justru mesin yang paling sering jadi tempat homelab hidup.
Sebelas konektor, satu kontrak
Arsitekturnya berlapis rapi. Setiap konektor hanya mengerjakan satu hal: memindai sumbernya lalu mengembalikan Snapshot. Pendaftarannya sendiri cuma satu baris di cmd/homedex/main.go:
for _, c := range []connectors.Connector{
docker.New(), traefik.New(), caddy.New(), npm.New(), nginx.New(),
tlsprobe.New(), rdap.New(), imageregistry.New(), sshexec.New(),
tailscale.New(), proxmox.New(),
} {
if err = registry.Register(c); err != nil {
return err
}
}
Sebelas konektor itu: caddy, docker, nginx, npm, proxmox, rdap, registry, sshexec, tailscale, tlsprobe, traefik. Yang paling menarik adalah rdap (mengambil tanggal kedaluwarsa domain lewat protokol RDAP), registry (membandingkan digest image di Docker Hub atau GHCR), dan tlsprobe (tanggal habis sertifikat).
Kontrak bersama di internal/connectors/connector.go dibuat ketat, dan alasannya ditulis langsung di komentar:
const MaxResponseBytes = 8 << 20
const DefaultTimeout = 30 * time.Second
Server RDAP yang dipakai ditemukan dari data bootstrap IANA, jadi “upstream” di sini tidak selalu pilihan administrator. Server yang dikendalikan penyerang tidak boleh bisa mengalirkan JSON tanpa batas sampai menghabiskan memori. Batas 8 MiB itu jauh di atas payload legit mana pun.
Identitas container: nama, bukan ID
Ini bagian yang paling layak dicatat, dan jelas penulisnya sudah pernah kena masalah yang sama. Di internal/connectors/docker/docker.go:
// The container name is the identity. It is unique per daemon and a recreate
// (image bump, or `up` after `down`) reuses it, whereas the container ID is
// new every time -- keying on the ID orphaned the notes and first_seen of
// anything that got redeployed. The compose service name cannot be the
// identity either: `--scale web=3` gives three live containers the same one.
Tiga identitas yang tampak mirip, tiga jawaban berbeda:
| Kandidat | Masalahnya |
|---|---|
| Container ID | Berubah tiap recreate, sehingga catatan dan first_seen selalu jadi yatim |
| Nama container | Unik per daemon, dan dipakai ulang setelah recreate |
| Nama service Compose | Enak dibaca, tapi tidak unik saat --scale web=3 |
Akhirnya nama container dipakai sebagai kunci natural, sementara nama service Compose disimpan sebagai label tampilan. Detail yang hanya ketahuan setelah penulisnya benar-benar kena bug-nya sendiri.
Dua detail kecil lain di file yang sama. Inspeksi container dibatasi maksimal delapan goroutine lewat semaphore, dan satu container yang gagal di-inspect membatalkan seluruh scan, bukan diam-diam dilewati.
Resolusi route yang jujur soal ketidakpastian
Ini bagian paling menarik secara teknik. Kalau reverse proxy menyebut upstream 10.0.0.5:8080, tool biasanya asal menebak container mana. internal/resolve/resolve.go punya strategi bertingkat yang lebih jujur:
func uniqueCandidate(candidates []candidate) (candidate, bool) {
byService := make(map[EntityRef]candidate, len(candidates))
for _, candidate := range candidates {
byService[candidate.service.Ref] = candidate
}
if len(byService) != 1 {
return candidate{}, false
}
...
}
Kalau kandidat lebih dari satu service, hasilnya bukan “pilih yang paling mirip”, melainkan ditandai broken. Aturannya tegas: satu-satunya cara memastikan adalah kandidat tunggal, kalau tidak maka tidak ada.
Port yang tidak bisa diverifikasi menurunkan tingkat keyakinan:
func confidenceForPort(verified bool) string {
if verified {
return "high"
}
return "medium"
}
Artinya kecocokan nama yang portnya tidak bisa dibuktikan hanya sampai medium, tidak pernah high. Untuk siapa pun yang pernah bingung karena dashboard menampilkan tautan yang salah, ini perbedaan besar.
Konsep yang paling tidak akan kepikiran tanpa homelab yang benar-benar berantakan: sebuah device Tailscale atau entri DNS bukan mesin baru, melainkan pandangan lain dari mesin yang sama. Kode itu ada di internal/domain/snapshot.go:
// HostKindDNS marks a DNS view of one address, as a local resolver
// (Pi-hole, AdGuard Home) answers it: Address is the IP and Aliases are
// the names that resolve to it. Like a tailnet device it is a view, not
// a machine; route resolution links it to the one machine reported at
// that exact address, never by name.
Pemisahan “mesin” dan “pandangan mesin” itu membuat route resolution bisa menyambung dua sumber berbeda tanpa menggandakan entri.
Keamanan: read-only yang memang dijaga
docker-compose.yml tidak mount socket Docker mentah ke aplikasi. Yang dipakai tecnativa/docker-socket-proxy dengan POST: 0, ALLOW_START: 0, ALLOW_STOP: 0, ALLOW_RESTARTS: 0, ditambah cap_drop: ALL dan no-new-privileges: true. Komentarnya jujur:
# `:ro` protects the socket file from replacement; POST=0 above is what
# filters mutating Docker API calls. A raw socket mounted read-only is
# still a privileged Docker API connection.
Read-only pada mount bukan read-only pada API. Itu benar, dan mengakui hal itu membuat saya lebih percaya pada sisanya.
Password admin di-hash dengan Argon2id di internal/auth/password.go, parameter t=3, m=64MiB, p=2, minimum 12 karakter, diverifikasi dengan subtle.ConstantTimeCompare. VerifyPassword juga menolak parameter berlebihan (memory > 256*1024 atau timeCost > 10), supaya hash yang ditanam orang tidak bisa dipakai sebagai serangan kehabisan sumber daya.
Secara bawaan aplikasi hanya bind ke loopback. Akses LAN harus dibuka lewat HOMEDEX_BIND=0.0.0.0, dan ada peringatan eksplisit kalau cookie non-secure dipakai di luar loopback:
if !secureCookies && !isLoopbackListen(*listen) {
slog.Warn("WARNING: HOMEDEX_SECURE_COOKIES is off and Homedex is not bound to loopback; ...")
}
Ribet seperti ini justru tanda penulis yang sadar risiko.
Kekurangan dan batasan
Beberapa hal nyata yang perlu Anda tahu sebelum menjatuhkan penilaian:
Komunitasnya masih kecil. 62 stars dan 1 fork pada saat review. Proyek ini muda, dibuat Juli 2026 dengan commit terakhir September 2026. Angka itu akan berubah saat Anda membaca, tapi jangan diambil sebagai benchmark.
Keunikan nama container belum tentu berlaku di semua setup. Kalau Anda punya banyak stack dengan nama container mirip, atau sering memakai docker rename, fondasi identitas yang diandalkan penulisnya bisa mulai bermasalah. Belum saya uji di sini.
Service yang hilang tidak langsung dihapus. Deteksi berhenti terlihat ditandai state='gone', bukan dihapus seketika. Penghapusan baru berjalan lewat PurgeGone dengan retensi bawaan 30 hari (HOMEDEX_GONE_RETENTION_DAYS). Ini pilihan yang benar untuk keperluan audit, tapi artinya dashboard bisa menaruh service mati lama sebagai “gone” selama sebulan. Bukan bug, tapi ekspektasinya perlu disesuaikan.
Read-only berarti ia tidak memperbaiki apa pun. Route yang benar-benar rusak akan ditandai broken dan dibiarkan. Ini alat visibilitas, bukan orkestrator.
Siapa yang cocok pakai
Cocok kalau Anda punya beberapa host dengan service yang bertumpuk, sering memindahkan stack, dan butuh satu halaman yang menunjukkan: apa yang berjalan, di mana, lewat jalur mana, dan domain mana yang mau habis. Sangat membantu untuk orang yang lelah mencatat manual.
Kurang cocok kalau Anda cuma punya satu Raspberry Pi dengan tiga container. Situasi itu belum cukup rumit untuk perlu inventaris, dan overhead SQLite plus web UI-nya jadi tidak sepadan.
Menyiapkan sebelas koneksi itu sendiri sudah jadi pekerjaan nyata: token Tailscale, token API Proxmox, kredensial, dan seterusnya.
Penutup
Yang membuat Homedex layak dicoba bukan jumlah konektornya, tapi konsistensinya dalam hal yang tidak enak dibaca: ia menolak menebak saat bukti tidak cukup. Ia tidak melaporkan pembaruan image untuk build lokal yang namanya kebetulan sama dengan image publik. Ia tidak mengklaim read-only yang tidak dipegang di file compose.
Saya belum menjalankannya di server saya sendiri, jadi semua penilaian performa dan kelemahannya di atas berasal dari pembacaan kode, bukan hasil pengujian.
Source lengkapnya ada di github.com/HarshShah0203/homedex, dengan demo langsung di harshshah0203.github.io/homedex. Lisensi MIT, jadi bebas dipakai dan dimodifikasi.