You are looking at the documentation of a prior release. To read the documentation of the latest release, please
visit here.
New to KubeDB? Please start here.
KubeDB ClickHouse - Continuous Archiving and Point-in-time Recovery
Here, we will show you how to use KubeDB to continuously archive a ClickHouse database and restore it to a specific point in time.
Before You Begin
At first, you need to have a Kubernetes cluster, and the kubectl command-line tool must be configured to communicate with your cluster. If you do not already have a cluster, you can create one by using kind.
Now,
- install
KubeDBoperator in your cluster following the steps here. - install
KubeStashoperator in your cluster following the steps here.
To keep things isolated, this tutorial uses a separate namespace called demo throughout this tutorial.
$ kubectl create ns demo
namespace/demo created
Note: The yaml files used in this tutorial are stored in docs/guides/clickhouse/pitr/examples folder in GitHub repository kubedb/docs.
Continuous Archiving
Continuous archiving involves taking a periodic full backup of the ClickHouse cluster and, in between full backups, continuously archiving the incremental changes. To ensure continuous archiving to a remote location we need to prepare a BackupStorage, a RetentionPolicy, and a ClickHouseArchiver for the KubeDB managed ClickHouse database.
Prepare Backend
We are going to store our backed up data into an S3 bucket. We have to create a Secret with necessary credentials and a BackupStorage CR to use this backend. If you want to use a different backend, please read the respective backend configuration doc from here.
Note: ClickHouse currently supports
S3,Azure Blob Storage, andGoogle Cloud Storage(via S3 compatibility mode) as backup storage backends.
Create Secret:
Let’s create a secret called s3-secret with access credentials to our desired S3 bucket,
$ echo -n '<your-aws-access-key-id-here>' > AWS_ACCESS_KEY_ID
$ echo -n '<your-aws-secret-access-key-here>' > AWS_SECRET_ACCESS_KEY
$ kubectl create secret generic -n demo s3-secret \
--from-file=./AWS_ACCESS_KEY_ID \
--from-file=./AWS_SECRET_ACCESS_KEY
secret/s3-secret created
Create BackupStorage:
Now, create a BackupStorage that references this secret. Below is the YAML of the BackupStorage CR we are going to create,
apiVersion: storage.kubestash.com/v1alpha1
kind: BackupStorage
metadata:
name: s3-storage
namespace: demo
spec:
storage:
provider: s3
s3:
bucket: kubestash
prefix: clickhouse-pitr
secretName: s3-secret
region: us-east-2
endpoint: https://s3.us-east-2.amazonaws.com
usagePolicy:
allowedNamespaces:
from: All
default: true
deletionPolicy: Delete
Let’s create the BackupStorage we have shown above,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.10.12/docs/guides/clickhouse/pitr/examples/backupstorage.yaml
backupstorage.storage.kubestash.com/s3-storage created
Create RetentionPolicy:
Now, let’s create a RetentionPolicy to specify how the old Snapshots should be cleaned up.
Below is the YAML of the RetentionPolicy object that we are going to create,
apiVersion: storage.kubestash.com/v1alpha1
kind: RetentionPolicy
metadata:
name: demo-retention
namespace: demo
spec:
default: true
failedSnapshots:
last: 2
successfulSnapshots:
last: 2
usagePolicy:
allowedNamespaces:
from: All
Let’s create the above RetentionPolicy,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.10.12/docs/guides/clickhouse/pitr/examples/retentionpolicy.yaml
retentionpolicy.storage.kubestash.com/demo-retention created
Create Encryption Secret
Let’s create a secret called encrypt-secret with the Restic password,
$ echo -n 'changeit' > RESTIC_PASSWORD
$ kubectl create secret generic -n demo encrypt-secret \
--from-file=./RESTIC_PASSWORD
secret/encrypt-secret created
Create ClickHouseArchiver CR:
ClickHouseArchiver is a CR provided by KubeDB for managing the continuous archiving (periodic full backup plus incremental backups) of ClickHouse databases.
apiVersion: archiver.kubedb.com/v1alpha1
kind: ClickHouseArchiver
metadata:
name: sample-clickhouse-archiver
namespace: demo
spec:
pause: false
databases:
namespaces:
from: Selector
selector:
matchLabels:
kubernetes.io/metadata.name: demo
selector:
matchLabels:
archiver: "true"
retentionPolicy:
name: demo-retention
namespace: demo
encryptionSecret:
name: "encrypt-secret"
namespace: "demo"
fullBackup:
driver: "ClickHouseBackup"
scheduler:
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
schedule: "*/30 * * * *"
sessionHistoryLimit: 2
backupStorage:
ref:
name: "s3-storage"
namespace: "demo"
Let’s create the above ClickHouseArchiver,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.10.12/docs/guides/clickhouse/pitr/examples/sample-clickhouse-archiver.yaml
clickhousearchiver.archiver.kubedb.com/sample-clickhouse-archiver created
Here,
spec.databases.namespacesandspec.databases.selectorselect the targetClickHousedatabases this archiver applies to (by namespace and label). AClickHouseCR can alternatively (or additionally) reference an archiver directly via its ownspec.archiver.reffield, as shown below.spec.fullBackup.scheduler.schedulespecifies how often a new full backup is taken. In between full backups, KubeStash continuously archives incremental changes into the same repository.spec.backupStorage.refandspec.retentionPolicyreuse theBackupStorageandRetentionPolicywe created earlier.
The
KubeDBprovisioner usesKubeStashfor the periodic full backup and asidekickcontainer for continuous incremental archiving ofClickHousedatabases, both using theClickHouseBackupdriver, which runs ClickHouse’s nativeBACKUPcommand. Incremental backups are taken with the nativeBACKUPcommand using the latest full backup as thebase_backup, and the data is restored with the nativeRESTOREcommand.
Deploy Sample ClickHouse Database
Below is the YAML of a sample ClickHouse CR that we are going to create for this tutorial. Notice the archiver: "true" label (matched by the ClickHouseArchiver’s spec.databases.selector) and the explicit spec.archiver.ref pointing to the ClickHouseArchiver we created above:
apiVersion: kubedb.com/v1alpha2
kind: ClickHouse
metadata:
name: sample-clickhouse
namespace: demo
labels:
archiver: "true"
spec:
version: 25.7.1
archiver:
ref:
name: sample-clickhouse-archiver
namespace: demo
clusterTopology:
clickHouseKeeper:
externallyManaged: false
spec:
replicas: 3
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
cluster:
name: appscode-cluster
shards: 2
replicas: 2
podTemplate:
spec:
containers:
- name: clickhouse
resources:
limits:
memory: 4Gi
requests:
cpu: 1
memory: 2Gi
initContainers:
- name: clickhouse-init
resources:
limits:
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
deletionPolicy: WipeOut
Create the above ClickHouse CR,
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.10.12/docs/guides/clickhouse/pitr/examples/sample-clickhouse.yaml
clickhouse.kubedb.com/sample-clickhouse created
Wait until the database goes into Ready state,
$ kubectl get clickhouse -n demo sample-clickhouse
NAME VERSION STATUS AGE
sample-clickhouse 25.7.1 Ready 61s
Insert Initial Data
Since sample-clickhouse is deployed with a clusterTopology, we create a ReplicatedMergeTree table (replicated within a shard via ClickHouseKeeper) together with a Distributed table on top of it (to read across both shards) — a plain MergeTree table would live on a single node only and wouldn’t exercise the cluster at all.
$ kubectl get secret -n demo sample-clickhouse-auth -o jsonpath='{.data.username}' | base64 -d
admin⏎
$ kubectl get secret -n demo sample-clickhouse-auth -o jsonpath='{.data.password}' | base64 -d
psL)PHO!)ryZEcoX⏎
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) CREATE DATABASE IF NOT EXISTS demo_db ON CLUSTER '{cluster}';
:) CREATE TABLE demo_db.users ON CLUSTER '{cluster}'
(
id UInt64,
name String,
shard_id UInt8
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/demo_db/users', '{replica}')
ORDER BY id;
:) CREATE TABLE demo_db.users_dist ON CLUSTER '{cluster}' AS demo_db.users
ENGINE = Distributed('{cluster}', demo_db, users, rand());
:) exit
To control exactly which shard each row lives on, we insert directly into the local demo_db.users table of one replica of each shard; ReplicatedMergeTree then replicates the rows to the other replica of that shard.
Let’s exec into the first replica of shard 0 and insert the rows of shard 0,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (1, 'Alice_shard0', 0), (2, 'Bob_shard0', 0);
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name─────────┬─shard_id─┐
1. │ 1 │ Alice_shard0 │ 0 │
2. │ 2 │ Bob_shard0 │ 0 │
└────┴──────────────┴──────────┘
:) exit
Now, exec into the first replica of shard 1 and insert the rows of shard 1,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (3, 'Charlie_shard1', 1), (4, 'David_shard1', 1);
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name───────────┬─shard_id─┐
1. │ 3 │ Charlie_shard1 │ 1 │
2. │ 4 │ David_shard1 │ 1 │
└────┴────────────────┴──────────┘
:) exit
Let’s verify that the rows have been replicated to the second replica of each shard,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-1 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name─────────┬─shard_id─┐
1. │ 1 │ Alice_shard0 │ 0 │
2. │ 2 │ Bob_shard0 │ 0 │
└────┴──────────────┴──────────┘
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-1 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name───────────┬─shard_id─┐
1. │ 3 │ Charlie_shard1 │ 1 │
2. │ 4 │ David_shard1 │ 1 │
└────┴────────────────┴──────────┘
:) exit
And through the Distributed table, which returns the rows from both shards,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users_dist ORDER BY id;
┌─id─┬─name───────────┬─shard_id─┐
1. │ 1 │ Alice_shard0 │ 0 │
2. │ 2 │ Bob_shard0 │ 0 │
3. │ 3 │ Charlie_shard1 │ 1 │
4. │ 4 │ David_shard1 │ 1 │
└────┴────────────────┴──────────┘
:) exit
Verify Backup Setup Successful
If everything goes well, the KubeDB provisioner will create a BackupConfiguration for the database, and its phase should be Ready. The Ready phase indicates that the backup setup is successful.
$ kubectl get backupconfiguration -n demo
NAME PHASE PAUSED AGE
sample-clickhouse-archiver Ready 15m
Verify BackupSession:
KubeStash triggers an instant full backup as soon as the BackupConfiguration is ready. After that, full backups are taken according to spec.fullBackup.scheduler.schedule (every 30 minutes here), while incremental backups are taken continuously in between.
Note: You can also trigger a full backup manually at any time, but we are not doing that here. In this tutorial, we simply rely on the instant backup, the full backup schedule and the continuous incremental backups.
$ kubectl get backupsession -n demo
NAME INVOKER-TYPE INVOKER-NAME PHASE DURATION AGE
sample-clickhouse-archiver-full-backup-1790745995 BackupConfiguration sample-clickhouse-archiver Succeeded 34s 14m
sample-clickhouse-archiver-full-backup-1790746201 BackupConfiguration sample-clickhouse-archiver Succeeded 30s 11m
Here, the first BackupSession is the instant full backup taken right after the BackupConfiguration became ready, and the second one was taken by the */30 * * * * schedule.
Verify Snapshot:
$ kubectl get snapshots -n demo -l=kubestash.com/repo-name=sample-clickhouse-full
NAME REPOSITORY SESSION SNAPSHOT-TIME DELETION-POLICY PHASE AGE
sample-clickhouse-full-sample-clarchiver-full-backup-1790745995 sample-clickhouse-full full-backup 2026-09-30T05:26:46Z Delete Succeeded 14m
sample-clickhouse-full-sample-clarchiver-full-backup-1790746201 sample-clickhouse-full full-backup 2026-09-30T05:30:01Z Delete Succeeded 11m
Verify Incremental Backup:
Let’s check the pods which are related to the backup,
$ kubectl get pods -n demo
NAME READY STATUS RESTARTS AGE
retention-policy-sample-clickhouse-archiver-full-bac-179079knjw 0/1 Completed 0 11m
retention-policy-sample-clickhouse-archiver-full-bac-17907z92h8 0/1 Completed 0 14m
sample-clickhouse-appscode-cluster-shard-0-0 1/1 Running 0 16m
sample-clickhouse-appscode-cluster-shard-0-1 1/1 Running 0 16m
sample-clickhouse-appscode-cluster-shard-1-0 1/1 Running 0 16m
sample-clickhouse-appscode-cluster-shard-1-1 1/1 Running 0 16m
sample-clickhouse-archiver-full-backup-1790745995-9lh4m 0/1 Completed 0 14m
sample-clickhouse-archiver-full-backup-1790746201-ggjd9 0/1 Completed 0 11m
sample-clickhouse-keeper-0 1/1 Running 0 16m
sample-clickhouse-keeper-1 1/1 Running 0 16m
sample-clickhouse-keeper-2 1/1 Running 0 16m
sample-clickhouse-sidekick 1/1 Running 0 3m37s
trigger-sample-clickhouse-archiver-full-backup-29845770-ff2nw 0/1 Completed 0 11m
Here,
- Pods
sample-clickhouse-archiver-full-backup-*performed the full backups. - Pod
sample-clickhouse-sidekickperforms continuous incremental archiving. It stays inRunningphase and runs an incremental backup cycle roughly every minute, archiving the changes made since the latest full backup.
Each incremental cycle backs up the metadata and data. You can follow the cycles in the sidekick logs,
$ kubectl logs -n demo sample-clickhouse-sidekick --tail=12
I0930 05:41:08.574880 1 helpers.go:347] Latest successful snapshot: sample-clickhouse-full-sample-clarchiver-full-backup-1790746201
I0930 05:41:08.629451 1 output.go:290] Backup started with OperationID=dce60f64-7a38-4f87-9c10-3c01a5b0cd8d InitialStatus=CREATING_BACKUP
I0930 05:41:18.704574 1 output.go:337] Backup daua1t1t3d5c73dcv5b0-metadata completed successfully!
I0930 05:41:18.704594 1 archiver.go:225] Metadata incremental backup completed successfully
I0930 05:41:18.758046 1 output.go:58] Backup started with OperationID=d697a0c8-497f-4a1c-af38-f8ea2d66b34d InitialStatus=CREATING_BACKUP
I0930 05:41:18.758548 1 output.go:58] Backup started with OperationID=a2bf8757-8080-43be-810d-5db522f00d64 InitialStatus=CREATING_BACKUP
I0930 05:41:28.815165 1 output.go:105] Backup daua1t1t3d5c73dcv5b0-shard-1 completed successfully!
I0930 05:41:28.815189 1 archiver.go:274] Data incremental backup completed for shard 1
I0930 05:41:28.815619 1 output.go:105] Backup daua1t1t3d5c73dcv5b0-shard-0 completed successfully!
I0930 05:41:28.815643 1 archiver.go:274] Data incremental backup completed for shard 0
I0930 05:41:28.815656 1 archiver.go:187] Cluster incremental backup completed successfully for all 2 shards
I0930 05:41:28.838303 1 incremental_backup.go:146] Incremental backup cycle completed in 20.271855756s
Insert Incremental Data
Now, let’s insert more data into the database over time. Each batch is picked up by the next incremental backup cycle of the sidekick.
Insert the second batch,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (5, 'Eve_shard0', 0), (6, 'Frank_shard0', 0);
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (7, 'Grace_shard1', 1), (8, 'Henry_shard1', 1);
:) exit
Insert the third batch,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (9, 'Ivy_shard0', 0), (10, 'Jack_shard0', 0), (11, 'Kate_shard0', 0);
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (12, 'Leo_shard1', 1), (13, 'Mia_shard1', 1);
:) exit
At this point, shard 0 holds ids 1, 2, 5, 6, 9, 10, 11 and shard 1 holds ids 3, 4, 7, 8, 12, 13 (13 rows in total). This is the state we are going to restore to later. Wait for the next incremental backup cycle to complete (check the sidekick logs as shown above); any time after that cycle and before the next batch is inserted is a valid recovery point for this state. In this run, the third batch was inserted at 05:33:07Z and was archived by the incremental cycle that started at 05:33:49Z and completed at 05:34:09Z.
Some time later (the fourth batch was inserted at 05:35:09Z), we insert two more batches into the same, still-running database — these rows must not appear in our restore.
Insert the fourth batch,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (14, 'Noah_shard0', 0), (15, 'Olivia_shard0', 0);
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (16, 'Paul_shard1', 1), (17, 'Quinn_shard1', 1), (18, 'Riley_shard1', 1);
:) exit
Insert the fifth batch,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (19, 'Sophia_shard0', 0), (20, 'Thomas_shard0', 0), (21, 'Uma_shard0', 0);
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) INSERT INTO demo_db.users VALUES (22, 'Victor_shard1', 1), (23, 'Willow_shard1', 1);
:) exit
The original database now has all 23 rows,
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name──────────┬─shard_id─┐
1. │ 1 │ Alice_shard0 │ 0 │
2. │ 2 │ Bob_shard0 │ 0 │
3. │ 5 │ Eve_shard0 │ 0 │
4. │ 6 │ Frank_shard0 │ 0 │
5. │ 9 │ Ivy_shard0 │ 0 │
6. │ 10 │ Jack_shard0 │ 0 │
7. │ 11 │ Kate_shard0 │ 0 │
8. │ 14 │ Noah_shard0 │ 0 │
9. │ 15 │ Olivia_shard0 │ 0 │
10. │ 19 │ Sophia_shard0 │ 0 │
11. │ 20 │ Thomas_shard0 │ 0 │
12. │ 21 │ Uma_shard0 │ 0 │
└────┴───────────────┴──────────┘
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name───────────┬─shard_id─┐
1. │ 3 │ Charlie_shard1 │ 1 │
2. │ 4 │ David_shard1 │ 1 │
3. │ 7 │ Grace_shard1 │ 1 │
4. │ 8 │ Henry_shard1 │ 1 │
5. │ 12 │ Leo_shard1 │ 1 │
6. │ 13 │ Mia_shard1 │ 1 │
7. │ 16 │ Paul_shard1 │ 1 │
8. │ 17 │ Quinn_shard1 │ 1 │
9. │ 18 │ Riley_shard1 │ 1 │
10. │ 22 │ Victor_shard1 │ 1 │
11. │ 23 │ Willow_shard1 │ 1 │
└────┴────────────────┴──────────┘
:) exit
$ kubectl exec -it -n demo sample-clickhouse-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT count() FROM demo_db.users_dist;
┌─count()─┐
1. │ 23 │
└─────────┘
:) exit
Note that we are not touching or dropping anything in the original sample-clickhouse database — it keeps running with all 23 rows. This lets us prove, side-by-side, that the restored database ends up with the older 13-row state (still correctly split across shards) instead of the latest data.
Point-in-time Recovery
Point-In-Time Recovery allows you to restore a ClickHouse database to a specific point in time using the continuously archived incremental backups. This is particularly useful in scenarios where you need to recover to a state just before a specific error or unwanted change occurred — without necessarily touching the original database.
We use the recovery timestamp 2026-09-30T05:34:00Z, which falls after the third batch was archived (incremental cycle started at 05:33:49Z) and before the fourth batch was inserted (05:35:09Z).
Create Restored ClickHouse CR:
apiVersion: kubedb.com/v1alpha2
kind: ClickHouse
metadata:
name: restored-clickhouse-pitr
namespace: demo
spec:
init:
archiver:
encryptionSecret:
name: encrypt-secret
namespace: demo
fullDBRepository:
name: sample-clickhouse-full
namespace: demo
recoveryTimestamp: "2026-09-30T05:34:00Z"
version: 25.7.1
clusterTopology:
clickHouseKeeper:
externallyManaged: false
spec:
replicas: 3
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
cluster:
name: appscode-cluster
shards: 2
replicas: 2
podTemplate:
spec:
containers:
- name: clickhouse
resources:
limits:
memory: 4Gi
requests:
cpu: 1
memory: 2Gi
initContainers:
- name: clickhouse-init
resources:
limits:
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
storage:
storageClassName: "local-path"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
deletionPolicy: WipeOut
Here,
spec.init.archiver.fullDBRepositoryrefers to theRepositorycreated by theClickHouseArchiver(named<ClickHouse-name>-fullby convention, i.e.sample-clickhouse-full).spec.init.archiver.recoveryTimestampspecifies the point in time to recover to. KubeDB will restore the latest full backup taken before this timestamp together with the latest incremental backup taken before it.spec.init.archiver.encryptionSecretrefers to the same encryption secret used by theClickHouseArchiver.
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.10.12/docs/guides/clickhouse/pitr/examples/restored-clickhouse-pitr.yaml
clickhouse.kubedb.com/restored-clickhouse-pitr created
Let’s check the pods which are related to the restore,
$ kubectl get pods -n demo | grep restored-clickhouse-pitr
restored-clickhouse-pitr-appscode-cluster-shard-0-0 1/1 Running 0 110s
restored-clickhouse-pitr-appscode-cluster-shard-0-1 1/1 Running 0 106s
restored-clickhouse-pitr-appscode-cluster-shard-1-0 1/1 Running 0 107s
restored-clickhouse-pitr-appscode-cluster-shard-1-1 1/1 Running 0 102s
restored-clickhouse-pitr-inc-backup-restorer-2fnpv 0/1 Completed 0 48s
restored-clickhouse-pitr-keeper-0 1/1 Running 0 113s
restored-clickhouse-pitr-keeper-1 1/1 Running 0 107s
restored-clickhouse-pitr-keeper-2 1/1 Running 0 103s
restored-clickhouse-pitr-manifest-restorer-hc6sp 0/1 Completed 0 2m6s
Here,
- Pod
restored-clickhouse-pitr-manifest-restorer-*is responsible for restoring the database manifest. - Pod
restored-clickhouse-pitr-inc-backup-restorer-*restores the latest full backup taken beforerecoveryTimestamptogether with the latest incremental backup taken beforerecoveryTimestamp.
Note: Restore process works sequentially. Manifest Restore –> Full-backup + Incremental Restore.
Let’s watch the underlying RestoreSession objects,
$ kubectl get restoresession -n demo
NAME REPOSITORY PHASE DURATION AGE
restored-clickhouse-pitr-inc-backup-restorer sample-clickhouse-full Succeeded 23s 49s
restored-clickhouse-pitr-manifest-restorer sample-clickhouse-full Succeeded 2s 2m6s
You can also see which backups were picked in the logs of the incremental restorer pod,
$ kubectl logs -n demo restored-clickhouse-pitr-inc-backup-restorer-2fnpv | grep -E "Starting restore|Selected Backup|completed successfully"
I0930 05:40:56.162546 1 pitr_restorer.go:115] Starting restore for snapshot: sample-clickhouse-full-sample-clarchiver-full-backup-1790746201
I0930 05:40:56.694446 1 helpers.go:495] Selected Backup For Restore: dau9uf9t3d5c73fgt720
I0930 05:41:16.907283 1 pitr_restorer.go:130] Restore completed successfully for snapshot: sample-clickhouse-full-sample-clarchiver-full-backup-1790746201
Here, the full backup sample-clickhouse-full-sample-clarchiver-full-backup-1790746201 (taken at 05:30:01Z) was restored together with the incremental backup dau9uf9t3d5c73fgt720, which is the incremental backup cycle started at 05:33:49Z — the last one before our recovery timestamp 05:34:00Z.
Verify Restored Data:
At first, check if the database has gone into Ready state by the following command,
$ kubectl get clickhouse -n demo restored-clickhouse-pitr
NAME VERSION STATUS AGE
restored-clickhouse-pitr 25.7.1 Ready 2m7s
Now, let’s exec into the pods to verify the restored data. The original sample-clickhouse database (which we never touched) still has all 23 rows, while the restored restored-clickhouse-pitr database — restored to 2026-09-30T05:34:00Z — should only have the 13 rows that existed at that point in time, on the same shards as before.
Since the manifest restorer also restores the auth secret of the original database, the restored database uses the same credentials as sample-clickhouse,
$ kubectl get secret -n demo restored-clickhouse-pitr-auth -o jsonpath='{.data.username}' | base64 -d
admin⏎
$ kubectl get secret -n demo restored-clickhouse-pitr-auth -o jsonpath='{.data.password}' | base64 -d
psL)PHO!)ryZEcoX⏎
Let’s check the data on every replica of each shard,
$ kubectl exec -it -n demo restored-clickhouse-pitr-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name─────────┬─shard_id─┐
1. │ 1 │ Alice_shard0 │ 0 │
2. │ 2 │ Bob_shard0 │ 0 │
3. │ 5 │ Eve_shard0 │ 0 │
4. │ 6 │ Frank_shard0 │ 0 │
5. │ 9 │ Ivy_shard0 │ 0 │
6. │ 10 │ Jack_shard0 │ 0 │
7. │ 11 │ Kate_shard0 │ 0 │
└────┴──────────────┴──────────┘
:) exit
$ kubectl exec -it -n demo restored-clickhouse-pitr-appscode-cluster-shard-0-1 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name─────────┬─shard_id─┐
1. │ 1 │ Alice_shard0 │ 0 │
2. │ 2 │ Bob_shard0 │ 0 │
3. │ 5 │ Eve_shard0 │ 0 │
4. │ 6 │ Frank_shard0 │ 0 │
5. │ 9 │ Ivy_shard0 │ 0 │
6. │ 10 │ Jack_shard0 │ 0 │
7. │ 11 │ Kate_shard0 │ 0 │
└────┴──────────────┴──────────┘
:) exit
$ kubectl exec -it -n demo restored-clickhouse-pitr-appscode-cluster-shard-1-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name───────────┬─shard_id─┐
1. │ 3 │ Charlie_shard1 │ 1 │
2. │ 4 │ David_shard1 │ 1 │
3. │ 7 │ Grace_shard1 │ 1 │
4. │ 8 │ Henry_shard1 │ 1 │
5. │ 12 │ Leo_shard1 │ 1 │
6. │ 13 │ Mia_shard1 │ 1 │
└────┴────────────────┴──────────┘
:) exit
$ kubectl exec -it -n demo restored-clickhouse-pitr-appscode-cluster-shard-1-1 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT * FROM demo_db.users ORDER BY id;
┌─id─┬─name───────────┬─shard_id─┐
1. │ 3 │ Charlie_shard1 │ 1 │
2. │ 4 │ David_shard1 │ 1 │
3. │ 7 │ Grace_shard1 │ 1 │
4. │ 8 │ Henry_shard1 │ 1 │
5. │ 12 │ Leo_shard1 │ 1 │
6. │ 13 │ Mia_shard1 │ 1 │
└────┴────────────────┴──────────┘
:) exit
And through the Distributed table,
$ kubectl exec -it -n demo restored-clickhouse-pitr-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SELECT count() FROM demo_db.users_dist;
┌─count()─┐
1. │ 13 │
└─────────┘
:) exit
Let’s also verify that both table definitions were restored,
$ kubectl exec -it -n demo restored-clickhouse-pitr-appscode-cluster-shard-0-0 -- clickhouse-client --user admin --password 'psL)PHO!)ryZEcoX'
:) SHOW CREATE TABLE demo_db.users;
CREATE TABLE demo_db.users
(
`id` UInt64,
`name` String,
`shard_id` UInt8
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/demo_db/users', '{replica}')
ORDER BY id
SETTINGS index_granularity = 8192
:) SHOW CREATE TABLE demo_db.users_dist;
CREATE TABLE demo_db.users_dist
(
`id` UInt64,
`name` String,
`shard_id` UInt8
)
ENGINE = Distributed('{cluster}', 'demo_db', 'users', rand())
:) exit
As shown above, restored-clickhouse-pitr has exactly the 13 rows that existed at 2026-09-30T05:34:00Z — ids 1, 2, 5, 6, 9, 10, 11 on both replicas of shard 0 and ids 3, 4, 7, 8, 12, 13 on both replicas of shard 1. The rows of the fourth and fifth batches (ids 14-23), which were inserted after the recovery timestamp, are correctly not present, while the original sample-clickhouse database still has all 23 rows. This confirms that spec.init.archiver.recoveryTimestamp recovers both the schema (ReplicatedMergeTree + Distributed tables) and the exact per-shard data to the specified historical point, rather than the latest available state.
Cleaning up
To cleanup the Kubernetes resources created by this tutorial, run:
kubectl delete clickhouse -n demo restored-clickhouse-pitr
kubectl delete clickhouse -n demo sample-clickhouse
kubectl delete clickhousearchiver -n demo sample-clickhouse-archiver
kubectl delete retentionpolicies.storage.kubestash.com -n demo demo-retention
kubectl delete backupstorage -n demo s3-storage
kubectl delete secret -n demo s3-secret encrypt-secret
































