feat: documentation site

Docusaurus 3 docs-only site at docsite/, served statically by nginx under
/docs on the marketing host. Covers getting started (self-hosted install
through first server and first licence), the control plane, Vantage HQ,
a reference section and operations.

Wired into docker-compose.site.yml as docsite (3005:80) and into the
image build workflow, rebuilding on its own directory only. Never added
to the self-hosted compose file.
This commit is contained in:
2026-07-28 15:46:33 +01:00
commit 11b1936bb2
44 changed files with 23669 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
---
id: environment-variables
title: Environment variables
sidebar_label: Environment variables
---
Everything the control plane reads from the environment, and what happens when
it is absent.
## Server
| Name | Required | Default | Notes |
| --- | --- | --- | --- |
| `GRPC_HOST` | **yes** | — | The `host:port` agents dial. Boot fails without it. There is deliberately no fallback to the web host: that would hand every agent a port that does not speak gRPC |
| `MONGO_URI` | no | `mongodb://localhost:27017` | The database name is taken from the URI path, falling back to `vantage`. There is no separate `MONGO_DB` |
| `REDIS_ADDR` | no | `localhost:6379` | Sessions only |
| `KEY_ENCRYPTION_KEY` | yes in practice | — | 64 hex characters (32 bytes) for AES-256-GCM. Required for private keys, vault secrets, OIDC client secrets and console credentials |
| `GITEA_HOST` | yes | `gitea.example.com` | Used to build the install scripts and agent download URLs. The default is a placeholder that will not resolve |
| `GUACD_ADDR` | no | `guacd:4822` | The [browser console](../vantage/browser-console.md) daemon |
| `APP_ROOT_LABEL` | no | `vantage` | The app root label for the host and session organisation guard |
| `VANTAGE_WORKFLOW_LOG_DIR` | no | — | Where workflow run logs are written |
| `VANTAGE_DEFAULT_STEPS_DIR` | no | baked into the image | Where the seeded step library is read from |
| `VANTAGE_DEPLOYMENT` | no | self-hosted | Set to `cloud` on a cloud instance. Governs whether a licence may be pasted |
| `VANTAGE_LICENSE` | no | — | A licence blob, used **only** when the instance has no stored one |
| `FREE_INSTANCE_REAP_AFTER` | no | empty | How long past a Free licence's expiry before the instance and all its data are deleted |
:::danger `KEY_ENCRYPTION_KEY` has no recovery path
It encrypts SSH private keys, vault secrets, OIDC client secrets and console
credentials. Lose it and all of them are unreadable. Back it up separately from
the database it protects.
:::
:::warning `FREE_INSTANCE_REAP_AFTER` empty means disabled, and empty is the default
That is the correct value for a self-hosted install, which must never reap. It
is set only on the hosted deployment.
:::
:::info A wrong `APP_ROOT_LABEL` fails quietly
It does not error. It simply stops matching, and the host/session guard stops
protecting anything.
:::
### Not configurable
The HTTP port (`8080`) and the gRPC port (`9090`) are fixed in the server. The
`HTTP_PORT` and `GRPC_PORT` entries in the shipped Compose file are inert —
remap with Docker's port publishing instead.
## Agent
The agent reads no environment variables. Everything is in its
[config file](./agent-config.md).
## Hosted-only services
These run only on the hosted deployment, from
`deploy/docker-compose.site.yml`. A self-hosted install runs none of them.
### sitesvc — the public contact form
| Name | Required | Notes |
| --- | --- | --- |
| `MONGO_URI` | yes | Must point at the control plane's database. Refuses to start against a database that has not run the instances migration. The database name is read from the URI path; a URI without one is refused rather than defaulted |
| `SMTP_HOST`, `SMTP_FROM` | yes | Without them the contact form answers `503` rather than silently dropping messages |
| `SMTP_TO` | no | Defaults to `support@hostxtra.co.uk` |
| `SMTP_PORT` | no | Defaults to `587`; `465` uses implicit TLS |
| `SMTP_USERNAME`, `SMTP_PASSWORD` | no | Auth is skipped when the username is empty |
| `SITE_ORIGIN` | yes in practice | Comma-separated allowed origins. Unset refuses every cross-origin browser request |
| `TRUST_PROXY` | no | Only `true` behind a proxy that overwrites `X-Forwarded-For`, or clients spoof past the rate limiter |
### admin — the licensing authority
| Name | Required | Notes |
| --- | --- | --- |
| `ADMIN_MONGO_URI` | yes | Admin's own database |
| `CONTROL_MONGO_URI` | yes | The control plane's database, for licence injection and user projection |
| `LICENSE_SIGNING_KEY` | yes | **The only service that ever holds this.** Never add it to the server, and never add admin to the self-hosted Compose file |
| `REDIS_ADDR`, `REDIS_USERNAME`, `REDIS_PASSWORD` | yes | Admin uses an external Redis; the base Compose file hardcodes `redis:6379` for the server, so these reach admin only |
| `ADMIN_ORIGIN` | yes | Comma-separated browser origins that call admin. See the warning below |
| `PADDLE_API_KEY` | yes | Boot-required |
| `PADDLE_WEBHOOK_SECRET` | yes | Boot-required. An unverified webhook endpoint is one anyone can issue licences through |
| `PADDLE_ENV` | yes | `sandbox` or `production`. Selects which catalogue price IDs are served, and must match the value baked into the portal build |
| `SMTP_*` | yes in practice | Account, licence and billing email |
| `PUBLIC_URL`, `APP_LOGIN_URL` | yes in practice | Used in links inside emails |
| `FREE_INSTANCE_REAP_AFTER` | yes | Must match the control plane's value. Admin only uses it to name the date in warning emails; the control plane performs the delete |
:::warning A missing `ADMIN_ORIGIN` entry produces no error anywhere
The CORS layer simply omits the allow-origin header and still answers the
preflight with `204`. The browser blocks the request and **admin logs nothing at
all**. The symptom is a preflight failure on an endpoint that works perfectly
under `curl`.
:::
## Build-time variables
These are baked into frontend images at build time, not read at runtime.
Changing one requires rebuilding that image — and because editing a CI variable
pushes no commit, nothing rebuilds on its own. See [CI/CD](../operations/ci-cd.md).
| Name | Baked into |
| --- | --- |
| `API_URL` | `web` |
| `HQ_URL` | `web` |
| `SITE_API_URL`, `SITE_CONTACT_EMAIL` | `site` |
| `ADMIN_API_URL` | `adminsite` **and** `site` |
| `ADMIN_ENV`, `PADDLE_CLIENT_TOKEN`, `PADDLE_ENV` | `adminsite` |
| `DOCS_URL`, `DOCS_BASE_URL` | `docsite` |