Skip to main content
The web app is distributed as an independently versioned OCI image on GitHub Container Registry. The final image holds only the compiled SPA and an unprivileged nginx process — Node, pnpm, and the source tree stay in the build stage.
Deploy an immutable version tag. latest is published as a convenience and should not be the only tag you rely on. Images are built for linux/amd64 and linux/arm64.

Run it

The browser only ever calls same-origin paths — /api/* for the control-plane API and /collect for Faro telemetry — and nginx forwards them to upstreams chosen when the container is created. Both upstream variables are required: When the control plane and Alloy run on the Docker host:
Open http://localhost:3000; the health endpoint is /healthz. host.docker.internal already exists on Docker Desktop, and the explicit --add-host makes the example work on Docker Engine for Linux too. If the other processes are containers, put them on the same user-defined network and use their container names. No Compose file is needed:
Changing an upstream means recreating the container, because Docker environment variables are immutable. It never means rebuilding the image.

Runtime settings

The container writes /config.js at startup rather than baking installation-specific values into the JavaScript bundle. So the same image works for every installation.
CUSTOS_SERVER_TRANSPORT_PUBLIC_KEY is the value custoscp gen-keys prints — see Configuration. It must match the control plane’s CUSTOS_SERVER_TRANSPORT_PRIVATE_KEY, and CUSTOS_ENCRYPTION must agree on both sides.
Never put secrets in these variables. The public key and every browser runtime setting are visible to users by design.

Request flow

nginx strips /api before forwarding, so a browser request to /api/credentials arrives at the control plane as /credentials. /collect is forwarded unchanged to Alloy. traceparent and other headers pass through normally, so frontend-to-backend trace correlation survives. Neither upstream has to be publicly reachable. TLS terminates at the load balancer or ingress in front of port 8080.
Because the browser calls same-origin paths through this proxy, a container deployment normally does not need CUSTOS_CORS_ORIGINS set on the control plane. That setting matters when a browser calls the control plane directly on another origin.

Build it yourself

APP_VERSION identifies the exact build; it is reported to Faro and shown in the account menu.

Publishing a release

Releases are tag-driven, matching the control-plane and daemon repositories:
The target pushes a custos-web/v* tag; the workflow then creates the GitHub Release and publishes the three image tags above. Set the GHCR package visibility to public if installations should pull without GitHub credentials.