Server
Two processes run on the box: Caddy, the only public listener, and a small
dependency-free Node API bound to 127.0.0.1:8787 that Caddy reverse-proxies
to. The API serves the token-authed sync and
tools surfaces, answers Caddy's on-demand-TLS gate, and samples
the machine's health on a timer; Caddy serves the static sites straight off disk. There is no
configuration to speak of — every location has a fixed default and the only per-install state is one
secret. Everything on disc is namespaced under localhoster so several hoster services can
run side by side on one box: this service's own files sit under
/opt/localhoster/sitehoster/, beside any sibling service — including the hosted-sites tree
Caddy serves, at /opt/localhoster/sitehoster/sitehoster-sites/ — while the parts every
service shares (the access logs, the machine-health samples, and the bare-IP certificate) live once under
/srv/localhoster/ and /etc/localhoster/. The installer migrates the sites into
that location once, from the old /srv/sitehoster/.
/opt/localhoster/ every localhoster service on this box, side by side
sitehoster/ this service (a sibling like gamehoster/ sits beside it)
sitehoster-server-<version>/ all the sitehoster server code (one dir per installed version)
sitehoster-api.js the entry systemd runs: the localhost HTTP API
VERSION this package's version (matches the dir name)
localhoster-server/ shared server toolkit, identical across every hoster
logs-stats.js the read-only /logs + /stats routes
stats-recorder.js the loop that writes the stat day-files
stats.js one machine-health snapshot
sitehoster-sync/ sitehoster's own sync runtime
manifest.js Merkle tree + path-safety (its own copy; the sync carries a duplicate)
version.js the /api/vN protocol version
deploy/
install.sh the installer this box ran
uninstall.sh
start.sh
stop.sh
Caddyfile the Caddy config source
sitehoster-api.service the systemd unit source
sitehoster-server-current → sitehoster-server-<version>/ (the unit follows this; an upgrade flips it)
sitehoster-token this service's bearer token (mode 600)
sitehoster-sites/ the hosted sites — one folder per domain (Caddy serves this)
example.com/
index.html
/srv/localhoster/ box-wide runtime data, shared by every service
web-logs/ Caddy access logs (one Caddy fronts every domain)
access.log the active log, one JSON line per request
access-<timestamp>.log rotated, immutable (keyed by mtime, not name)
server-logs/ machine-health samples (the box, not any one service)
cpu-<YYYY>-<MM>-<DD>.log
memory-<YYYY>-<MM>-<DD>.log
disk-<YYYY>-<MM>-<DD>.log
diskio-<YYYY>-<MM>-<DD>.log
netio-<YYYY>-<MM>-<DD>.log
/etc/localhoster/ box-wide localhoster config, shared
localhoster-api-certificate.crt the self-signed cert for the bare-IP endpoint
localhoster-api-certificate.key its private key (mode 640 root:caddy)
/etc/caddy/
Caddyfile the Caddy config (serves every service's domains)
/etc/systemd/system/
sitehoster-api.service one unit per service, side by side
caddy.service the shared public listener (from the Caddy package)
Contents
Every path has a fixed default in the code or the service unit; the installer creates all of them and nothing lives outside them.
| Name | Type | Description |
|---|---|---|
/opt/localhoster/sitehoster/sitehoster-server-<version>/ | directory | The self-contained server package: the entry, the shared localhoster-server/ toolkit, sitehoster's own sitehoster-sync/, and the deploy scripts. |
/opt/localhoster/sitehoster/sitehoster-token | file | The one secret: the bearer token the whole API is gated on. Absent stops the API from starting. |
/opt/localhoster/sitehoster/sitehoster-sites/ | directory | The hosted sites, one folder per domain — what the sync writes and Caddy serves, in the localhoster namespace. |
/srv/localhoster/web-logs/ | directory | Caddy's combined JSON access log, active plus size-rotated files. |
/srv/localhoster/server-logs/ | directory | The server-health samples, one dated file per statistic type. |
/etc/localhoster/ | directory | The self-signed certificate for the bare-IP endpoint. |
/etc/caddy/Caddyfile | file | The Caddy config the installer lays down, plus a backup of any earlier one. |
/etc/systemd/system/ | directory | The service units — the sitehoster API, and Caddy's from its package. |
/opt/localhoster/sitehoster/sitehoster-server-<version>/
Each version installs into its own directory; the service
unit points at sitehoster-server-current/sitehoster-api.js, a symlink to the installed
sitehoster-server-<version>/, so an upgrade is atomic (the installer flips the symlink)
and the previous version stays on disc for rollback. The tree below is the whole self-contained package:
the entry, the shared localhoster-server/ toolkit (the read-only /logs +
/stats API and the health recorder), sitehoster's own sitehoster-sync/ (the
Merkle tree and path-safety, and the protocol version), and the deploy scripts. Every module is plain
Node with no dependencies — the API has no WebSocket half, so there is no node_modules and
the box needs only Node and Caddy. Nothing is copied alongside the package: it carries its own copies of
the shared server files rather than a common/ + shared/ tree.
sitehoster-server-<version>/
sitehoster-api.js the entry systemd runs (the localhost HTTP API)
VERSION this package's version (names the install dir)
localhoster-server/ shared server toolkit, identical across every hoster
logs-stats.js the read-only /logs + /stats routes
stats-recorder.js the loop that writes the stat day-files
stats.js one machine-health snapshot
sitehoster-sync/ sitehoster's own sync runtime
manifest.js Merkle tree + path-safety (its own copy; the sync carries a duplicate)
version.js the /api/vN protocol version
deploy/
install.sh the installer this box ran
uninstall.sh
start.sh
stop.sh
Caddyfile the Caddy config source
sitehoster-api.service the systemd unit source
| Name | Type | Description |
|---|---|---|
sitehoster-api.js | file | The API server and the process systemd starts. A localhost-only HTTP listener carrying the sync routes (/tree, /file), the read-only logs/stats routes, the version probe, and Caddy's on-demand-TLS gate (/internal/allow). It builds the content Merkle tree once at startup and keeps it in memory, reads the token once, and starts the stats recorder on listen(). |
localhoster-server/logs-stats.js | file | The read-only /logs and /stats routes (plus the /stats/now debug snapshot), identical across every hoster and mounted under the same token. |
localhoster-server/stats-recorder.js | file | Started by the API, it snapshots on the interval, appends a JSON line per type to the dated day-file, and prunes files past the retention window. |
localhoster-server/stats.js | file | Takes one snapshot of the machine's health — CPU, memory, disk, and cumulative disk- and network-IO counters. Common to every hoster. |
sitehoster-sync/manifest.js | file | The Merkle-tree builder and the path-safety helpers, plus the in-memory server tree. Its own copy of the algorithm the client carries too, so both compute identical hashes and agree on which paths are legal. |
sitehoster-sync/version.js | file | The single API/protocol version that forms the /api/vN path prefix; a request on a different version's path answers 426 (or the wall, unauthenticated). |
deploy/ | dir | The install and service scripts: install.sh, uninstall.sh, start.sh, stop.sh, the Caddyfile, and the sitehoster-api.service unit. |
/opt/localhoster/sitehoster/sitehoster-token
The whole per-install configuration is one secret. The installer generates it once
(openssl rand -hex 24, printed once) and the API reads it at startup; the sync and tools
client is handed the same value out of band. Every API call is gated on it, and the API refuses to
start without it. To rotate it, write a new value and restart the API. It is mode 600,
owned by the sitehoster user. It is preserved across upgrades and carried over from the
pre-localhoster /etc/sitehoster/token on the one-time migration.
/opt/localhoster/sitehoster/sitehoster-sites/
One directory per domain, named exactly for the hostname it serves; this is the tree the
sync mirrors up and Caddy serves from
(root * /opt/localhoster/sitehoster/sitehoster-sites/{host}). It sits in the
localhoster namespace beside the rest of the service; the installer migrates it there once
from the old /srv/sitehoster/ (a single atomic rename), then never bulk-moves or
bulk-chowns it again — on a fresh box it just creates it, empty. Adding a folder here and pointing the
domain's A record at the box is all it takes to go live — a certificate is issued on the first request.
It is owned by the sitehoster service user, and the API only ever writes inside it. The
server stores a flat list of domains; local group folders are flattened by the client before upload, so
nothing dotless or nested reaches here.
/opt/localhoster/sitehoster/sitehoster-sites/
example.com/
index.html
styles.css
www.example.com/ served, because a www. folder exists
blog.example.com/
.shtmp-a1b2c3 an in-flight upload, renamed into place atomically
Uploads stream to a hidden .shtmp-* temp file and are renamed atomically, so a
half-written file is never served (Caddy hides the pattern). Renaming a local folder to
_example.com removes it from here on the next sync — a one-rename "take it offline"
switch.
/srv/localhoster/web-logs/
Caddy writes one combined JSON access log covering every hosted domain and the proxied API, each line
tagged with its host. It rotates by size only (there is no daily rotation), so there is one active
file and a trail of size-rotated ones. The API exposes this directory read-only
(GET /api/v2/logs) and the client mirrors it down for the
Web Logs tool; the directory is mode 0770 group
caddy, which writes it, and the sitehoster user is added to the
caddy group so the API can read it.
/srv/localhoster/web-logs/
access.log the active log, one JSON line per request
access-<timestamp>.log rotated and immutable; the number is its rotation time in ms
access-<timestamp>.log
Rotation is set in the Caddyfile (roll_size 64MiB, roll_keep 50,
roll_keep_for 2160h). Rotated files are keyed to the client by mtime, never by name, so
filenames never cross the wire.
/srv/localhoster/server-logs/
The API process samples the machine's own health every 15 seconds and appends one JSON line per
statistic type to a dated, per-type day-file. A day-file is frozen the moment the UTC date rolls over
— no rename, no lock — and files older than 60 days are pruned. The API serves these read-only
(GET /api/v2/stats); the client mirrors each past day once and keeps it, which is the
data behind the Server Logs tool. The directory is mode
750, owned by the sitehoster user, and is the only path besides
/opt/localhoster/sitehoster/sitehoster-sites the service may write.
/srv/localhoster/server-logs/
cpu-<YYYY>-<MM>-<DD>.log per-core cumulative CPU times
memory-<YYYY>-<MM>-<DD>.log total / used / available / free bytes
disk-<YYYY>-<MM>-<DD>.log usage of the filesystem holding /
diskio-<YYYY>-<MM>-<DD>.log cumulative bytes read / written
netio-<YYYY>-<MM>-<DD>.log cumulative bytes in / out
cpu-<YYYY>-<MM>-<DD>.log yesterday's files, immutable until the retention cutoff
Rates are never stored: the counter fields are cumulative, so the reader diffs two samples and divides by the true elapsed time — a gap after downtime averages out instead of spiking, and a reboot's counter reset is dropped. Metrics a host can't report (e.g. disk/net IO off Linux) are simply absent.
/etc/localhoster/
A self-signed certificate for the bare-IP endpoint, generated once by the installer
(CN=sitehoster, ten-year life) and shared by every localhoster service. Caddy obtains real
certificates on demand for the hosted domains; this one only completes the handshake for a no-SNI /
bare-IP connection, selected via the Caddyfile's default_sni. It is what lets the local
tool reach the API by the server's IP — it skips verification for an IP endpoint, and the
token is what actually authenticates it.
/etc/localhoster/
localhoster-api-certificate.crt self-signed cert for the bare-IP endpoint
localhoster-api-certificate.key its private key (mode 640 root:caddy)
/etc/caddy/Caddyfile
The Caddyfile is fully static — no environment placeholders. It terminates TLS, obtains real certs on
demand (gated by a localhost call to /internal/allow, so issuance can't be forced for
domains you don't host), redirects HTTP → HTTPS and
www.X → X, serves the bare-IP block from the self-signed cert,
reverse-proxies IP-addressed requests to the API on 127.0.0.1:8787, and serves everything
else from /opt/localhoster/sitehoster/sitehoster-sites/{host}. The installer copies it from the source
deploy/Caddyfile, backing up any file already there once.
/etc/caddy/
Caddyfile our config, installed from deploy/Caddyfile
Caddyfile.pre-sitehoster.<ts> a one-time backup of any earlier Caddyfile
/etc/systemd/system/
Two units run at boot. sitehoster ships its own; Caddy's comes from its package (the installer
requires Caddy to be installed from a package so this unit exists) and the installer just enables and
restarts it. The API unit runs as the unprivileged sitehoster user with
NoNewPrivileges, ProtectSystem=strict and ProtectHome, and may
write only /opt/localhoster/sitehoster/sitehoster-sites and /srv/localhoster/server-logs. It has no
EnvironmentFile — the process reads its one secret from
/opt/localhoster/sitehoster/sitehoster-token and takes everything else from fixed
Environment= lines (bind address, port, the content root, and the log/stats dirs). It is
added to the caddy supplementary group so it can read the access logs.
/etc/systemd/system/
sitehoster-api.service the localhost API — runs /opt/localhoster/sitehoster/sitehoster-server-<version>/sitehoster-api.js
caddy.service the public listener, from the Caddy package