Termavox

Configuration reference

Generated from docs/guides/configuration.md · Edit on GitHub · All docs

On this page

Every setting Termavox reads: the Docker Compose variables, the environment of each service, the secret files, ports, networks and volumes, and the instance settings admins change in the UI. For a first installation, follow the installation guide; for the security meaning of these settings, see the security model.

The sources of truth are apps/session-manager/src/config/config.ts, apps/web/lib/config.ts, deploy/compose.yaml, deploy/.env.example, deploy/Caddyfile and the Dockerfiles in deploy/docker/.

How configuration works

  • With Docker Compose you set only the variables in Compose settings, in deploy/.env (copy it from deploy/.env.example; compose reads it automatically) or in your shell, which takes precedence. deploy/compose.yaml turns them into each container's environment and fixes everything else. To change a fixed value, edit deploy/compose.yaml. The Caddyfile is part of the Caddy image: to change it, edit deploy/Caddyfile and build the images from source (docker compose -f deploy/compose.yaml up -d --build, see Building from source).
  • Outside Compose (development, or running a service by hand) each service reads its variables directly from its environment; see the per-service tables.
  • Secrets are always files. The services take the path of each secret file (…_FILE variables), never the secret itself. There is no TERMAVOX_WS_SECRET, TERMAVOX_INTERNAL_TOKEN or BETTER_AUTH_SECRET variable.
  • Validation. Both services validate their environment with zod and report every invalid variable at once. session-manager exits at boot with termavox session-manager: invalid configuration: …. web validates when it builds its auth runtime: it tries at startup, logs a warning if that fails (for example while session-manager has not migrated the database yet), and tries again on the first request.
  • Flags (TERMAVOX_ENABLE_LOCAL_SHELL, TERMAVOX_SSH_ALLOW_LOCAL_TARGETS) accept true or 1 for on, and false, 0 or an empty value for off. Any other value is a configuration error.

Compose settings

These are the only variables deploy/compose.yaml takes from deploy/.env or the shell. None is required.

VariableDefaultPassed toMeaning
TERMAVOX_DOMAINlocalhostcaddyCaddy's site address: the name browsers use. Also the default host of TERMAVOX_PUBLIC_URL.
TERMAVOX_HTTP_PORT80caddyHTTP port, published on the host and used inside the container. Redirects to HTTPS on TERMAVOX_HTTPS_PORT.
TERMAVOX_HTTPS_PORT443caddyHTTPS port, published on the host (TCP, plus UDP for HTTP/3) and used inside the container.
TERMAVOX_PUBLIC_URLhttps://<TERMAVOX_DOMAIN>:<TERMAVOX_HTTPS_PORT>web, session-managerThe browser-facing URL (see the service tables). A :443 suffix is normalized away. Set it only when browsers reach Termavox under another URL, for example through a port forward.
TERMAVOX_TLSautocaddyinternal: Caddy's local CA, 30-day certificates (the env template's choice, for localhost, IP addresses and LAN names). auto: Caddy's automatic HTTPS, ACME for public domains and the local CA with 12-hour certificates for localhost and IP addresses. off: plain HTTP for a reverse proxy in front that terminates TLS. See Domain and TLS.
TERMAVOX_BIND_ADDRESSunsetCompose ports of caddyHost address the ports are published on (e.g. 127.0.0.1); unset, every interface. On a host with IPv6, Docker forwards IPv6 clients to Caddy from the edge network's gateway address, so they all share one sign-in lockout and rate limit: 0.0.0.0 publishes on IPv4 only.
TERMAVOX_REVERSE_PROXYunsetcaddyAddresses (IPs or CIDR ranges, space-separated) of a reverse proxy in front, as Caddy sees them: Caddy reads the client address from X-Forwarded-For only on their requests, right to left. Required with TERMAVOX_TLS=off: until it is set, Caddy answers 503. A proxy on the same host is 172.30.10.1. See Behind a reverse proxy.
TERMAVOX_LOG_LEVELinfosession-managerDaemon log level. web has no log level setting.
TERMAVOX_DAEMON_MEM_LIMIT1gCompose mem_limit of session-managerMemory ceiling of the daemon container. Raise it for many concurrent sessions or long scrollback.
TERMAVOX_WEB_MEM_LIMIT768mCompose mem_limit of webMemory ceiling of the web container.
TERMAVOX_SSH_ALLOW_LOCAL_TARGETSfalsesession-managerAllow SSH targets on loopback, link-local, multicast, unspecified or the daemon's own addresses. See SSH targets.
TERMAVOX_EVENT_RETENTION_DAYS180session-managerDays to keep audit events and session events. See Event retention.

Caddy's memory limit (256m) and every pids_limit (caddy 128, web 256, session-manager 512) are fixed in deploy/compose.yaml.

session-manager environment

Read by apps/session-manager/src/config/config.ts.

VariableRequiredDefaultCompose valueMeaning
TERMAVOX_PUBLIC_URLyes—TERMAVOX_PUBLIC_URL (derived, see above)An http or https URL. Its origin must equal the Origin header of every WebSocket upgrade; anything else is closed with 4403.
TERMAVOX_DATA_DIRno/data/dataData directory. The SQLite database is <dir>/app.db, shared with web. The daemon applies the migrations at boot.
TERMAVOX_WS_HOSTno0.0.0.00.0.0.0Listen address of the WebSocket server.
TERMAVOX_WS_PORTno30013001Listen port of the WebSocket server (path /ws).
TERMAVOX_WS_SECRET_FILEyes—/run/secrets/ws_secretPath of the WebSocket ticket key (the content is what the code calls TERMAVOX_WS_SECRET). Must be the same file content web uses.
TERMAVOX_INTERNAL_TOKEN_FILEyes—/run/secrets/internal_tokenPath of the internal API bearer token. Must be the same file content web uses.
TERMAVOX_MASTER_KEY_FILEyes—/run/secrets/master_keyPath of the vault master key. The daemon refuses to start if the key does not match the database (vault_key_check).
TERMAVOX_INTERNAL_HOSTno127.0.0.1session-manager-internalListen address of the internal API. Compose binds it to the alias that exists only on the internal network.
TERMAVOX_INTERNAL_PORTno30023002Listen port of the internal API.
TERMAVOX_TRUSTED_PROXIESnoempty: trust nobody172.30.10.0/24 (the edge subnet)Comma-separated IPs and CIDRs whose connections may name the client in X-Forwarded-For. The daemon takes the rightmost value that is not a trusted proxy; from any other peer it uses the socket address. Used for the IP in session events. An entry that is not an IP or CIDR is a configuration error.
TERMAVOX_ENABLE_LOCAL_SHELLnofalsefalseGate of local direct mode, which is not implemented yet (Phase 6): session-manager refuses local sessions even when it is on. Leave it off.
TERMAVOX_SSH_ALLOW_LOCAL_TARGETSnofalsefrom the compose settingSee Compose settings. Applies to sessions and to host tests (Fetch & verify).
TERMAVOX_EVENT_RETENTION_DAYSno180from the compose settingWhole days: 0 keeps every row forever, otherwise 7 to 3650. Unset or empty means 180; any other value is a configuration error. See Event retention.
TERMAVOX_LOG_LEVELnoinfofrom the compose settingfatal, error, warn, info, debug, trace or silent. Logs are JSON on stdout.

The session-manager image also sets NODE_ENV=production and TERMAVOX_DATA_DIR=/data; the daemon does not read NODE_ENV.

Event retention

session-manager deletes audit_events and session_events rows older than TERMAVOX_EVENT_RETENTION_DAYS days: once a few minutes after it starts, then every 6 hours. The purge applies to the whole tables, so the audit events web writes (sign-ins, lockouts, admin actions) age out with the daemon's own; the admin audit log and each user's activity page show only what is left. It deletes the oldest rows first, 500 per short transaction, so other writes are never held up, and it logs how many rows it deleted from each table (never their contents). With 0 nothing is ever deleted. A session that has stayed failed for longer than the retention period keeps its state, but its specific failure reason ages out with its events and the UI shows the generic one.

web environment

Read by apps/web/lib/config.ts, plus the variables below it.

VariableRequiredDefaultCompose valueMeaning
TERMAVOX_PUBLIC_URLyes—TERMAVOX_PUBLIC_URL (derived)An http or https URL. Better Auth's base URL and its only trusted origin; the ticket and layout endpoints compare Origin with it exactly; the setup URL in the logs is built from it. It must match the address bar (scheme, host and port), or sign-in and the terminal fail.
TERMAVOX_DATA_DIRno/data/dataData directory; web opens <dir>/app.db, which session-manager creates and migrates.
BETTER_AUTH_SECRET_FILEyes—/run/secrets/auth_secretPath of Better Auth's secret.
TERMAVOX_WS_SECRET_FILEyes—/run/secrets/ws_secretPath of the WebSocket ticket key; same content as session-manager's.
TERMAVOX_INTERNAL_URLyes—http://session-manager-internal:3002Base URL of session-manager's internal API (http or https).
TERMAVOX_INTERNAL_TOKEN_FILEyes—/run/secrets/internal_tokenPath of the internal API bearer token; same content as session-manager's.

Set by the web image (deploy/docker/web.Dockerfile) for Next.js's standalone server:

VariableValueMeaning
NODE_ENVproductionProduction mode. With development (next dev), the page CSP also allows 'unsafe-eval'.
NEXT_TELEMETRY_DISABLED1Turns off Next.js telemetry (also set during the image build).
HOSTNAME0.0.0.0Listen address of the Next.js server.
PORT3000Listen port of the Next.js server. Caddy proxies to web:3000.
TERMAVOX_DATA_DIR/dataAs above.

NEXT_RUNTIME is set by Next.js itself (web only boots its auth runtime in the nodejs runtime); it is not a setting. The build stages of both images set CI=true for a non-interactive pnpm install; it does not reach the runtime images.

Build-time variable

VariableDefaultMeaning
NEXT_PUBLIC_TERMAVOX_WS_URLunset: the browser uses same-origin /wsA development override, for example ws://localhost:3001/ws when session-manager runs without the proxy. Next.js inlines it at next build (and in next dev), so setting it on a running container has no effect. When set, its origin is added to the page CSP's connect-src, together with the matching http(s) origin, where the file panel's uploads go (/transfer/* on the same host; the daemon answers their CORS preflight for the TERMAVOX_PUBLIC_URL origin only). The web Dockerfile does not set it. session-manager still requires the Origin of TERMAVOX_PUBLIC_URL.

Caddy environment

Set by deploy/compose.yaml from the compose settings and read by deploy/Caddyfile: TERMAVOX_DOMAIN, TERMAVOX_HTTP_PORT, TERMAVOX_HTTPS_PORT, TERMAVOX_TLS and TERMAVOX_REVERSE_PROXY, with the same defaults as above. The upstreams web:3000 and session-manager:3001 are fixed in the Caddyfile.

The Caddy image (deploy/docker/caddy.Dockerfile) is the official caddy:2 image, pinned by digest, with deploy/Caddyfile baked in at /etc/caddy/Caddyfile; it runs as UID/GID 10002 with no capabilities. compose sets net.ipv4.ip_unprivileged_port_start=0 for its container (a namespaced sysctl: the host is unchanged), so it binds ports below 1024 without NET_BIND_SERVICE.

Images

deploy/compose.yaml names one image per service, tagged with the repository version (node scripts/set-version.ts moves the three tags and every package.json version together), and keeps each image's build: section. Release installs pull them; up -d --build builds them from the checkout under the same names.

ImageDockerfileRuns as
ghcr.io/ativaone/termavox-caddy:<version>deploy/docker/caddy.Dockerfile10002:10002
ghcr.io/ativaone/termavox-web:<version>deploy/docker/web.Dockerfile10001:10001
ghcr.io/ativaone/termavox-session-manager:<version>deploy/docker/session-manager.Dockerfile10001:10001

Secret files

deploy/scripts/init-secrets.sh creates them in deploy/secrets/ (directory mode 0700; each file group 10001, mode 0440). Compose mounts them as Docker secrets under /run/secrets/.

FileMounted intoVariableFormatGenerated as
master_keysession-managerTERMAVOX_MASTER_KEY_FILEStandard base64 of exactly 32 bytes (surrounding whitespace ignored)openssl rand -base64 32
internal_tokenweb, session-managerTERMAVOX_INTERNAL_TOKEN_FILEAt least 32 characters after trimmingopenssl rand -hex 32
ws_secretweb, session-managerTERMAVOX_WS_SECRET_FILEAt least 32 characters after trimmingopenssl rand -hex 32
auth_secretwebBETTER_AUTH_SECRET_FILEAt least 32 characters after trimmingopenssl rand -hex 32
  • For internal_token, ws_secret and auth_secret, the trimmed text itself (as UTF-8 bytes) is the secret; it is never base64-decoded. web and session-manager must read identical internal_token and ws_secret files.
  • master_key: back it up separately from the database. A different key makes the daemon refuse to start; a lost key means every stored credential is lost. To replace it, follow Rotating the master key; never just overwrite the file.
  • auth_secret: Better Auth signs its cookies and encrypts TOTP secrets with it. Replacing it signs everyone out and breaks existing TOTP enrollments.
  • Both services read the secret files once per process. After replacing internal_token or ws_secret, restart web and session-manager.
  • init-secrets.sh refuses to overwrite existing files; --force replaces all four, including master_key.

Ports

ListenerDefaultReachable fromConfigured with
Caddy HTTP80/tcpthe host's network (published)TERMAVOX_HTTP_PORT
Caddy HTTPS443/tcp and 443/udp (HTTP/3)the host's network (published)TERMAVOX_HTTPS_PORT
web3000the edge network only (Caddy)PORT, HOSTNAME (image)
session-manager WebSocket3001the edge network only (Caddy)TERMAVOX_WS_HOST, TERMAVOX_WS_PORT
session-manager internal API3002the internal network only (web)TERMAVOX_INTERNAL_HOST, TERMAVOX_INTERNAL_PORT

Only Caddy publishes ports. The ports 3000, 3001 and 3002 also appear in the Caddyfile, in TERMAVOX_INTERNAL_URL and in the healthchecks of deploy/compose.yaml; change them together.

Networks

NetworkMembersNotes
edgecaddy, web, session-managerSubnet 172.30.10.0/24, fixed in deploy/compose.yaml (x-edge-subnet) because session-manager trusts X-Forwarded-For from it. Change it there if it collides with a network of the host. Also the containers' outbound path (SSH).
internalweb, session-managerinternal: true: no gateway, no outside connectivity. session-manager carries the alias session-manager-internal here. Caddy is not a member.

session-manager also maps host.docker.internal to the Docker host gateway, for the "This Docker host" preset.

Volumes and writable paths

VolumeMounted atServiceContent
termavox-data/dataweb, session-managerapp.db and its -wal/-shm files. Back it up with deploy/scripts/backup.sh. Also session-manager.lock, the data directory lock that session-manager and vault rotate hold while they run (released when the holder exits, even after a crash; do not delete it while either runs).
caddy-data/datacaddyCertificates and Caddy's local CA. Owned by Caddy's UID/GID 10002.
caddy-config/configcaddyCaddy's autosaved configuration. Owned by 10002 too.

Bind mounts: the four secret files only (the Caddyfile is part of the Caddy image). Every root filesystem is read-only; /tmp is a tmpfs in every container, and web also gets a tmpfs at /app/apps/web/.next/cache. SQLite's WAL mode needs a local volume shared on one kernel: network filesystems are not supported.

deploy/scripts/backup.sh and restore.sh run inside the session-manager container and use its TERMAVOX_DATA_DIR (default /data).

Instance settings

Admins edit these at /admin/settings. They are stored in the instance_settings table, take effect without a restart, and every change is audited (admin_setting.change).

Setting in the UIKeyDefaultEffect
Require two-step verification for everyonerequire_totpoffEvery user without TOTP, admins included, must enroll before anything else opens, and cannot disable TOTP. Admins can also require TOTP for single users.
Maximum running sessions per usermax_running_sessions_per_user50A whole number from 1 to 1000. Creating or reopening a session is refused (RATE_LIMITED) when the user already has this many sessions meant to run.
Show local shells (direct mode) to administratorslocal_shell_visibleoffOffers the local host kind to admins. It has no effect until direct mode ships (Phase 6), and even then only with TERMAVOX_ENABLE_LOCAL_SHELL=true on the daemon. See the warning in the security model.
Allow the file panel (upload and download)file_transfer_enabledonThe Files panel of every session (plan §5.12). Off: the panel's button disappears and the daemon refuses every file request (FILES_DISABLED); transfers already running finish.
Record file operations in the audit logfile_audit_enabledoffWrites file.upload, file.download (with the bytes sent and whether the download completed), file.mkdir, file.rename and file.remove audit events (server, paths, sizes, client address; never contents).

session-manager also stores two keys of its own there, which are not settings: installation_id (used in the names of the remote tmux sockets) and vault_key_check.

Per-user preferences (language, theme, terminal font and colors, OSC 52 clipboard writes, link confirmation) are under /settings, and connection options (protection mode, startup command, TERM, environment, keepalive, auto-reopen, scrollback) are per server, under Advanced on its page.

Built-in limits

These are constants in the code, not settings.

LimitValue
WebSocket ticket lifetime30 s, single use
WebSocket connections per user16 (the next is closed with 4429)
Attached terminals per user, across connections64
Channels per connection32 (one more closes the connection with 4400)
Messages in flight per connection64 (then 4429)
Client frame size1 MiB
Auth-session revalidation of open WebSocketsevery 60 s
Sign-in lockout per (username, IP)5 failures → 30 s, doubling up to 15 min
Host tests (Fetch & verify)10 per user per minute
Keyboard-interactive prompt timeout100 s
Reconnect backoff1 s doubling to 60 s, ±20% jitter
Idle runtime of a session that is not runningfreed after 60 s without viewers
Pending host keysexpire after 24 h
Daemon shutdown budget25 s (stop_grace_period: 30s in compose)
File panel requests in flight per connection8 (then RATE_LIMITED)
File panel transfers per user, at once6 (then RATE_LIMITED); a browser tab runs 2 at a time
File panel transfers in the whole daemon, at once64 (then RATE_LIMITED)
Upload and download URL lifetime60 s, single use (at most 64 unused per user)
Upload with no datastopped after 60 s
Download the browser takes nothing fromstopped after 120 s
Auth-session revalidation of running transfersevery 60 s
SFTP request the server does not answer60 s for the oldest waiting request, then the channel is given up
Folder listingfirst 10 000 entries or 2 MiB of names and link texts
Delete in the file panel64 folder levels; 100 000 entries or 32 MiB of names read per request
Idle SFTP channel of a sessionclosed after 5 min

The test suites also read TV_TMUX_IMAGES and TV_S4_PAUSE_MS (under apps/session-manager/test/); they are test knobs, not product settings.