Server
Two processes run on the box: Caddy, the only public listener, and a small dependency-free Node API bound to localhost that Caddy reverse-proxies to (it also samples the machine's health on a timer). There is no configuration to speak of — every location is fixed in the code, and the only per-install state is one secret. Everything sitehoster puts on disk lives under four roots.
/srv/
sitehoster/ the hosted sites, one folder per domain
sitehoster-logs/ Caddy's access logs
sitehoster-server-stats/ server-health samples
/etc/
sitehoster/ the API token and the TLS cert
caddy/
Caddyfile the Caddy config
systemd/
system/ the service units
/opt/
sitehoster/
<version>/ the server code
Contents
Every path is fixed in the code; the installer creates all of them and nothing lives outside them.
| Name | Type | Description |
|---|---|---|
/srv/sitehoster/ | directory | The hosted sites, one folder per domain — what the sync writes and Caddy serves. |
/srv/sitehoster-logs/ | directory | Caddy's combined JSON access log, active plus size-rotated files. |
/srv/sitehoster-server-stats/ | directory | The server-health samples, one dated file per statistic type. |
/etc/sitehoster/ | directory | The one secret (the API token) and the self-signed bare-IP certificate. |
/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/sitehoster/<version>/ | directory | The program itself: every source file the server runs, under a versioned folder. |
/srv/sitehoster/
One directory per domain, named exactly for the hostname it serves; this is the tree the
sync mirrors up and Caddy serves from
(root * /srv/sitehoster/{host}). 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.
/srv/sitehoster/
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/sitehoster-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/v1/logs) and the client mirrors it down; the directory is group-readable by
caddy, which writes it, and by the sitehoster user, which reads it.
/srv/sitehoster-logs/
access.log the active log, one JSON line per request
access-1784514159284.log rotated and immutable; the number is its rotation time in ms
access-1784370489663.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/sitehoster-server-stats/
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/v1/stats); the client mirrors each past day once and keeps it, which is the
data behind the Server Logs tool.
/srv/sitehoster-server-stats/
cpu-2026-07-20.log per-core cumulative CPU times
memory-2026-07-20.log total / used / available / free bytes
disk-2026-07-20.log usage of the filesystem holding /
diskio-2026-07-20.log cumulative bytes read / written
netio-2026-07-20.log cumulative bytes in / out
cpu-2026-07-19.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/sitehoster/
The whole per-install configuration is one secret plus a self-signed certificate. The token is read
once at startup and never put in an environment variable (which leaks via /proc); the API
refuses to run without it. To rotate it, write a new value and restart the API. The certificate only
completes the handshake for IP connections — the token is what authenticates you.
/etc/sitehoster/
token the bearer token, mode 640 root:sitehoster
tls.crt self-signed cert for the bare-IP endpoint
tls.key its private key
/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 the API,
so issuance can't be forced for domains you don't host), redirects HTTP → HTTPS and
www.X → X, reverse-proxies IP-addressed requests to
127.0.0.1:8787, and serves everything else from /srv/sitehoster/{host}. The
installer copies it from the source server/Caddyfile, backing up any file already there
once.
/etc/caddy/
Caddyfile our config, installed from server/Caddyfile
Caddyfile.pre-sitehoster 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=full and PrivateTmp, and may
write only /srv/sitehoster and /srv/sitehoster-server-stats. It has no
EnvironmentFile — the process reads its one secret from /etc/sitehoster/token
and hardcodes everything else.
/etc/systemd/system/
sitehoster-api.service the localhost API — runs /opt/sitehoster/<version>/server/api.js
caddy.service the public listener, from the Caddy package
/opt/sitehoster/<version>/
The program installs under a versioned directory; the systemd unit points at the exact version, and
the two newest are kept so an upgrade can roll back. The tree below is every source file the running
server loads — the API entry point and the modules it imports. The installer copies the whole portable
shared/ and common/ trees verbatim (they are shared unchanged across the
hoster family), but only the files listed here are executed on the server; the rest of
common/ is client and tools code that merely rides along. Every module is plain Node with
no dependencies.
/opt/sitehoster/
0.2.0/ the current version; the previous is kept for rollback
server/
api.js the API server, and the entry point systemd runs
Caddyfile source of the config installed to /etc/caddy/
shared/
manifest.js Merkle tree and path-safety, shared with the client
version.js the /api/vN protocol version
stats.js one machine-health snapshot
stats-recorder.js the loop that writes the stat day-files
common/
server/
logs-stats.js the read-only /logs and /stats routes
uninstall.sh the full-teardown script, deposited so it runs without the bundle
| Name | Type | Description |
|---|---|---|
server/api.js | file | The API server and the process systemd starts. A localhost-only HTTP listener carrying the sync routes, the read-only logs/stats routes, the version probe, and Caddy's on-demand-TLS gate. Reads the token once at startup and starts the stats recorder on listen(). |
server/Caddyfile | file | The production Caddy config, source of the copy at /etc/caddy/Caddyfile. |
shared/manifest.js | file | The Merkle-tree builder and the path-safety helpers, shared with the client so both compute identical hashes and agree on which paths are legal. |
shared/version.js | file | The single API/protocol version that forms the /api/vN path prefix; a mismatch lands on the uniform 404. |
shared/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. |
shared/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. |
common/server/logs-stats.js | file | The read-only /logs and /stats routes (plus the /stats/now debug snapshot), identical across every hoster. |
uninstall.sh | file | The full-teardown script the installer deposits at /opt/sitehoster/uninstall.sh so it can be run later without the bundle. |
Also copied under the version dir but not run by the API: shared/directives.js
(used by the client's automation) and the rest of common/ (net.js,
browse/, client/) — the shared client and tools toolkit, carried so one
bundle serves every role.