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

# CPLN Advisor

> Deploy CPLN Advisor, an AI advisor that right-sizes the workloads in your org from their metrics, logs and billed cost, with optional Autopilot. Single location, bundled Postgres. Needs an existing one-location GVC and two prerequisite dictionary secrets.

## Overview

CPLN Advisor watches the workloads in the org it runs in, tracks CPU, memory, replica counts, error rates and billed cost, and turns what it finds into concrete tuning suggestions generated by an LLM: memory limits, autoscaling thresholds, replica counts. Suggestions appear in a dashboard, and **Autopilot** can apply the qualifying ones for you, each with a one-click revert.

The template brings up the dashboard, an API, a worker, a scheduler, a Redis broker and a bundled Postgres database in one install. It creates no secret and takes no credential as a value: it reads two dictionary secrets you create first, and the advisor's own Control Plane token is entered in the dashboard after install.

<Note>
  This template deploys into an **existing GVC** you already have. It does not create, own or delete a GVC. The GVC must have **exactly one location**: every workload runs in every location of its GVC, so a second location would give you a second scheduler firing every scan twice and a second, independent database. Nothing checks this for you.
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-web` | Next.js dashboard on port 3000 (serverless, 1 to 3 replicas). The only public workload | — |
| Workload | `RELEASE_NAME-api` | FastAPI backend on port 8000 (standard, 1 replica). Verifies the login and runs the database migration at startup. Reachable only from the dashboard | — |
| Workload | `RELEASE_NAME-worker` | Runs scans, applies Autopilot changes, sends the Slack digest (standard, 1 replica). No inbound traffic | — |
| Workload | `RELEASE_NAME-scheduler` | Fires the schedules that create the scans (standard, 1 replica). No inbound traffic | — |
| Workload | `RELEASE_NAME-redis` | Task broker and cache on port 6379. Not persistent, not authenticated; reachable only from the API, worker and scheduler | — |
| Workload | `RELEASE_NAME-postgres` | Bundled PostgreSQL 18 database on port 5432 (stateful, 1 replica), from the catalog [Postgres](/template-catalog/templates/postgres) template 3.4.1 | — |
| Volume set | `RELEASE_NAME-pg-vs` | Database storage, 10 GiB, autoscaling to 50 GiB | — |
| Workload | `RELEASE_NAME-postgres-backup` | Scheduled `pg_dumpall` to object storage (cron) | `postgres.backup.enabled: true` |
| Identity | `RELEASE_NAME-identity` | Identity of the web, API, worker and scheduler workloads | — |
| Policy | `RELEASE_NAME-policy` | Grants `RELEASE_NAME-identity` `reveal` on the advisor credentials secret only | — |
| Identity | `RELEASE_NAME-pg-identity` | Identity of the database and backup workloads | — |
| Policy | `RELEASE_NAME-pg-policy` | Grants `RELEASE_NAME-pg-identity` `reveal` on the database credentials secret only | — |

**What the chart can access.** The two policies are the template's entire grant. Each targets kind `secret`, grants only the `reveal` permission, and lists exactly one secret by name:

| Identity | Permission | Target |
| - | - | - |
| `RELEASE_NAME-identity` | `reveal` | the secret named by `auth.secretName` |
| `RELEASE_NAME-pg-identity` | `reveal` | the secret named by `postgres.config.credentialsSecretName` |

Neither identity can read workloads, metrics, logs or usage, and neither can change anything in your org. The advisor reads and tunes your org with the **service account token you enter in the dashboard**, so its reach is exactly the permissions you give that service account (see [Prerequisites](#prerequisites)).

## Prerequisites

You need an existing GVC with exactly one location, two `dictionary` secrets created **before** you install, and a service account whose token you paste into the dashboard **after** you install. Secret names are org-wide, so give each release its own pair.

<Steps>
  <Step title="Choose the database password">
    Use the same password in both secrets. The URL in the advisor secret must match the database credentials secret, and nothing cross-checks them.

    ```bash theme={null}
    DB_PASS="$(openssl rand -hex 32)"
    ```
  </Step>

  <Step title="Create the database credentials secret">
    The bundled Postgres reads exactly three keys: `username`, `password` and `database`.

    ```bash theme={null}
    cpln secret create-dictionary --name DB_CREDENTIALS_SECRET_NAME \
      --entry username=advisor \
      --entry password="$DB_PASS" \
      --entry database=advisor
    ```

    Set `postgres.config.credentialsSecretName` to this name.
  </Step>

  <Step title="Create the advisor credentials secret">
    The keys are the application's own environment variable names. Replace `RELEASE_NAME` and `GVC_NAME` in `DATABASE_URL` with the release name and GVC you will install into.

    ```bash theme={null}
    cpln secret create-dictionary --name ADVISOR_SECRET_NAME \
      --entry ADVISOR_API_TOKEN="$(openssl rand -hex 32)" \
      --entry ADVISOR_SECRET_KEY="$(openssl rand -hex 32)" \
      --entry ADVISOR_SESSION_SECRET="$(openssl rand -hex 32)" \
      --entry ADVISOR_USERNAME=admin \
      --entry ADVISOR_PASSWORD='YOUR-STRONG-PASSPHRASE' \
      --entry DATABASE_URL="postgresql+asyncpg://advisor:$DB_PASS@RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432/advisor"
    ```

    | Key | What it is |
    | - | - |
    | `ADVISOR_API_TOKEN` | Shared between the dashboard and the API. Without it the API refuses every route except `/health` |
    | `ADVISOR_SECRET_KEY` | Encrypts the AI and Slack credentials you enter in the dashboard. **Losing or changing it loses those credentials** |
    | `ADVISOR_SESSION_SECRET` | Signs the dashboard session cookie. Rotating it signs everyone out |
    | `ADVISOR_USERNAME` / `ADVISOR_PASSWORD` | The dashboard login. One account for everyone |
    | `DATABASE_URL` | Must start `postgresql+asyncpg://`, not `postgres://`. The host is `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432` |

    Set `auth.secretName` to this name.
  </Step>

  <Step title="Create a service account for the advisor">
    Create a [service account](/guides/create-service-account) and a key for it. You paste the key into the dashboard after install (see [First Run](#first-run)); it is not a value and not in either secret. Grant it at minimum:

    | Permission | Why |
    | - | - |
    | `gvc: view` | List GVCs and their workloads |
    | `workload: view` | Read specs, deployments and events |
    | `org: readLogs` | Classify errors from workload logs |
    | `org: readMetrics` | CPU, memory, latency and request metrics |
    | `org: readUsage` | Billed cost per workload rather than an estimate |
    | `workload: edit` | **Only** to apply or revert a suggestion, which patches and redeploys the workload. Omit it to run the advisor read-only |
  </Step>
</Steps>

<Warning>
  If either secret is missing at install time, the deployment waits silently: `cpln logs` returns nothing because no container starts. Read `status.versions[].message` from `cpln workload get-deployments RELEASE_NAME-api --gvc GVC_NAME -o yaml`; it names the missing secret. See [Troubleshooting](#troubleshooting).
</Warning>

## Installation

Create both prerequisite secrets first, then install:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/cpln-advisor \
  --version 1.0.0 --gvc GVC_NAME \
  --set auth.secretName=ADVISOR_SECRET_NAME \
  --set postgres.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME
```

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

### Advisor Credentials

```yaml theme={null}
auth:
  # Secret (b) above. MUST EXIST BEFORE INSTALL.
  secretName: advisor-config
```

The login name and password are keys in this secret, not values. The login is verified by the API, and there is one account for everyone, so the dashboard's activity history attributes every change to a single user.

### Images

```yaml theme={null}
images:
  backend: ghcr.io/controlplane-com/cpln-advisor/advisor-backend:latest
  web: ghcr.io/controlplane-com/cpln-advisor/advisor-web:latest
```

The API, worker and scheduler run the same backend image with different commands, so keep them on one tag. Both images are public and need no pull secret. `:latest` resolves when a workload is deployed, so two installs a week apart can differ; every build also publishes a `:sha-<commit>` tag you can pin.

### Dashboard URL

```yaml theme={null}
appUrl: ""
```

Leave it empty unless you serve the dashboard on a custom domain. The app derives its own public URL for this release's dashboard workload, and uses it for Slack "View in Advisor" links and as the CORS allowlist. When set, it must be a full URL including the scheme (for example `https://advisor.example.com`), and it must never be `*`; the chart refuses to render either mistake.

### Sessions and Logging

```yaml theme={null}
session:
  hours: 12
  rememberDays: 30

# INFO logs one line per scan. DEBUG is very noisy.
logLevel: INFO
```

`session.hours` is the idle timeout and `session.rememberDays` the idle timeout with "Keep me signed in". Both slide forward while you work.

### Workload Resources

```yaml theme={null}
# The dashboard. The only workload reachable from the internet.
web:
  replicas:
    min: 1 # 0 scales to zero when idle, at the cost of a cold start
    max: 3
  resources:
    minCpu: 75m
    maxCpu: 250m
    minMemory: 128Mi
    maxMemory: 512Mi

# The API. Internal only; runs the database migration at startup.
api:
  resources:
    minCpu: 75m
    maxCpu: 256m
    minMemory: 128Mi
    maxMemory: 512Mi

# Runs the scans, applies Autopilot changes, sends the Slack digest.
worker:
  resources:
    maxCpu: 100m
    maxMemory: 256Mi

# Fires the crons that CREATE the scans. Without it nothing is ever scanned on a
# schedule, while everything still looks healthy.
scheduler:
  resources:
    minCpu: 25m
    maxCpu: 50m
    minMemory: 64Mi
    maxMemory: 128Mi

# Task broker and cache. Not persistent; not authenticated — its firewall admits
# only the three workloads that use it.
redis:
  image: redis:7-alpine
  resources:
    maxCpu: 50m
    maxMemory: 128Mi
  maxmemory: 100mb # Redis' own cache cap, below maxMemory on purpose
```

`maxCpu`/`maxMemory` are the container limits. `minCpu`/`minMemory` are the floor [Capacity AI](/reference/workload/capacity) scales from, and are set only on the dashboard, API and scheduler, which run with Capacity AI on. The chart refuses to render when `maxCpu` is 4 or more times `minCpu`, when `maxMemory` is more than 4 times `minMemory`, or when a value looks like a unit typo (`512Gi` for `512Mi`). Raising a `maxCpu` or `maxMemory` on `web`, `api` or `scheduler` usually means raising the matching minimum too.

### Database

```yaml theme={null}
postgres:
  config:
    # Secret (a) above. MUST EXIST BEFORE INSTALL.
    credentialsSecretName: advisor-db-credentials

  image: postgres:18 # 17+ is required for backups

  resources:
    minCpu: 150m
    maxCpu: 512m
    minMemory: 256Mi
    maxMemory: 512Mi

  volumeset:
    capacity: 10 # GiB, 10 is the minimum. Volumes never shrink.
    autoscaling:
      enabled: true
      maxCapacity: 50
      minFreePercentage: 20
      scalingFactor: 1.5

  # Who inside the GVC may reach the database. Narrow this if the GVC is shared.
  internalAccess:
    type: same-gvc

  # Connection pooling. The advisor holds few connections, so it is off.
  pgbouncer:
    enabled: false
```

Values under `postgres` are passed to the bundled [Postgres](/template-catalog/templates/postgres) template. `internalAccess.type: same-gvc` lets **every** workload in your GVC reach port 5432. If the GVC is shared, narrow it at install time with the real workload links:

```bash theme={null}
--set postgres.internalAccess.type=workload-list \
--set 'postgres.internalAccess.workloads[0]=//gvc/GVC_NAME/workload/RELEASE_NAME-api' \
--set 'postgres.internalAccess.workloads[1]=//gvc/GVC_NAME/workload/RELEASE_NAME-worker' \
--set 'postgres.internalAccess.workloads[2]=//gvc/GVC_NAME/workload/RELEASE_NAME-scheduler'
```

Add `RELEASE_NAME-postgres-backup` to that list if you turn backups on.

### Backup

```yaml theme={null}
postgres:
  # OFF because it needs a bucket and a cloud account you create first.
  # TURN IT ON — a volume is not a backup, and nothing else protects your data.
  # backup.image is COUPLED to image above: 18.1.0 backs up Postgres 18.
  backup:
    enabled: false
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0
    schedule: "0 2 * * *" # daily, 02:00 UTC
    provider: aws # aws, gcp or minio
    aws:
      bucket: my-backup-bucket
      region: us-east-1
      cloudAccountName: my-backup-cloudaccount
      policyName: my-backup-policy
      prefix: advisor/backups
```

Backups are off by default and you should turn them on. See [Backing Up](#backing-up).

### Access

There is no access knob on the advisor's own workloads. The firewall is fixed by the chart:

| Workload | Public | Reachable inside the GVC by |
| - | - | - |
| `RELEASE_NAME-web` | **yes** | nothing |
| `RELEASE_NAME-api` | no | the dashboard only |
| `RELEASE_NAME-redis` | no | the API, worker and scheduler only |
| `RELEASE_NAME-worker` and `RELEASE_NAME-scheduler` | no | nothing |
| `RELEASE_NAME-postgres` | no | per `postgres.internalAccess` (default: the whole GVC) |

The dashboard login is the boundary. To narrow the dashboard to an office or VPN range, edit `inboundAllowCIDR` on the `RELEASE_NAME-web` workload after installing; a firewall change can take a few minutes to take effect.

## Connecting

| What | Address | Notes |
| - | - | - |
| Dashboard | `status.canonicalEndpoint` of `RELEASE_NAME-web` | Always public. Sign in with `ADVISOR_USERNAME` and `ADVISOR_PASSWORD` from your advisor secret |
| API | `http://RELEASE_NAME-api.GVC_NAME.cpln.local:8000` | Internal, reachable only from the dashboard |
| Database | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432` | Internal. Credentials are in your database credentials secret |

The advisor's output (suggestions, scores, Autopilot changes and activity) lives in the dashboard, plus the Slack digest if you configure one. Read the dashboard address from the workload:

```bash theme={null}
cpln workload get RELEASE_NAME-web --gvc GVC_NAME -o yaml      # status.canonicalEndpoint
```

To check the API directly from your machine without opening anything up, forward its port (it answers `/health` without a token):

```bash theme={null}
cpln port-forward RELEASE_NAME-api 8123:8000 --gvc GVC_NAME
curl http://127.0.0.1:8123/health
```

To read your credentials back:

```bash theme={null}
cpln secret reveal ADVISOR_SECRET_NAME -o yaml
```

### First Run

<Steps>
  <Step title="Sign in">
    Open the dashboard and sign in with `ADVISOR_USERNAME` and `ADVISOR_PASSWORD` from your advisor secret.
  </Step>

  <Step title="Connect Control Plane">
    Go to **Configuration → Control Plane**, paste the service account key, and press **Test connection**. The key is stored encrypted in the database.
  </Step>

  <Step title="Add an AI provider and optionally Slack">
    Add an Anthropic or OpenAI key on the same page, and a Slack bot token if you want digests. These are encrypted with `ADVISOR_SECRET_KEY`.
  </Step>

  <Step title="Enroll workloads and scan">
    Enable **Scan** on the workloads you want watched, then press **Scan now**.
  </Step>

  <Step title="Confirm the scheduler is running">
    Scheduled scans depend on `RELEASE_NAME-scheduler`. Its logs should show it firing `run_scan`:

    ```bash theme={null}
    cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-scheduler"}' --limit 50 --since 10m
    ```
  </Step>
</Steps>

## Operations

### Backing Up

All advisor state (scans, scores, settings and Autopilot history) lives in the bundled Postgres. Turn on `postgres.backup.enabled` and fill in the `postgres.backup` block shown in [Backup](#backup). It needs a bucket and a [cloud account](/guides/create-cloud-account) you create first; the bucket, cloud account and IAM policy steps are the same as for the [Postgres](/template-catalog/templates/postgres) template. `provider: minio` also needs a prerequisite dictionary secret holding `accessKey` and `secretKey`, named by `postgres.backup.minio.credentialsSecretName`.

Keep `postgres.backup.image` matched to `postgres.image`: tag `18.1.0` backs up Postgres 18 and `17.1.0` backs up Postgres 17.

Each run writes one gzipped `pg_dumpall` file, `postgres-TIMESTAMP.sql.gz`, under `BUCKET/PREFIX/` in your bucket. It is a whole-cluster SQL script, including `CREATE ROLE` and `CREATE DATABASE` statements.

### Restoring a Backup

<Warning>
  This restore path has not been verified end to end for this template. Test it on a scratch install before you rely on it.
</Warning>

<Steps>
  <Step title="Stop the writers">
    Scale `RELEASE_NAME-api`, `RELEASE_NAME-worker` and `RELEASE_NAME-scheduler` to zero so nothing writes during the restore.
  </Step>

  <Step title="Download the dump">
    Copy `postgres-TIMESTAMP.sql.gz` from your bucket with your own cloud tooling.
  </Step>

  <Step title="Open a tunnel to the database">
    ```bash theme={null}
    cpln port-forward RELEASE_NAME-postgres 5432:5432 --gvc GVC_NAME
    ```
  </Step>

  <Step title="Load the dump">
    Use `psql` 18 or newer, which understands the meta-commands a PostgreSQL 18 `pg_dumpall` writes. Use the username and password from your database credentials secret.

    ```bash theme={null}
    gunzip -c postgres-TIMESTAMP.sql.gz | psql -h 127.0.0.1 -p 5432 -U USERNAME -d postgres
    ```

    The bundled server already has the role and database from your credentials secret, so `already exists` errors for those are expected. Treat any other error as a failed restore.
  </Step>
</Steps>

### Rotating Credentials

Running workloads keep the old value of a rotated secret until they are redeployed. Export the advisor secret, edit only the key you are rotating, re-apply it, then force a redeployment of every workload that reads it:

```bash theme={null}
cpln secret reveal ADVISOR_SECRET_NAME -o yaml > secret.yaml
# edit only the key you are rotating in secret.yaml
cpln apply -f secret.yaml
cpln secret reveal ADVISOR_SECRET_NAME -o yaml
cpln workload force-redeployment RELEASE_NAME-api --gvc GVC_NAME
```

Repeat the last command for `RELEASE_NAME-web`, `RELEASE_NAME-worker` and `RELEASE_NAME-scheduler`. Never change `ADVISOR_SECRET_KEY`: it would make every credential stored in the dashboard unreadable. The database password is read only when the database first initializes, so changing it in the secret does not change it in Postgres; change it in Postgres too and update `DATABASE_URL` to match.

### Scaling and Availability

The API, worker and scheduler each run one replica, and the GVC must have one location.

* **Dashboard** scales between `web.replicas.min` and `web.replicas.max`; `min: 0` scales to zero when idle at the cost of a cold start.
* **API** runs the database migration at startup, so two replicas would race on it.
* **Worker** scans are limited by upstream rate limits, so a second worker mostly adds rate-limit errors.
* **Scheduler** must stay at one replica: two would fire every scan twice.

### Troubleshooting

<AccordionGroup>
  <Accordion title="The install sits waiting and cpln logs shows nothing">
    **Cause:** a prerequisite secret does not exist, so no container starts.

    **Fix:** read `status.versions[].message`, which names the missing secret:

    ```bash theme={null}
    cpln workload get-deployments RELEASE_NAME-api --gvc GVC_NAME -o yaml
    ```

    Create the secret. The deployment recovers on its own after a few minutes, or force it:

    ```bash theme={null}
    cpln workload force-redeployment RELEASE_NAME-api --gvc GVC_NAME
    ```
  </Accordion>

  <Accordion title="Everything installs but the API fails to authenticate to the database">
    **Cause:** the username, password or database name in `DATABASE_URL` does not match the database credentials secret, or its host does not use your real release name and GVC.

    **Fix:** compare the two secrets with `cpln secret reveal ADVISOR_SECRET_NAME -o yaml` and `cpln secret reveal DB_CREDENTIALS_SECRET_NAME -o yaml`, correct `DATABASE_URL`, re-apply the secret and force a redeployment as in [Rotating Credentials](#rotating-credentials).
  </Accordion>

  <Accordion title="Nothing is scanned on a schedule but every workload is healthy">
    **Cause:** the scheduler is not running, or the GVC has more than one location.

    **Fix:** check the `RELEASE_NAME-scheduler` logs as in [First Run](#first-run), and confirm the GVC has exactly one location with `cpln gvc get GVC_NAME -o yaml` (`spec.staticPlacement.locationLinks`).
  </Accordion>

  <Accordion title="The chart refuses to render and says the gvc values key is no longer used">
    **Cause:** your values file still carries a `gvc` block. The chart installs into the GVC you select with `--gvc` and does not read one.

    **Fix:** remove the `gvc` block from your values file.
  </Accordion>

  <Accordion title="AI or Slack credentials show as not set">
    **Cause:** `ADVISOR_SECRET_KEY` changed since they were entered, so they can no longer be decrypted.

    **Fix:** restore the original `ADVISOR_SECRET_KEY` if you still have it; otherwise re-enter the credentials in **Configuration**.
  </Accordion>
</AccordionGroup>

## Important Notes

* Create both prerequisite secrets before installing, and make `DATABASE_URL` agree with the database credentials secret.
* Install into a GVC that already exists and has exactly one location.
* Keep `ADVISOR_SECRET_KEY` somewhere durable; losing it loses every credential entered in the dashboard.
* Turn on `postgres.backup.enabled`: a volume is not a backup.
* The dashboard is public; narrow `inboundAllowCIDR` on `RELEASE_NAME-web` if you want it tighter.
* Narrow `postgres.internalAccess` if the GVC is shared with other workloads.
* Grant the service account `workload: edit` only if you want Autopilot and one-click apply; each applied suggestion redeploys a live workload.
* Do not widen Redis access: it is unauthenticated and its firewall is its only protection.
* Uninstalling deletes the database volume set and all scan history; it leaves the GVC and both prerequisite secrets in place.

## External References

<CardGroup cols={2}>
  <Card title="CPLN Advisor Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/cpln-advisor">
    View the source files, default values, and chart definition
  </Card>

  <Card title="Create a Service Account" icon="key" href="https://docs.controlplane.com/guides/create-service-account">
    Issue the token the advisor uses to read and tune your org
  </Card>

  <Card title="Capacity AI" icon="microchip" href="https://docs.controlplane.com/reference/workload/capacity">
    How resource floors and automatic sizing work on Control Plane
  </Card>

  <Card title="Workload Firewall" icon="shield" href="https://docs.controlplane.com/reference/workload/firewall">
    Internal and external access controls used by every workload here
  </Card>
</CardGroup>
