9.2 KiB
Production operation
npm run prod is the everyday way to run RealHermes, especially with a headset. It builds once, then serves the built app and /api from one loopback origin (default http://127.0.0.1:5173). It starts and owns a headless Hermes audio backend and, optionally, keeps a reverse SSH tunnel to the headset. There is no Vite dev server, hot reload or proxy. App behavior is the same as in development.
Before you start
- Dependencies are installed with
npm ci(including development dependencies, which the build and thetsxruntime need). HERMES_PYTHONpoints at the Python interpreter inside your Hermes installation, or that interpreter ispython3onPATH. The bootstrap needs Hermes' dashboard and audio dependencies and configured speech providers.npm run doctorchecks that it can importhermes_bootstrap.- The Hermes gateway is running and managed by you. Put
HERMES_API_KEY(orAPI_SERVER_KEY) and, if needed,HERMES_CHAT_BASE_URLin your shell or.env. Without a key the app and audio still work, but chat returns 503. A configured key is not proof that the gateway is reachable. - The app port and the audio port are free and different. The defaults, 5173 and 9119, are the same ones
npm run dev:fulluses, so stop your development servers first or choose other ports.
| Variable | Default | Purpose |
|---|---|---|
REALHERMES_PORT |
5173 |
Loopback port for the app and API |
REALHERMES_AUDIO_PORT |
9119 |
Loopback port of the owned audio backend |
HERMES_PYTHON |
python3 |
Interpreter with Hermes installed |
REALHERMES_TUNNEL |
0 |
1 enables the reverse SSH tunnel |
REALHERMES_SSH_ALIAS |
frame |
SSH alias, hostname or IPv4 address of the headset |
REALHERMES_FRAME_PORT |
5173 |
Loopback port on the headset |
REALHERMES_LOG_FILE |
~/.hermes/logs/realhermes-relay.log |
The relay's sanitized artifact diagnostics (JSON lines) |
Approved folders and Poppler are configured as in development; see configuration.md. They reach only the app process, never the audio child or SSH.
Start and readiness
npm run prod
# with automatic headset reconnects:
REALHERMES_TUNNEL=1 npm run prod
The command builds, then runs the launcher in the foreground; keep the terminal open. To restart an already built version without rebuilding, run node scripts/production.mjs from the repository root.
The launcher:
- validates its settings and checks both ports, refusing to start (and stopping nothing) if either is busy;
- generates a new random audio session token, kept only in memory and in the two child environments;
- starts exactly two processes, the Hermes audio backend and the production server;
- waits up to 30 seconds for real local checks:
/api/readymust report a running app with audio configured, and the audio backend's/api/host/identitymust accept the session token; - prints
Local services ready: http://127.0.0.1:<port>; chat configured|not configured; audio configured; upstream connectivity not probed.
This proves that the local processes are up and that the token is accepted. It does not prove that the speech providers, the chat gateway or a live voice turn work. If readiness fails, or any required child exits (even with status 0), the launcher stops what it started and exits with status 1.
Connecting a headset over SSH
The headset reaches the app at its own loopback address through a reverse SSH tunnel. That keeps the browser in a secure context, which microphone access and WebXR require, without exposing anything to your network.
-
Enable an SSH server on the headset using its operating system's developer options, and make sure you can log in from the host with a key.
-
Add an alias to
~/.ssh/configon the host, with your own values:Host frame HostName <headset-hostname-or-ip> User <headset-user> IdentityFile ~/.ssh/<your-key> -
Connect once interactively (
ssh frame true) to verify and record the host key. The launcher uses batch mode and will not prompt. -
Make sure the headset's SSH server allows remote forwarding (
AllowTcpForwardingmust not beno) and binds forwards to loopback (GatewayPorts no, the default). Do not add other forwards orSendEnvoptions for this alias. -
Start with the tunnel and your alias:
REALHERMES_TUNNEL=1 REALHERMES_SSH_ALIAS=frame npm run prod -
On the headset, open
http://127.0.0.1:5173, orhttp://127.0.0.1:<REALHERMES_FRAME_PORT>if you changed it. The host's app port can differ from the headset port. Never put a host path or a token in the headset URL.
The tunnel runs ssh -N -T with BatchMode=yes, ExitOnForwardFailure=yes, ConnectTimeout=10, ServerAliveInterval=10, ServerAliveCountMax=3, connection sharing disabled, ForkAfterAuthentication=no and PermitLocalCommand=no. It forwards 127.0.0.1:<headset port> to 127.0.0.1:<app port> and nothing else: the audio backend and the gateway stay on the host.
For development servers, run an equivalent tunnel yourself, for example ssh -N -R 127.0.0.1:5173:127.0.0.1:5173 frame. Do not run a manual tunnel on the same headset port as the launcher.
Reconnects
The tunnel starts after local readiness. If the headset sleeps, reboots or disconnects, local services keep running. Keepalives detect silent drops. After an SSH process exits, the launcher waits for it to close and retires its process group with a 3-second grace period. It then retries after 1, 2, 4, 8, 16 and then every 30 seconds, so 4 to 33 seconds pass between attempts, and only one attempt runs at a time. A connection that lasted at least 60 seconds resets the delay. Status lines say "connecting" or "retrying"; the launcher does not probe the headset. When the headset is back, reload its page. A busy headset port causes retries, not a takeover: stop whatever owns it yourself, or choose another REALHERMES_FRAME_PORT.
Health checks
On the host (substitute your port):
curl --fail http://127.0.0.1:5173/api/health # {"status":"ok","chatConfigured":...,"audioConfigured":...}
curl --fail http://127.0.0.1:5173/api/ready # {"appRunning":true,...,"upstreamConnectivity":"not-probed"}
curl -I http://127.0.0.1:5173/ # the app shell, with security headers
curl -i http://127.0.0.1:5173/api/no-such-route # a JSON 404, never the app shell
On the headset, http://127.0.0.1:<headset port>/api/health answers through the tunnel. These endpoints report configuration only; they expose no credentials, URLs or paths. /api/ready returns 503 while the server shuts down. Check real chat and voice from the app itself.
Stop, logs and recovery
- Stop: press Ctrl-C in the launcher terminal, or send
SIGTERMto the launcher. Stopping is idempotent and cancels readiness checks and retries at once. Only the launcher's own process groups receiveSIGTERM, escalated toSIGKILLafter 3 seconds. The production server closes connections and deletes its artifact cache. Exit codes: 130 (SIGINT), 143 (SIGTERM), 1 (startup or service failure). Do not usehermes serve --stopor broadpkillcommands for this workflow. - Logs: the launcher terminal is the log. It shows startup, readiness, configuration status, tunnel attempts and delays, failures and shutdown. You may redirect it to a file. Child output is deliberately suppressed because Hermes and SSH diagnostics can contain secrets or paths, so diagnose a failing child outside the running session, for example with
npm run doctoror by starting Hermes by hand. Never dump an environment that contains credentials. The production app separately appends its own sanitized artifact diagnostics toREALHERMES_LOG_FILE(default~/.hermes/logs/realhermes-relay.log, created0600in a0700folder): one JSON line per link attempt with the host, path, a reason code such ashttp_404and verified page counts, never queries, headers, bodies or tokens. That file is what the in-headset answer "I errored out while loading the artifact. Check the logs." refers to. - Recovery: for a local failure, check the build, the ports and
HERMES_PYTHON, then restart. For a tunnel failure, check the headset's power and network, the alias, its key and host key, and the headset port, and leave the launcher running to reconnect. A forced kill can leave an old artifact cache in$TMPDIR; the next start creates a fresh one and never reuses old IDs.
Development versus production
| Development | Production | |
|---|---|---|
| Command | npm run dev / npm run dev:full |
npm run prod |
| Origin | Vite on 5173 proxying /api to the relay on 3001 |
One Express origin on REALHERMES_PORT |
| Audio backend | Yours (dev) or hermes serve on 9119 (dev:full) |
Owned headless backend on REALHERMES_AUDIO_PORT |
| Headset tunnel | Manual | Optional, owned, reconnecting |
| Child output | Shown | Suppressed |
| Security headers | Vite defaults | Production policy (security-privacy.md) |
Never run development and production on the same ports. Running the launcher under launchd or systemd is not covered by this guide. If you do it, set PATH (or REALHERMES_POPPLER_DIR) and HERMES_PYTHON explicitly, because service managers start with a minimal environment.