> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tofunmiadewuyi.com/custos/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy the web app

> Run the Custos frontend container and point it at your control plane.

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.

```text theme={null}
ghcr.io/tofunmiadewuyi/custos-frontend:v1.0.0
ghcr.io/tofunmiadewuyi/custos-frontend:<git-commit-sha>
ghcr.io/tofunmiadewuyi/custos-frontend:latest
```

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:

| Variable | Example | Purpose |
| - | - | - |
| `CUSTOS_API_UPSTREAM` | `http://host.docker.internal:8080` | control plane, reachable from the container |
| `CUSTOS_FARO_UPSTREAM` | `http://host.docker.internal:12347` | Alloy Faro receiver, reachable from the container |

When the control plane and Alloy run on the Docker host:

```bash theme={null}
docker run --rm \
  --name custos-frontend \
  --add-host host.docker.internal:host-gateway \
  -p 3000:8080 \
  -e CUSTOS_API_UPSTREAM=http://host.docker.internal:8080 \
  -e CUSTOS_FARO_UPSTREAM=http://host.docker.internal:12347 \
  ghcr.io/tofunmiadewuyi/custos-frontend:v1.0.0
```

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:

```bash theme={null}
docker network create custos

docker run --rm \
  --name custos-frontend \
  --network custos \
  -p 3000:8080 \
  -e CUSTOS_API_UPSTREAM=http://custos-cp:8080 \
  -e CUSTOS_FARO_UPSTREAM=http://alloy:12347 \
  ghcr.io/tofunmiadewuyi/custos-frontend:v1.0.0
```

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.

| Variable | Default | Purpose |
| - | - | - |
| `CUSTOS_ENCRYPTION` | `false` | enable the encrypted API transport |
| `CUSTOS_SERVER_TRANSPORT_PUBLIC_KEY` | empty | the control plane's X25519 transport public key; required when encryption is on |
| `CUSTOS_APP_VERSION` | image build version | shown in the account menu and sent to Faro |
| `CUSTOS_ENVIRONMENT` | `production` | Faro deployment environment |

```bash theme={null}
docker run --rm \
  -p 3000:8080 \
  -e CUSTOS_API_UPSTREAM=https://cp.internal.example \
  -e CUSTOS_FARO_UPSTREAM=https://alloy.internal.example \
  -e CUSTOS_ENCRYPTION=true \
  -e CUSTOS_SERVER_TRANSPORT_PUBLIC_KEY='<base64-public-key>' \
  ghcr.io/tofunmiadewuyi/custos-frontend:v1.0.0
```

`CUSTOS_SERVER_TRANSPORT_PUBLIC_KEY` is the value `custoscp gen-keys` prints — see
[Configuration](/custos/custos/control-plane/configuration). It must match the control plane's
`CUSTOS_SERVER_TRANSPORT_PRIVATE_KEY`, and `CUSTOS_ENCRYPTION` must agree on both sides.

<Warning>
  Never put secrets in these variables. The public key and every browser runtime setting are visible
  to users by design.
</Warning>

## 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`.

<Note>
  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.
</Note>

## Build it yourself

```bash theme={null}
docker build \
  --build-arg APP_VERSION="$(git rev-parse HEAD)" \
  -t custos-frontend:local \
  .
```

`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:

```bash theme={null}
make release                  # auto-increment the patch version
make release VERSION=v1.0.0   # choose explicitly
make rerelease                # delete and re-push the newest tag after a failed release
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.