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

# Guacamole

> Deploy Apache Guacamole on Control Plane. A clientless remote desktop gateway that opens RDP, VNC, SSH and telnet sessions to internal machines in a browser tab. Single replica with a bundled PostgreSQL, private by default. Needs an existing GVC and a pre-created admin dictionary secret.

## Overview

Apache Guacamole is a clientless remote desktop gateway: users open RDP, VNC, SSH, and telnet sessions to internal machines in a plain browser tab, with no client, plugin, or VPN to install. This template deploys the Guacamole web application together with the `guacd` protocol daemon in one workload, backed by a bundled PostgreSQL that holds users, connections, permissions, and session history. The gateway runs as a single replica and is **private by default** — you reach the UI over a port forward and opt in to publishing it.

The administrator login is not a template value. It comes from a dictionary secret you create before installing, so it never passes through Helm and never lands in the release.

<Note>
  This template deploys into an existing [GVC](/reference/gvc) that you already have — it does not create or manage a GVC.
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-guacamole` | Gateway: the `guacamole` web application (UI, REST API, and browser tunnel on port `8080`), the `guacd` protocol daemon, and a `schema-init` container that loads the database schema on first boot (standard, 1 replica) | — |
| Workload | `RELEASE_NAME-postgres` | Bundled PostgreSQL from the [postgres](/template-catalog/templates/postgres) template — users, connections, connection parameters, permissions, and session history (stateful, 1 replica) | — |
| Workload | `RELEASE_NAME-postgres-backup` | Cron job that dumps the database to object storage on a schedule | `postgres.backup.enabled: true` |
| Volume set | `RELEASE_NAME-pg-vs` | PostgreSQL data directory; takes no scheduled snapshots; a final snapshot is taken when the volume set is deleted and kept 7 days. The gateway workload itself mounts no volume set | — |
| Secret | the name in `postgres.config.credentialsSecretName` | Dictionary secret the chart creates (`username`, `password`, `database`) from `postgres.credentials.*` and hands to the bundled PostgreSQL | — |
| Secret | `RELEASE_NAME-guacamole-start` | Opaque secret holding the start script of the `guacamole` container; contains no credential | — |
| Secret | `RELEASE_NAME-guacamole-init` | Opaque secret holding the schema bootstrap script of the `schema-init` container; contains no credential | — |
| Identity | `RELEASE_NAME-guacamole-identity` | Bound to the gateway workload; no cloud bindings | — |
| Policy | `RELEASE_NAME-guacamole-policy` | Grants that identity `reveal` on exactly four secrets: your admin secret, the database credentials secret, and the two script secrets | — |
| Identity | `RELEASE_NAME-pg-identity` | Bound to the bundled PostgreSQL (and its backup job) | — |
| Policy | `RELEASE_NAME-pg-policy` | Grants the PostgreSQL identity `reveal` on the database credentials secret only | — |

Your admin secret is **not** created by the template — see [Prerequisites](#prerequisites).

<Warning>
  `guacd` listens on port `4822` and that port is deliberately **not published**. The protocol daemon is unauthenticated — anything that can reach it can drive an arbitrary RDP, VNC, or SSH session — so only the web application running beside it in the same workload can reach it. Do not "fix" this by declaring the port.
</Warning>

## Prerequisites

**One secret must exist before you install.** It holds the Guacamole administrator login. The values never pass through Helm, so they do not land in the release. Secrets are org-level, so no GVC flag is involved.

<Steps>
  <Step title="Create the admin secret">
    A [dictionary secret](/guides/create-secret/dictionary) holding exactly two keys — `username` and `password`. They become the Guacamole administrator account, replacing the well-known stock `guacadmin` account before the web application ever serves a request:

    ```bash theme={null}
    cpln secret create-dictionary --name ADMIN_SECRET_NAME \
      --entry username=admin \
      --entry password='YOUR-STRONG-PASSWORD'
    ```

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

  <Step title="Read the secret back later">
    Pass `-o yaml`. A bare `cpln secret reveal` prints only a summary table, not the values:

    ```bash theme={null}
    cpln secret reveal ADMIN_SECRET_NAME -o yaml
    ```
  </Step>
</Steps>

<Warning>
  **Create the secret before installing, or the deployment wedges silently.** The template refuses to render when `admin.secretName` is blank, but a name that points at a secret which does not exist installs "successfully" and then never starts. No container runs, so `cpln logs` returns **zero lines**. The one place the reason appears is `status.versions[].message`:

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

  ```text theme={null}
  The secret ADMIN_SECRET_NAME no longer exists. Workload updates are paused until the
  secret is added or the reference to the secret removed.
  ```

  Use `get-deployments` — plain `cpln workload get` has no `versions` key. Creating the missing secret lets the deployment recover on its own; `cpln workload force-redeployment RELEASE_NAME-guacamole --gvc GVC_NAME` skips the wait.
</Warning>

<Note>
  **The administrator credentials are applied on first boot only.** They seed the database once. Rotating the secret afterwards does not change your login — change the password in the Guacamole UI instead (see [First Run](#first-run)).
</Note>

Nothing else is required for a default install: the bundled database password is internal plumbing that this template turns into a secret for you. Scheduled database backups need a bucket and, for AWS or GCP, a Control Plane cloud account — see [Backing Up](#backing-up).

## Installation

Once your admin secret exists, install from the marketplace registry, pointing `admin.secretName` at it and replacing the bundled database password:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/guacamole \
  --version 1.0.0 --gvc GVC_NAME \
  --set admin.secretName=ADMIN_SECRET_NAME \
  --set postgres.credentials.password='YOUR-DB-PASSWORD'
```

Installing a second Guacamole release in the same org? Also set `postgres.config.credentialsSecretName` to a name no other release uses — secret names are org-wide.

Other ways to install and manage the template:

<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

### Guacamole Web Application

```yaml theme={null}
guacamole:
  image: guacamole/guacamole:1.6.0 # keep this tag equal to the guacd tag
  resources:
    minCpu: 500m
    maxCpu: 1000m
    minMemory: 1Gi
    maxMemory: 2Gi   # JVM heap defaults to ~1/4 of this
```

### guacd Protocol Daemon

`guacd:1.6.0` has **RDP, VNC, SSH, and telnet** compiled in. The Kubernetes protocol is not included.

```yaml theme={null}
guacd:
  image: guacamole/guacd:1.6.0 # keep this tag equal to the guacamole tag
  resources:
    minCpu: 100m
    maxCpu: 1000m
    minMemory: 256Mi
    maxMemory: 1Gi   # raise for many concurrent RDP sessions
```

### Admin Account

```yaml theme={null}
admin:
  secretName: my-guacamole-admin # dictionary secret with username + password — MUST exist before install
```

`admin.secretName` is required. The secret is read on first boot only; afterwards, change the password in the Guacamole UI, not in the secret.

### Logging

```yaml theme={null}
logLevel: info # trace | debug | info | error; applies to both containers
```

`warn` is deliberately not an option: `guacd` spells that level `warning` while the web application spells it `warn`, so no single value covers both containers. The chart rejects anything outside the four listed values at render.

### Access

```yaml theme={null}
publicAccess:
  enabled: false  # false = internal only; true = UI on the auto-assigned *.cpln.app HTTPS endpoint
internalAccess:
  type: same-gvc  # none | same-gvc | same-org | workload-list
  workloads: []   # used only with workload-list, e.g. //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

| `internalAccess.type` | Who can reach Guacamole over the internal network |
| - | - |
| `none` | No workload |
| `same-gvc` | Every workload in the same GVC (default) |
| `same-org` | Every workload in the same org |
| `workload-list` | Only the workloads listed in `internalAccess.workloads` |

A change to either access knob takes a couple of minutes to take effect after the upgrade reports success.

These knobs govern **inbound** traffic to Guacamole. They do not restrict which hosts `guacd` may dial: a remote desktop gateway needs outbound reach to the machines its connections target, so egress stays open. Control what a user can connect to through the connections and permissions you create inside Guacamole.

### Bundled PostgreSQL

The database is the [postgres](/template-catalog/templates/postgres) template as a subchart, so every knob of that template is available under `postgres.*`. The chart builds the credentials secret from `postgres.credentials.*` for you — nothing to create before installing. `postgres.image` also sets the image of the `schema-init` container, so `psql` always matches the server version.

```yaml theme={null}
postgres:
  image: postgres:18 # also used by the schema-init container, so psql matches the server
  credentials: # the chart writes these into the secret named below
    username: guacamole
    password: change-me-guacamole-db # change before installing — seeds the database on first boot
    database: guacamole
  config:
    credentialsSecretName: my-guacamole-db-credentials # chart-created; secret names are org-wide, so give each release its own
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 1Gi
  volumeset:
    capacity: 10 # GiB (minimum 10)
  backup:
    enabled: false # scheduled pg_dumpall to object storage — see Backing Up
    schedule: "0 2 * * *" # daily at 02:00 UTC
    provider: aws # aws | gcp | minio
```

<Note>
  `postgres.credentials.password` seeds the database on first boot and is not updated by later value edits. Uninstalling the release deletes the volume set; reinstall to reset.
</Note>

## Connecting

| What | Address | Notes |
| - | - | - |
| Web UI (default, private) | `http://localhost:8080` through `cpln port-forward` (below) | Tunnels through Control Plane and works with public access off. |
| Web UI (public) | `https://<canonical>.cpln.app` | Only while `publicAccess.enabled` is `true`. Read it from `status.canonicalEndpoint` with `cpln workload get RELEASE_NAME-guacamole --gvc GVC_NAME -o yaml`. |
| Internal (same GVC) | `http://RELEASE_NAME-guacamole.GVC_NAME.cpln.local:8080` | Reachable per `internalAccess.type`. Use the fully qualified name — a bare short name does not reliably resolve. |
| Admin login | `username` / `password` from your admin secret | `cpln secret reveal ADMIN_SECRET_NAME -o yaml` |
| PostgreSQL (same GVC) | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432` | `username` / `password` / `database` from `cpln secret reveal SECRET_NAME -o yaml`, where `SECRET_NAME` is your `postgres.config.credentialsSecretName`. |

Guacamole is served at `/`, not at `/guacamole/`. With the default private install, reach the UI from your machine through a tunnel and open `http://localhost:8080`:

```bash theme={null}
cpln port-forward RELEASE_NAME-guacamole 8080:8080 --gvc GVC_NAME
```

### First Run

A default install is closed to the internet. Sign in through a port forward first, set your own password, add a connection, and publish the UI only if you want it reachable from outside the GVC.

<Steps>
  <Step title="Wait for the gateway to report ready">
    PostgreSQL comes up first, then the `schema-init` container loads the schema and writes your administrator account, and only then does the web application start serving. Expect the first boot to take a few minutes.

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

    While it waits, the gateway's logs show `guacamole: waiting for the schema and admin account to be applied...`; the boot is finished when `schema-init: bootstrap complete, idling` and `guacamole: schema ready, starting Tomcat` appear:

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

  <Step title="Sign in over a port forward">
    `cpln port-forward` is a top-level command, not a `cpln workload` subcommand:

    ```bash theme={null}
    cpln port-forward RELEASE_NAME-guacamole 8080:8080 --gvc GVC_NAME
    ```

    Open `http://localhost:8080` and sign in with the `username` and `password` from your admin secret. The stock `guacadmin` / `guacadmin` login is never valid here — the stock account is replaced by yours before the web application starts.
  </Step>

  <Step title="Change the administrator password">
    Open the Guacamole menu (top right) → **Settings → Preferences** and use **Change Password**. The password now lives in the database; the secret you created is not consulted again after the first boot, so leaving it unchanged there does not weaken anything.
  </Step>

  <Step title="Add a connection">
    A fresh install has no connections and looks empty until you add one. Go to **Settings → Connections → New Connection**, choose a protocol (RDP, VNC, SSH, or telnet), and set the target host and port. For a machine running as a workload in the same GVC, use its fully qualified internal name as the hostname — `WORKLOAD_NAME.GVC_NAME.cpln.local` — with the service's port (for example `22` for SSH). A bare short name does not reliably resolve. Then grant users access to the connection under **Settings → Users**.
  </Step>

  <Step title="Publish the UI, if you want it public">
    Upgrade the release with `publicAccess.enabled` set to `true`. The canonical `*.cpln.app` hostname then appears under `status.canonicalEndpoint`:

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

    Allow a couple of minutes for the change to take effect before concluding it did not work.
  </Step>
</Steps>

## Operations

### Backing Up

Scheduled backups of the bundled PostgreSQL are off by default. Enabling `postgres.backup.enabled: true` adds one cron workload, `RELEASE_NAME-postgres-backup`, that runs `pg_dumpall` on the schedule and uploads a gzipped dump to your bucket under the configured prefix. The dump covers everything Guacamole persists: users, connections, connection parameters, permissions, and session history. Pick a `postgres.backup.provider` and complete the matching setup **before** installing or upgrading with it on.

<Tabs>
  <Tab title="AWS S3">
    ```yaml theme={null}
    postgres:
      backup:
        enabled: true
        provider: aws
        aws:
          bucket: my-postgres-bucket
          region: us-east-1
          cloudAccountName: my-s3-cloud-account
          policyName: my-postgres-backup-policy # bucket-scoped IAM policy
          prefix: postgres/backups
    ```

    <Steps>
      <Step title="Create a bucket">
        Create an S3 bucket. Set `postgres.backup.aws.bucket` and `postgres.backup.aws.region` to match.
      </Step>

      <Step title="Set up a cloud account">
        If you do not have one, [create a cloud account](/guides/create-cloud-account) for the AWS account holding the bucket. Set `postgres.backup.aws.cloudAccountName` to its name.
      </Step>

      <Step title="Create a bucket-scoped IAM policy">
        Create an IAM policy with the JSON below (replace `YOUR_BUCKET_NAME`) and set `postgres.backup.aws.policyName` to its name. The template attaches it to the PostgreSQL identity and nothing else — the bucket in your policy is the only storage the backup job can reach:

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion",
                "s3:GetBucketLocation",
                "s3:AbortMultipartUpload",
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts"
              ],
              "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
              ]
            }
          ]
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Google Cloud Storage">
    ```yaml theme={null}
    postgres:
      backup:
        enabled: true
        provider: gcp
        gcp:
          bucket: my-postgres-bucket
          cloudAccountName: my-gcs-cloud-account
          prefix: postgres/backups
    ```

    <Steps>
      <Step title="Create a bucket">
        Create a GCS bucket. Set `postgres.backup.gcp.bucket` to its name.
      </Step>

      <Step title="Set up a cloud account">
        [Create a cloud account](/guides/create-cloud-account) for the GCP project holding the bucket and set `postgres.backup.gcp.cloudAccountName` to its name.
      </Step>

      <Step title="Grant storage access">
        Grant the cloud account's service account the **Storage Admin** (`roles/storage.admin`) role on the project. The chart additionally binds the identity to `roles/storage.objectAdmin` on exactly the bucket in `postgres.backup.gcp.bucket`.
      </Step>
    </Steps>
  </Tab>

  <Tab title="MinIO / S3-compatible">
    ```yaml theme={null}
    postgres:
      backup:
        enabled: true
        provider: minio
        minio:
          endpoint: http://my-minio-workload:9000 # S3 API address including the port
          bucket: my-postgres-bucket
          credentialsSecretName: my-postgres-minio-credentials # dictionary secret with accessKey + secretKey
          prefix: postgres/backups
    ```

    <Steps>
      <Step title="Create a bucket">
        Create the bucket on your server. Set `postgres.backup.minio.bucket` to its name and `postgres.backup.minio.endpoint` to the S3 API address including the port — for the [MinIO](/template-catalog/templates/minio) template in the same GVC, `http://WORKLOAD_NAME.GVC_NAME.cpln.local:9000`.
      </Step>

      <Step title="Create the credentials secret">
        Create a dictionary secret with the server's keys and set `postgres.backup.minio.credentialsSecretName` to its name. No cloud account is involved.

        ```bash theme={null}
        cpln secret create-dictionary --name BACKUP_MINIO_SECRET_NAME \
          --entry accessKey=YOUR-ACCESS-KEY \
          --entry secretKey='YOUR-SECRET-KEY'
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

<Note>
  `postgres.backup.image` must match the PostgreSQL major version in `postgres.image`. If you move off the defaults, change both together. The full per-provider walkthrough lives on the [postgres](/template-catalog/templates/postgres) template page.
</Note>

The database dump does not include your admin secret, which lives in the secret you created. The PostgreSQL volume set `RELEASE_NAME-pg-vs` takes no scheduled [snapshots](/reference/volumeset); a final snapshot is taken when the volume set is deleted and kept 7 days.

### Restoring a Backup

Each backup is a gzipped plain-SQL `pg_dumpall` archive. Restoring means replaying that SQL into the bundled PostgreSQL. The one transport that carries a dump into the container intact is `cpln workload exec` with `--stdin` and a `base64` wrapper on both ends — raw binary piped through exec is corrupted:

```bash theme={null}
gunzip -c BACKUP_FILE.sql.gz | base64 | cpln workload exec RELEASE_NAME-postgres --gvc GVC_NAME \
  --container postgresql --stdin --quiet -- sh -c 'base64 -d | psql -U "$POSTGRES_USER" -d postgres'
```

Two constraints make this a planned operation: the archive contains `CREATE ROLE` and `CREATE DATABASE` statements, so a straight replay collides with the `guacamole` database the schema-init container already created on first boot; and no user should be signed in while you drop and recreate that database before the replay — every session is cut when its rows disappear.

<Warning>
  This restore path has not been exercised against this template end to end. Rehearse it on a throwaway release before you depend on it. Restoring from a volume set snapshot is likewise unverified here.
</Warning>

### Scaling and Availability

This template runs **exactly one replica**, and there is no `replicas` knob. Guacamole keeps authentication tokens in each web application's memory with no cross-instance sharing, so a second replica would randomly sign users out.

Any restart — an upgrade, a replica reschedule, or a forced redeployment — therefore drops active desktop sessions and signs everyone out. Reconnecting re-establishes the session. Nothing persisted is lost: users, connections, permissions, and history all live in PostgreSQL on the `RELEASE_NAME-pg-vs` volume set, which survives restarts, redeploys, and upgrades under the same release name.

<Note>
  The first `helm upgrade` after an install can restart the bundled PostgreSQL even when its values are unchanged, so expect a brief interruption.
</Note>

Rotating a secret the workload references does not restart it; the old value stays in force until you run `cpln workload force-redeployment RELEASE_NAME-guacamole --gvc GVC_NAME`. For the admin credentials specifically, even a redeployment does not change your login — those are applied on first boot only.

### Troubleshooting

<AccordionGroup>
  <Accordion title="Deployment never becomes ready and the logs are empty">
    **Symptom:** `cpln helm install` reported success, `cpln logs` returns nothing, and `cpln workload get-deployments RELEASE_NAME-guacamole --gvc GVC_NAME -o yaml` shows `The secret ADMIN_SECRET_NAME no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed.` under `status.versions[].message`.

    **Cause:** The prerequisite admin secret named by `admin.secretName` does not exist.

    **Fix:** Create it (see [Prerequisites](#prerequisites)). The deployment recovers on its own once the secret exists; `cpln workload force-redeployment RELEASE_NAME-guacamole --gvc GVC_NAME` skips the wait.
  </Accordion>

  <Accordion title="Gateway stays at waiting for the schema and admin account to be applied">
    **Symptom:** The workload is not ready and its logs repeat `guacamole: waiting for the schema and admin account to be applied...` without ever reaching `schema-init: bootstrap complete, idling`.

    **Cause:** The `schema-init` container is almost always still waiting on PostgreSQL — its own log line reads `schema-init: waiting for postgres at RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432 ...`.

    **Fix:** Check that `RELEASE_NAME-postgres` is ready with `cpln workload get-deployments RELEASE_NAME-postgres --gvc GVC_NAME -o yaml`, then follow the bootstrap with a server-side filter:

    ```bash theme={null}
    cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-guacamole"} |= "schema-init"' --limit 50 --since 10m
    ```
  </Accordion>

  <Accordion title="Second install refused as managed by a different release">
    **Symptom:** Installing a second Guacamole release fails with `cannot be updated because it is being managed by a different release` and creates nothing.

    **Cause:** Both releases use the same `postgres.config.credentialsSecretName`. Secret names are org-wide, and the first release owns that one. Nothing is shared, overwritten, or deleted.

    **Fix:** Set `postgres.config.credentialsSecretName` to a distinct name for the second release and install again.
  </Accordion>

  <Accordion title="Rotated the admin secret but the old password still works">
    **Symptom:** You changed `username` or `password` in the admin secret (and even forced a redeployment), yet the original credentials still sign in and the new ones are rejected.

    **Cause:** The admin credentials seed the database on first boot only. Once the schema exists the `schema-init` container logs `schema-init: guacamole schema already present, skipping bootstrap` and leaves the account alone — otherwise every restart would overwrite a password changed in the UI.

    **Fix:** Change the password in the Guacamole UI under **Settings → Preferences**, or as an administrator under **Settings → Users**.
  </Accordion>

  <Accordion title="guacadmin / guacadmin does not sign in">
    **Symptom:** Guacamole's documented stock login returns an invalid-credentials error.

    **Cause:** Expected. The stock account is renamed and re-hashed to the `username` and `password` in your admin secret before the web application starts serving.

    **Fix:** Sign in with the values from `cpln secret reveal ADMIN_SECRET_NAME -o yaml`.
  </Accordion>

  <Accordion title="Render fails because logLevel must be trace, debug, info or error">
    **Symptom:** `guacamole: logLevel must be 'trace', 'debug', 'info' or 'error', got 'warn'`.

    **Cause:** `warn` is excluded because `guacd` spells that level `warning` and the web application spells it `warn`, so no single value covers both containers.

    **Fix:** Use `info` or `error`.
  </Accordion>

  <Accordion title="Everyone was signed out after an upgrade or restart">
    **Symptom:** After a `helm upgrade`, a forced redeployment, or a replica reschedule, every user is back at the login page and open desktop sessions ended.

    **Cause:** Expected. Guacamole holds authentication tokens in the web application's memory, and the template runs one replica — see [Scaling and Availability](#scaling-and-availability).

    **Fix:** Sign in again and reconnect. Users, connections, and history are intact in PostgreSQL.
  </Accordion>

  <Accordion title="A connection to a workload in the GVC cannot resolve its host">
    **Symptom:** Opening a connection fails with a hostname-resolution error from `guacd`.

    **Cause:** The connection's hostname is a bare workload name. Short names do not reliably resolve.

    **Fix:** Edit the connection and set the hostname to the fully qualified form `WORKLOAD_NAME.GVC_NAME.cpln.local`.
  </Accordion>
</AccordionGroup>

## Important Notes

* **Create the admin secret before installing** — `admin.secretName` must name an existing dictionary secret with `username` and `password`. A missing secret wedges the deployment silently; see [Prerequisites](#prerequisites) for the one command that shows the reason.
* **The admin credentials are first-boot only.** Rotating the secret afterwards does not change your login. Change the password in the Guacamole UI instead.
* **`guacadmin` / `guacadmin` is never valid here** — the stock account is replaced by yours before the web application starts.
* **Rotating any referenced secret does not restart the workload.** The old value stays in force until you run `cpln workload force-redeployment RELEASE_NAME-guacamole --gvc GVC_NAME`.
* **Single replica by design, with no `replicas` knob.** Any restart drops active desktop sessions and signs everyone out; see [Scaling and Availability](#scaling-and-availability).
* **Public access is off by default.** Reach the UI over `cpln port-forward` until you deliberately publish it. Access changes take a couple of minutes to take effect.
* **`guacd`'s port `4822` is intentionally unpublished** because the protocol daemon is unauthenticated. Only the web application in the same workload can reach it.
* **Only RDP, VNC, SSH, and telnet are available** in `guacd:1.6.0`. The Kubernetes protocol is not compiled in.
* **Use fully qualified internal names** — `WORKLOAD_NAME.GVC_NAME.cpln.local` — both for reaching Guacamole from other workloads and for the connection targets you configure in it.
* **Keep `guacamole.image` and `guacd.image` on the same tag** — the web application and the protocol daemon are versioned together upstream.
* **Change `postgres.credentials.password` before installing** and give each release its own `postgres.config.credentialsSecretName` — secret names are org-wide, and a second release on the same name is refused at install.
* **Uninstalling deletes the volume set** and every user, connection, and history row in it; your admin secret is yours and survives.

## External References

<CardGroup cols={2}>
  <Card title="Guacamole Manual" href="https://guacamole.apache.org/doc/gug/" icon="book">
    The official Apache Guacamole administrator and user guide
  </Card>

  <Card title="Administration" href="https://guacamole.apache.org/doc/gug/administration.html" icon="users">
    Managing users, connections, groups, and permissions
  </Card>

  <Card title="Configuring Guacamole" href="https://guacamole.apache.org/doc/gug/configuring-guacamole.html" icon="sliders">
    Connection parameters for RDP, VNC, SSH, and telnet
  </Card>

  <Card title="PostgreSQL Authentication" href="https://guacamole.apache.org/doc/gug/postgresql-auth.html" icon="database">
    How Guacamole stores users and connections in PostgreSQL
  </Card>

  <Card title="Guacamole Template" href="https://github.com/controlplane-com/templates/tree/main/guacamole" icon="github">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
