Skip to content

Repository files navigation

🏠 home-hosted

The control panel for everything you self-host at home.

The harness for your servers.

Point it at the things you run — a gateway, a media server, a bot, a database — and it starts them, watches them, restarts what dies, and shows you one page of what is going on. Or BYOU, for a specialized UI that fits you exactly.

npm Downloads CI License Node

🚀 Quick start · 🤖 Agents & API · ✨ Features · 🧩 Servers · 🛠 CLI · 🔔 Notifications · 🎨 BYOU


Six views of the panel: the stock UI, the NOC-console, and UI examples

The stock panel, the NOC-console that ships alongside it (for TUI and shortcuts wizards), and UI directions you could build yourself — BYOU.


🤔 Why?

You run a handful of services at home. The usual choices are extremes — 🧟 tmux sessions you forget about, 📜 a hand-written systemd unit per service (times six), or 🐳 a whole docker/k8s stack??? - too extreme! — plus 😩 monitoring, rebooting and changing the host machine, yuck!

🙂✨ home-hosted enhances on top: a panel/supervisor that starts them, watches them, restarts what dies, and puts the whole stack on one page, with deep backup support — whether a server is a plain command or a docker compose stack.

                   ┌──────────────────────────────────────┐
   your browser ──▶│  home-hosted  ·  127.0.0.1:3999      │
                   │  your UI + JSON API + SSE logs       │
                   └───────────────┬──────────────────────┘
                                   │  supervises
        ┌──────────────────────────┼──────────────────────────┐
        ▼                          ▼                          ▼
   ┌─────────┐               ┌─────────┐                ┌─────────┐
   │ gateway │               │ files   │                │ bot     │
   │ :4000   │               │ :4010   │                │  ...    │
   └────┬────┘               └─────────┘                └─────────┘
        │ compose up -d
        ▼
   ┌────────────┬────────────┬────────────┐
   │  gateway   │  postgres  │   redis    │          restarts ↻
   └────────────┴────────────┴────────────┘
     health ✓ (the published port is the probe)
❌ "Is it still running?" Health, CPU/mem, uptime and live logs per server — no ps, no curl, no hope
❌ Silent deaths Restarted automatically, and the panel or Telegram notifies
❌ Fragile reboots autostart brings the stack back; one down stops it all cleanly
❌ "Move it to the new box" One archive: config and data, restored on a blank host
✅ home-hosted Declare it once, watch it forever, one command to stop it all

It ships with nothing: no blessed paths, no opinion about what you run — a server is a command, some arguments, and the environment you give it:

{ "id": "gateway", "command": "node", "args": ["server.js"], "port": 4000, "autostart": true }

Manage them from the panel, home-hosted start|stop <id> from a shell, home-hosted status --json, or GET /api/state; GET /healthz is the same status line for your own monitor, no session needed.


⚡ Quick start

npx home-hosted            # start it — detached, it stays running
npx home-hosted status     # where is it, is it healthy
# pnpm instead of npx: `pnpm dlx home-hosted …`

That is it. The panel is on http://127.0.0.1:3999 and keeps running after the terminal closes. Needs Node 24 or newer. Stop it whenever you like:

npx home-hosted down       # stops the panel *and* everything it started

Note

The first boot writes a default password (hh) so the panel is never unprotected. Change it under Settings → Authentication — binding beyond 127.0.0.1 stays refused until you do.

📦 Install it instead of npx-ing it
npm install -g home-hosted      # or: pnpm add -g home-hosted
home-hosted up                  # `hh up` does the same

Everything it owns — config, secrets, logs, TLS, backups — lives in $HHOSTED_HOME, default ~/.home-hosted. Delete that and nothing of yours is left behind.

An installed home-hosted answers to hh too — hh up, hh status, hh down. Only the installed form gets it; npx stays npx home-hosted ….

📁 Keep a whole setup in a project you can take anywhere

Commit the project and it is the setup. init scaffolds it:

npx home-hosted init            # or: pnpm dlx home-hosted init
#   project directory, package name, package manager, install, git — every step has a default
#   `--yes` takes them all, for an agent or a CI job

It writes a manifest whose scripts all pass --home ./state:

// package.json
{
  "scripts": {
    "up": "home-hosted up --home ./state",
    "down": "home-hosted down --home ./state",
    "status": "home-hosted status --home ./state"
    // …and restart, set-password, set-token, migrate
  }
}
node_modules/
state/*                       # secrets, logs, TLS keys and archives stay local…
!state/servers.config.json    # …but the server definitions are committed
data/                         # per-server data directories, declared through dataEnvs

One clone, pnpm install --frozen-lockfile, pnpm run up — the setup is up on any machine with Node. Worked example, with per-server data inside the project: hhosted-ai-pack.

Call them as pnpm run up — pnpm up is pnpm's own update, not your script.

[!TIP] Give a project its own panel port (control.port, e.g. 4399) — the default 3999 is what the global instance and every other project also want.

🧰 Run it under systemd or Docker (no daemon needed)
home-hosted up --foreground     # stays in the foreground, logs to stderr
[Unit]
Description=home-hosted
[Service]
ExecStart=/usr/local/bin/home-hosted up --foreground
Environment=HHOSTED_HOME=/srv/home-hosted
Restart=always
[Install]
WantedBy=multi-user.target

🤖 Agents, scripts and tools

Everything the panel does, a script or an agent can do: the API takes a long-lived token in place of the browser cookie. One command for a credential, then plain HTTP to start, stop, inspect, restart and read logs.

home-hosted set-token --generate
#   hh_9uA2…                     (printed once; only its hash is kept, mode 0600)

curl -H "Authorization: Bearer hh_9uA2…" http://127.0.0.1:3999/api/state
curl -H "Authorization: Bearer hh_9uA2…" -X POST http://127.0.0.1:3999/api/servers/omniroute/restart
curl -N -H "Authorization: Bearer hh_9uA2…" 'http://127.0.0.1:3999/api/events?serverId=omniroute'   # SSE
home-hosted status --json        # machine-readable: pid, url, health, paths

A token has the same authority as a signed-in browser and outlives restarts; set-token --clear revokes it instantly.

🌐 The endpoints worth knowing
GET /api/state the full snapshot: config, live status, host vitals
GET /api/events SSE: the live state, plus logs (?logs=0, ?serverId=…)
GET /api/servers/:id/stream SSE: one server's state and logs
POST /api/servers/:id/{start,stop,restart} lifecycle
POST /api/servers/:id/free-port ask whatever holds that server's port to stop
PATCH /api/servers/:id, PATCH /api/settings edit configuration
GET /api/logs, /api/backups history and archives
PUT/DELETE /api/notifications/token, POST /api/notifications/{test,detect-chats} the bot credential, a test send
GET /healthz no session needed — the one an external monitor wants (its per-server detail needs a credential)
GET /api/metrics Prometheus text (needs a token or session, like every /api route)

GET /openapi/spec.json describes all of it, /openapi/ui is the browsable version, and every error comes back as one envelope ({ message, code, detail }) with a stable code a tool can branch on.

🧑‍💻 Pointing an agent at it

Give the agent four things and it can run your home server without guessing:

  1. the token (home-hosted set-token --generate),
  2. http://127.0.0.1:3999/openapi/spec.json — the API it may call,
  3. home-hosted status --json — where things are,
  4. SERVERS.md — how an entry is declared when it needs a new server.

For a UI rather than the API, UI_CREATION.md is the whole contract, and the panel can be told what to be: "Help me build a UI for home-hosted: nostalgic game theme, including …".


✨ Features

🚦 Lifecycle Start, stop, restart from the panel or the API; autostart entries come up with it.
📝 Hand edits welcome Change servers.config.json in an editor, a git checkout or a config tool: the panel notices within seconds, no restart. A file it cannot read is reported in the panel, and the running servers are left alone.
♻️ Auto-restart Exponential backoff on crash, with the counter reset once a process stays up.
🩺 Health that acts TCP or HTTP probes per server: warn on the card, force a restart after a timeout, check ports before starting — and follow or replace a program that restarts itself.
🔗 Ordered startup dependsOn waits for a dependency to be healthy — not merely spawned — and stops in reverse.
📜 Logs Live per-server stream, buffer plus rotated files on disk, search, download, one click to clear.
📈 Resources CPU and RSS of the whole process tree, with an optional memory ceiling that triggers a restart.
🌡️ Host vitals Load, memory, swap, disk and CPU temperature, with thresholds that notify once and again on recovery.
🤖 Token API Scripts and agents drive it with Authorization: Bearer — no browser, no session. ↑
🔔 Notifications Telegram on crash, unhealthy, forced restart, recovery and host thresholds — setup here.
💾 Backups One click for config, secrets, TLS and your declared data directories — plain .zip, or AES-256 with a password, restored per path.
🎨 BYOU — Bring Your Own UI Upload a static build, ui-update to follow its releases, ui-revert to go back. UI_CREATION.md
🔐 Security Cookie sessions, API tokens, scrypt hashes, per-IP lockout, optional TLS, and a refusal to expose itself without a password.
🧩 No special treatment A server is command + args + env + cwd; nothing is built in for any particular app.
🖥 Cross-platform Linux, macOS and Windows: /proc, ps or Win32_Process, process groups or taskkill /T, no shell dependencies.

🧩 Servers

An entry is a few lines. Add one with ➕ Add server, or write it into servers.config.json:

{
  "id": "myapp",
  "command": "node",
  "args": ["server.js"],
  "port": 8080,
  "autostart": true,
  "dataEnvs": { "DATA_DIR": "{projectDir}/data/myapp" }
}

dataEnvs declares a data directory once: it is exported to the process and picked up by Backups. Every field, every placeholder, the port-conflict policies (including adopting a server that restarts itself), and how hand-edits are validated: SERVERS.md.


🛠 CLI

command
home-hosted up start the panel detached, and keep it alive in the background
home-hosted down stop it cleanly — supervised processes included, persistent entries left running
home-hosted restart down, then up
home-hosted status pid, URL, health, uptime, state and log paths (--json for scripts)
home-hosted start <id> start one server — and anything it dependsOn
home-hosted stop <id> stop one server, nothing else
home-hosted set-password set the panel password without opening a browser
home-hosted set-token set the API token scripts and agents use (--generate, --clear)
home-hosted migrate bring servers.config.json up to this release's schema (--dry-run, --yes)
home-hosted init scaffold a project that keeps state/ and its data in the repo
home-hosted ui-switch install a UI from a release asset, a zip file or a URL (interactive)
home-hosted ui-update bring an installed UI up to date, or pick a release (--old, --check)
home-hosted ui-revert go back to the stock panel UI after uploading your own
⚙️ Flags

home-hosted <command> --help prints what that command takes.

up, restart       -c/--config -p/--port --host --open --no-autostart --foreground --print-config

down              (no flags)
status            --json
start, stop       <id>   (the server's id in servers.config.json; both need the panel up)
init              --dir --name --pm --no-install -y/--yes
set-password      --clear
set-token         --generate --clear
migrate           --config --dry-run -y/--yes
ui-switch         --repo --tag --asset --file --list --token -y/--yes
ui-update         --check --tag --asset --old --repo --token -y/--yes
ui-revert         (no flags)

every command     --home <dir> --project <dir>       (or $HHOSTED_HOME, $HHOSTED_PROJECT)
env vars          HHOSTED_PASSWORD, HHOSTED_MIGRATE=allow, HHOSTED_TOKEN, GITHUB_TOKEN or GH_TOKEN

up and restart share the same flags: restart is down, then up with exactly what it was given.

🧭 Upgrading, and why the panel sometimes refuses to start

servers.config.json records what wrote it: meta.writtenBy (the release) and meta.schema (the config shape). That buys two guarantees:

  • A newer home-hosted always reads an older config — every existing key keeps its meaning.
  • Keys a newer release added are ignored, not fatal. The panel names them in its log, leaves them in the file, and never resets the settings around them.

What it will not do is run a config it cannot read. A wrong value, a duplicate id, an unreadable file or a config whose schema is newer than the running release stops up with the exact problem, rather than starting with defaults that quietly differ from your file. Fix the file, or install the release that wrote it.

When a release changes the shape itself, home-hosted migrate applies the steps it ships:

home-hosted migrate --dry-run   # print the steps, write nothing
home-hosted migrate             # ask, then write — keeps servers.config.json.bak

Unattended, consent comes from --yes or HHOSTED_MIGRATE=allow; without it the command stops.


🔐 Security

Everything binds 127.0.0.1 until you say otherwise.

  • A password is required to expose the panel. Replace the default, then bind to lan — in the UI, in the config, or with --host lan. The same guard applies in all three places.
  • Sessions live in memory only; the cookie is HttpOnly and SameSite=Strict, and the login route locks out repeated failures per IP.
  • API tokens for scripts and agents: home-hosted set-token --generate prints one once, and a request proves itself with Authorization: Bearer … — the same access as a signed-in browser, stored as a SHA-256 hash, revoked with set-token --clear.
  • Port conflicts are named — port 4010 is already in use (pid 4242) — and can be resolved from a confirmation popover on that banner or card. The process is looked up again at that moment, never taken from the message, and anything the panel supervises is refused, not killed. A server that restarts itself can be followed, or replaced with a supervised copy.
  • Secrets never enter the config: the password hash, the API token hash and the Telegram bot token live in $HHOSTED_HOME/.control-secrets.json with mode 0600; the TLS pair in .tls/.
  • Behind a proxy turn on trustProxy and let cookieSecure: auto add Secure on https, or upload a PEM pair and let home-hosted terminate TLS itself.

🔔 Notifications

Telegram, when something happens while you are not looking: a server that gave up restarting, a failing health check, a forced restart, a recovery, or a host threshold (disk, memory, swap, load, temperature). Opt-in, rate-limited per server and reason, and the bot token stays in the secrets file. Two minutes of setup: NOTIFICATIONS.md.


💾 Backups

Settings → Backups archives the config, secrets, TLS pair and every data directory your entries declare — an ordinary .zip, or WinZip AES-256 with a password, restored per path. Known build output and dependency directories (node_modules, dist, .next, framework caches) are skipped per entry; backupIgnoreGenerated: false captures them anyway.

🚚 One archive is a whole setup

Start a blank home-hosted anywhere — another machine, another user, a fresh container — upload the archive and restore. Definitions come back, data lands where this machine's config says, and autostart entries come up immediately.

It works because an archive carries its own servers.config.json and paths are matched by the declaration (omniroute:DATA_DIR), not by an absolute path from the source machine. A restore never writes where no config declares.


🎨 Bring your own UI (BYOU)

The panel is a static site: $HHOSTED_HOME/.ui overrides the packaged one, and Settings → Interface takes a zip. No restart, no fork — and home-hosted ui-revert brings back the stock panel if yours breaks.

Two ship in this repo: uis/stock, and uis/noc-console for TUI and shortcuts wizards; a release attaches both as home-hosted-ui-<name>.zip. Yours can be anything that compiles to static files — the server never cares what built it.

Install a UI from the CLI: home-hosted ui-switch — with no flags it fetches the official asset built for this release. An official UI keeps itself paired with the panel: upgrade the panel and the next up re-installs the matching asset. Someone else's UI declares its own repo/asset in ui.json, and home-hosted ui-update offers its newer releases to pick from — --old for older ones.

🤖 Or have an agent build the UI you actually want

The whole contract fits in one file, so a coding agent can do this. Point it at this repo and be specific:

Help me build a UI for home-hosted: nostalgic game theme, including … features.

UI_CREATION.md has the endpoints, the SSE frames, the auth rules and a checklist.


❓ FAQ

Is it a systemd replacement?

No — it is a friendlier layer for the handful of things you run yourself. Keep systemd for the panel itself (--foreground) and for system services; use home-hosted for the rest.

What happens to my servers when the panel stops?

home-hosted down stops them — that is the point of the command. SIGTERM/SIGINT are handled the same way: every supervised process tree is stopped before the panel exits.

An entry marked persistent is the exception, and down names it instead of stopping it: it runs under its own nanny process, keeps logging, and is reattached by the next panel (SERVERS.md).

Nothing starts and the port is busy

A supervised server whose port is taken is reported rather than started over — the panel names the holder and offers to free it, and a program that restarts itself can be followed or reclaimed instead (SERVERS.md). The control port itself is checked before the listener is opened.

Where is my state?

$HHOSTED_HOME, default ~/.home-hosted:

servers.config.json          your servers, plus meta: which release and schema wrote it
servers.config.schema.json   regenerated on every start, for editor autocomplete
.control-secrets.json        password hash + API token hash + Telegram token (mode 0600)
.logs/                       rotated per-server logs + history
.tls/                        an uploaded PEM pair
.backups/                    zip archives
run.json                     the running panel (pid, url, token, mode 0600)

home-hosted status prints the paths.

Which ports does it use?

Just the control panel, 3999 by default. Supervised servers use the ports you give them.

Windows support, really?

Yes. Process trees are sampled from Win32_Process, termination uses taskkill /T, and the shipped examples avoid POSIX-only commands. CPU temperature and swap are best-effort where the OS does not expose them to an unprivileged process, and adopting a self-restarted process is Linux/macOS only.


🗂 Working on it

src/            control plane: config, supervisor, API, providers, services
src/cli.ts      the command line; one file per command under src/cli/
src/index.ts    the control plane itself, used by `up --foreground`
uis/            UIs: `stock` (shipped in the package) and alternatives — any framework, static output
bin/            the published entry point
docs/           topic docs, UI examples and the README's media
scripts/        builds, typechecks, media capture, release helpers
test/           the vitest suite

pnpm dev runs the panel with tsx watch plus the stock UI's dev server (state goes to .dev-state/); pnpm build produces dist/ and uis/stock/dist/; pnpm quickcheck is lint plus types; pnpm test is vitest; pnpm run media regenerates the GIF above.

📚 Which doc do I need?
if you want to… read
declare a server: every field, placeholders, port conflicts SERVERS.md
get Telegram alerts working end to end NOTIFICATIONS.md
build a UI against the API UI_CREATION.md
change the internals: architecture and the rules AGENTS.md
poke the live API on your own panel /openapi/ui

🔗 Interesting resources

+ PR to add yours


MIT

About

Self-hosted control panel for your home server: supervises processes with restarts, port-conflict handling, health checks, logs and Telegram alerts — and a UI you can replace.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages