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)
Everything on disc: this service under /opt/localhoster/sitehoster/ (the sites included, at sitehoster-sites/), and the box-wide logs, stats, cert and Caddy shared under localhoster.

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.

NameTypeDescription
/opt/localhoster/sitehoster/sitehoster-server-<version>/directoryThe self-contained server package: the entry, the shared localhoster-server/ toolkit, sitehoster's own sitehoster-sync/, and the deploy scripts.
/opt/localhoster/sitehoster/sitehoster-tokenfileThe one secret: the bearer token the whole API is gated on. Absent stops the API from starting.
/opt/localhoster/sitehoster/sitehoster-sites/directoryThe hosted sites, one folder per domain — what the sync writes and Caddy serves, in the localhoster namespace.
/srv/localhoster/web-logs/directoryCaddy's combined JSON access log, active plus size-rotated files.
/srv/localhoster/server-logs/directoryThe server-health samples, one dated file per statistic type.
/etc/localhoster/directoryThe self-signed certificate for the bare-IP endpoint.
/etc/caddy/CaddyfilefileThe Caddy config the installer lays down, plus a backup of any earlier one.
/etc/systemd/system/directoryThe 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
The self-contained server package, unrolled file by file.
NameTypeDescription
sitehoster-api.jsfileThe 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.jsfileThe 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.jsfileStarted 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.jsfileTakes one snapshot of the machine's health — CPU, memory, disk, and cumulative disk- and network-IO counters. Common to every hoster.
sitehoster-sync/manifest.jsfileThe 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.jsfileThe 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/dirThe 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
The hosted sites, one folder per domain.

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
Caddy's active and rotated access logs.

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
One dated file per statistic, per day.

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)
The bare-IP certificate.

/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
The Caddy config, and a backup of any earlier one.

/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
The two service units.