> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-postgresql-tls-support.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage and volumes

> How the operator provisions persistent storage for ClickHouse clusters, including the primary data volume, multi-disk (JBOD) layouts, expansion, and what cannot change after creation.

This guide covers how the operator provisions persistent storage for a
`ClickHouseCluster`: the primary data volume, attaching extra disks in a
multi-disk (JBOD) layout, expanding capacity, and the rules that govern what you
can and cannot change after a cluster exists.

For the field-by-field reference, see
[Configuration → Storage configuration](/products/kubernetes-operator/guides/configuration#storage-configuration)
and the [API Reference](/products/kubernetes-operator/reference/api-reference).

<h2 id="primary-data-volume">
  Primary data volume
</h2>

`spec.dataVolumeClaimSpec` is a standard Kubernetes `PersistentVolumeClaimSpec`.
The operator turns it into a StatefulSet `volumeClaimTemplate`, so the StatefulSet
controller creates and retains one PersistentVolumeClaim per replica and mounts it
at the ClickHouse data path `/var/lib/clickhouse`.

```yaml theme={null}
apiVersion: clickhouse.com/v1alpha1
kind: ClickHouseCluster
metadata:
  name: my-cluster
spec:
  dataVolumeClaimSpec:
    storageClassName: fast-ssd   # optional; depends on the installed CSI driver
    resources:
      requests:
        storage: 100Gi
```

* When `accessModes` is omitted, the operator defaults it to `ReadWriteOnce`.
* The per-replica PVC is retained when the cluster is deleted, so data survives a
  delete-and-recreate of the Custom Resource. For data on an
  [encrypted policy](#at-rest-encryption) this additionally requires preserving the
  encryption key — see the note in that section.
* The same field exists on `KeeperCluster` and behaves the same way.

<h2 id="ephemeral-storage">
  Running without a persistent data volume
</h2>

`dataVolumeClaimSpec` is optional. If you omit it and do not mount your own volume
at the data path, ClickHouse writes to the container's ephemeral filesystem and the
admission webhook returns a warning that data may be lost if the cluster is restarted.

This is intended only for throwaway or test clusters. To supply your own storage
instead of `dataVolumeClaimSpec` — for example an `emptyDir` or a pre-provisioned
volume — define it through `spec.podTemplate.volumes` and mount it at
`/var/lib/clickhouse` with `spec.containerTemplate.volumeMounts`.

<Note>
  `dataVolumeClaimSpec` and a custom volume at the data path are mutually exclusive.
  If `dataVolumeClaimSpec` is set, mounting a custom volume at `/var/lib/clickhouse`
  is rejected. The reserved volume names `clickhouse-storage-volume`,
  `clickhouse-server-tls-volume`, and `clickhouse-server-custom-ca-volume` cannot be
  used in `podTemplate.volumes`.
</Note>

<h2 id="expanding-storage">
  Expanding storage
</h2>

To grow a volume, increase `resources.requests.storage` and apply the change. The
operator updates the existing PVCs in place.

```yaml theme={null}
spec:
  dataVolumeClaimSpec:
    resources:
      requests:
        storage: 200Gi   # was 100Gi
```

<Note>
  Expansion only works when the underlying StorageClass has
  `allowVolumeExpansion: true`. Kubernetes does not support shrinking a PVC, so the
  new size must be greater than or equal to the current size.
</Note>

<h2 id="multi-disk-jbod">
  Multi-disk (JBOD) storage
</h2>

`spec.additionalVolumeClaimTemplates` attaches extra disks to each ClickHouse
replica on top of the primary `dataVolumeClaimSpec`. Each entry is a named PVC
template — a `metadata.name` plus a PVC `spec` — reconciled exactly like the
primary data disk, so the StatefulSet controller creates and retains one PVC per
replica named `<name>-<statefulset>-0`.

```yaml theme={null}
spec:
  dataVolumeClaimSpec:
    storageClassName: fast-ssd
    resources:
      requests:
        storage: 100Gi
  additionalVolumeClaimTemplates:
    - metadata:
        name: disk1
      spec:
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi
    - metadata:
        name: disk2
      spec:
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi
```

The operator mounts each additional volume at `/var/lib/clickhouse/disks/<name>`
and **generates the ClickHouse `storage_configuration` for you** — you do not write
it by hand. It registers every additional disk and adds it to the built-in `default`
storage policy.

The primary data disk (`default`) and every additional disk share a single volume
of the `default` policy, so ClickHouse spreads new data parts across all of them in
round-robin fashion. Usable capacity is the sum of all disks, and every table that
does not set its own `storage_policy` — including `system.*` tables — uses the
combined set.

<Note>
  The mount path keeps the template name verbatim, but the disk identifier inside
  `storage_configuration` replaces hyphens with underscores. A template named
  `cold-disk` is mounted at `/var/lib/clickhouse/disks/cold-disk` and appears as
  `cold_disk` in the generated configuration.
</Note>

<h2 id="custom-storage-policies">
  Custom storage policies
</h2>

You do **not** need `extraConfig` for the JBOD layout above — the operator generates
the `default` policy automatically. Reach for `spec.settings.extraConfig` only when
you want storage policies *beyond* the generated default, for example a tiered
hot/cold policy with `move_factor` and `prefer_not_to_merge`, or an S3-backed disk.
Configuration you add there is merged on top of the generated `storage_configuration`.

See the
[ClickHouse storage documentation](https://clickhouse.com/docs/engines/table-engines/mergetree-family/mergetree#table_engine-mergetree-multiple-volumes)
for the policy fields.

<h2 id="at-rest-encryption">
  At-rest encryption
</h2>

Setting `spec.settings.encryption` enables at-rest encryption of table data. The
operator generates a 16-byte AES key — stored in the managed cluster Secret, or
supplied through `externalSecret` — and a dedicated storage policy that wraps every
data disk with ClickHouse's `encrypted` disk type.

```yaml theme={null}
spec:
  settings:
    encryption: {}   # enables the feature; the policy defaults to "encrypted"
```

Encryption is opt in per table; the default storage policy stays plaintext. Select
the encrypted policy when creating a table:

```sql theme={null}
CREATE TABLE secret_data (id UInt64) ENGINE = MergeTree ORDER BY id
SETTINGS storage_policy = 'encrypted';
```

Set `encryption.policyName` to use a different policy name.

<Note>
  This encrypts MergeTree data parts written through the encrypted policy with
  AES-128-CTR. ClickHouse server metadata and logs at the data root are not covered —
  use disk-level encryption such as LUKS or a CSI driver for those.
  Enabling encryption on a running cluster triggers a one-time rolling restart to
  inject the key; replicas may briefly report a configuration reload error until
  that restart completes.

  The key is stored in the operator-managed cluster Secret, which is owned by the
  Custom Resource and deleted with it. Encrypted parts are unreadable without the
  key: if encrypted data must survive deleting the CR (PVCs are retained), supply
  the key through `externalSecret` or back up the `disk-encryption-key` entry
  before deletion. Do not delete the managed Secret — the operator would generate
  a fresh key and existing encrypted parts would become unreadable.
</Note>

<h2 id="immutability">
  What you cannot change after creation
</h2>

Storage layout is largely fixed once a cluster exists. Updates that would orphan
data or rebind PersistentVolumeClaims are rejected at admission:

* The presence of `dataVolumeClaimSpec` is immutable — you cannot **add** a data
  volume to a cluster created without one, nor **remove** it from a cluster created
  with one.
* The set of `additionalVolumeClaimTemplates` is fixed — you cannot **add**,
  **remove**, or **rename** entries after creation.
* Expanding `resources.requests.storage` on an existing entry **is** allowed (subject
  to StorageClass support, see [Expanding storage](#expanding-storage)).
* Encryption cannot be **disabled** once enabled, and `encryption.policyName` cannot
  be **renamed** — tables already using the encrypted policy would become inaccessible.

<h2 id="validation-reference">
  Validation reference
</h2>

| Condition                                                                         | Result                                                                                |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| No `dataVolumeClaimSpec` and no custom volume at `/var/lib/clickhouse`            | Warning — possible data loss on restart                                               |
| Custom volume mounted at `/var/lib/clickhouse` while `dataVolumeClaimSpec` is set | Rejected                                                                              |
| `additionalVolumeClaimTemplates` set but `dataVolumeClaimSpec` missing            | Rejected                                                                              |
| Additional disk named `default`                                                   | Rejected — reserved by the ClickHouse default disk                                    |
| Additional disk name ending in `-encrypted`                                       | Rejected — collides with generated encrypted disk names                               |
| Additional disk named `clickhouse-storage-volume`                                 | Rejected — collides with the primary data volume name                                 |
| Duplicate additional disk name                                                    | Rejected                                                                              |
| Name not matching `^[a-z]([-a-z0-9]*[a-z0-9])?$` or longer than 63 characters     | Rejected by the CRD schema                                                            |
| Adding or removing `dataVolumeClaimSpec` after creation                           | Rejected                                                                              |
| Adding, removing, or renaming `additionalVolumeClaimTemplates` after creation     | Rejected                                                                              |
| Reserved volume name in `podTemplate.volumes`                                     | Rejected                                                                              |
| `encryption.policyName` set to `default`                                          | Rejected by the CRD schema — the encrypted policy must not replace the default policy |
| Disabling `encryption` or renaming its policy after creation                      | Rejected by the CRD schema                                                            |

<h2 id="related-guides">
  Related guides
</h2>

* [Configuration](/products/kubernetes-operator/guides/configuration) — the full field reference, including `extraConfig`.
* [Scaling clusters](/products/kubernetes-operator/guides/scaling) — how replicas and shards are added and removed.
