> ## Documentation Index
> Fetch the complete documentation index at: https://browser-mcp.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Helm deployment

> Dedicated namespace, explicit Ceph persistence, existing secrets, and migration-gated server startup.

## Requirements and defaults

* Kubernetes, Helm 3, a dedicated namespace, and an ingress controller if exposing HTTP.
* One server replica only; the chart rejects other values and the `default` namespace.
* An existing Kubernetes Secret; the chart does not generate passwords or embed plaintext secrets in Helm release values.
* Bundled PostgreSQL by default, one replica with a `ReadWriteOnce` PVC, `storageClassName: rook-ceph-block`, and 10Gi requested storage. No `subPath` mounts are used.
* An image you have built/published or verified. The default image naming convention is `ghcr.io/ricsam/browser-mcp-community:<appVersion>-community`; its presence in values is not a publication claim.

The source repository remains private. The initial chart is published separately on Cloudflare R2:

```bash theme={null}
helm repo add browser-mcp https://pub-4975d290a12d4bcc812ccaeb12fd9155.r2.dev
helm repo update
helm pull browser-mcp/browser-mcp --version 0.1.1
```

The public development URL is rate-limited. The initial `0.1.1-community` image supports linux/amd64. Managed deployments must override `image.repository` to `ghcr.io/ricsam/browser-mcp` and provide registry credentials.

## 1. Prepare namespace and secrets

The following commands are operator actions, not something chart rendering performs:

```bash theme={null}
kubectl create namespace browser-mcp
```

Create `browser-mcp-secrets` in that namespace using your secret manager. It must contain `DATABASE_URL`, `AUTH_SECRET`, `SETTINGS_ENCRYPTION_KEY`, and, for bundled PostgreSQL, `POSTGRES_PASSWORD`. Generate passwords and keys independently; do not use example strings. Managed builds also require `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`.

If importing from a protected local env file, keep it outside the repository, mode 0600, and never include `export` prefixes:

```bash theme={null}
kubectl -n browser-mcp create secret generic browser-mcp-secrets \
  --from-env-file=/secure/path/browser-mcp.env
```

With release name `browser` and the default chart name, the bundled database's service is `browser-browser-mcp-postgres`. Its connection string takes this shape, with a URL-encoded password:

```text theme={null}
postgresql://browser_mcp:<password>@browser-browser-mcp-postgres.browser-mcp.svc:5432/browser_mcp
```

`DATABASE_URL` must match `postgresql.username`, `postgresql.database`, and the password stored as `POSTGRES_PASSWORD`. A different release name or `fullnameOverride` changes the service name. The chart deliberately does not synthesize a secret database URL. Do not change the configured password expecting an initialized PostgreSQL PVC to rotate automatically.

## 2. Configure values

Keep values non-secret:

```yaml theme={null}
edition: community
secrets:
  existingSecret: browser-mcp-secrets
config:
  publicUrl: https://browser.example.com
image:
  repository: <your-registry>/<your-image>
  tag: 0.1.0-community
  # digest: sha256:<verified-digest>
ingress:
  enabled: true
  className: traefik
  host: browser.example.com
  tls: []
postgresql:
  enabled: true
  persistence:
    storageClass: rook-ceph-block
    size: 10Gi
```

On an r5d managed cluster, choose a one-label host such as `browser-<project-suffix>.r5d.app` from your assigned suffix, keep `ingressClassName: traefik`, and omit TLS entries: Cloudflare terminates public HTTPS. Use an `https://` public URL. The suffix alone without a prefix is not a valid workload host.

Outside that environment, configure your ingress TLS termination explicitly, for example `ingress.tls: [{secretName: browser-tls, hosts: [browser.example.com]}]`. Without TLS entries or an upstream TLS terminator, the chart does not magically provide HTTPS.

### External PostgreSQL

Set `postgresql.enabled: false`. Put your external database's URL into the same secret's `DATABASE_URL`; no StatefulSet or PVC is then rendered. The database must already exist, be reachable from the migration and server pods, and permit schema migration. Configure verified TLS in the connection string according to your provider; do not disable certificate verification.

### Private GHCR images on r5d

Use the worker's configured registry auth without printing its contents. After creating the namespace, an operator can create the rotation-aware pull secret:

```bash theme={null}
NAMESPACE=browser-mcp
kubectl -n "$NAMESPACE" create secret generic ghcr-pull \
  --type=kubernetes.io/dockerconfigjson \
  --from-file=.dockerconfigjson="${R5D_GHCR_AUTH_FILE:-$REGISTRY_AUTH_FILE}" \
  --dry-run=client -o yaml | \
  kubectl annotate --local -f - r5d.dev/credential-source=github-oauth -o yaml | \
  kubectl apply -f -
```

Then set `imagePullSecrets: [{name: ghcr-pull}]`. The annotation allows credential rotation; do not place auth JSON in values or source control.

## 3. Validate and install

Render the exact intended values and submit that rendered file to server admission **before** installation. The namespace must already exist for dry-run validation. The file contains secret references, not secret material.

```bash theme={null}
helm lint charts/browser-mcp --strict --namespace browser-mcp -f values-production.yaml
helm template browser charts/browser-mcp --namespace browser-mcp \
  -f values-production.yaml > /tmp/browser-mcp-rendered.yaml
kubectl apply --dry-run=server -f /tmp/browser-mcp-rendered.yaml
helm upgrade --install browser charts/browser-mcp --namespace browser-mcp \
  -f values-production.yaml --wait --wait-for-jobs --timeout 20m
```

Do not use `helm template | kubectl apply` as the upgrade mechanism: Helm release revisions identify migration jobs and prevent immutable Job updates. For an existing release, `helm upgrade --dry-run=server` additionally validates Helm's actual release context. Fix all admission failures before proceeding.

## Migration ordering

The migration Job is deliberately **not** a `pre-install` hook. Such a hook would run before bundled PostgreSQL, its Service, and ordinary release resources exist and can deadlock the first install.

Instead:

1. Helm creates the bundled database (if enabled), a revision-named migration Job, and the Deployment.
2. The Job's init container retries an authenticated `SELECT 1` against `DATABASE_URL`; a listening port alone is not considered usable.
3. The Job runs `bun dist/migrate.js` from the **same image** as the new server.
4. A Deployment init container reads only that exact Job's status, using namespace-scoped `get` RBAC. The application process does not start until the Job has a successful Complete condition. Failure leaves the server gated.

The Job is a regular Helm-managed resource without a TTL. **Do not delete the current migration Job**: restarted pods need its completion record. The service account credential is projected only into the startup init container, never into the app container. Neither app nor migration pods automatically mount a Kubernetes API credential.

On upgrade, Recreate prevents overlap of server replicas. Migrations can overlap the shutdown of the old version, so use backward-compatible, expand/contract schema changes or stop the old server deliberately for maintenance. This is a startup gate, not an atomic database/Deployment transaction. Automatic Helm rollback does not undo database migrations.

## 4. Verify the release

```bash theme={null}
kubectl -n browser-mcp get jobs,pods,pvc
kubectl -n browser-mcp rollout status statefulset/browser-browser-mcp-postgres --timeout=10m
kubectl -n browser-mcp rollout status deployment/browser-browser-mcp --timeout=20m
kubectl -n browser-mcp get events --sort-by=.lastTimestamp
```

Skip the StatefulSet check with an external database. Check the revision-named migration Job completed, Pods are Ready, the PVC is Bound to Ceph storage, and the public `/healthz` and `/readyz` return success. Inspect migration/server logs without exposing secrets. Complete one browser pairing and a harmless MCP request before declaring the deployment usable.

Uninstalling the chart may retain StatefulSet PVCs. Manage backup and deletion explicitly; do not delete retained volumes as a routine troubleshooting step.
