Quickstart
This guide walks you through a minimal Klio installation: the Klio operator, one Klio server using Tier 1 (local) storage, and a PostgreSQL cluster streaming its WAL files and base backups to it.
Follow the steps in order — each one builds on the previous.
This is a deliberately minimal setup. It uses self-signed certificates, a single PostgreSQL instance and Tier 1 storage only. See Next steps for the reference documentation covering object storage, retention, sizing and production concerns.
Before you start
You need:
- A Kubernetes cluster and
kubectlconfigured to access it - A default storage class (check with
kubectl get storageclass) - Helm — see the Helm installation guide
- CloudNativePG already installed — see the CloudNativePG installation guide
- cert-manager already installed — see the cert-manager installation guide
- The Age CLI, to create the backup encryption key
This guide installs the operator into the cnpg-system namespace and
creates everything else in default.
The Klio operator must run in the same namespace as the
CloudNativePG operator. If you installed CloudNativePG somewhere
other than cnpg-system, replace cnpg-system throughout this guide
with your namespace.
Step 1: Install the Klio operator
Create a values.yaml file. The chart expects the Prometheus Operator by
default, so disable it unless you have it installed:
# The Prometheus Operator is not needed for this guide
prometheus:
enable: false
Install the chart:
helm install klio-operator \
oci://ghcr.io/cloudnative-pg/klio-operator-chart \
--version 0.0.20 \
--namespace cnpg-system \
-f values.yaml
Keep values.yaml — you will reuse it when
upgrading.
Check that the operator is running and its custom resource definitions were created:
kubectl get pods -n cnpg-system -l app.kubernetes.io/name=klio
kubectl get crds | grep klio.cnpg.io
You should see the operator pod in a Running state, along with the
servers.klio.cnpg.io and pluginconfigurations.klio.cnpg.io CRDs.
Step 2: Create the encryption key
Klio encrypts your backups at rest. The encryption key is itself protected with Age, so that the credential can be rotated later without touching the backup repository.
Generate an Age key pair:
age-keygen -o identity.txt
The command prints the corresponding public key:
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
Generate a random encryption key and encrypt it with that public key:
openssl rand -hex 32 | age -e \
-i identity.txt \
-o encryption-key.age
Store both files as Kubernetes secrets:
kubectl create secret generic klio-encryption-key-age \
--from-file=encryption-key.age
kubectl create secret generic klio-age-identity \
--from-file=identity.txt
Keep identity.txt and encryption-key.age somewhere safe outside
the cluster. Losing them means permanent, unrecoverable loss of access
to every backup. There is no key recovery mechanism.
Step 3: Create the certificates
Klio secures all traffic with TLS and authenticates clients with mutual TLS. This step creates three certificates with cert-manager:
- a CA, used to sign and verify client certificates
- a server certificate, presented by the Klio server
- a client certificate, presented by the PostgreSQL instances
Save the following as klio-certificates.yaml:
---
# Self-signed issuer, used to bootstrap the CA and the server
# certificate. Trust is established through configuration, so a
# self-signed root is not a security problem here.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-issuer
namespace: default
spec:
selfSigned: {}
---
# The CA that signs client certificates
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: klio-server-ca
namespace: default
spec:
commonName: klio-server-ca
secretName: klio-server-ca
duration: 2160h # 90d
renewBefore: 360h # 15d
isCA: true
usages:
- cert sign
issuerRef:
name: selfsigned-issuer
kind: Issuer
group: cert-manager.io
---
# An issuer backed by the CA above, used for client certificates
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: klio-server-ca-issuer
namespace: default
spec:
ca:
secretName: klio-server-ca
---
# The certificate presented by the Klio server
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: klio-server-tls
namespace: default
spec:
secretName: klio-server-tls
commonName: klio-server
dnsNames:
- klio-server
- klio-server.default
- klio-server.default.svc
- klio-server.default.svc.cluster.local
duration: 2160h # 90d
renewBefore: 360h # 15d
isCA: false
usages:
- server auth
issuerRef:
name: selfsigned-issuer
kind: Issuer
group: cert-manager.io
---
# The certificate presented by the PostgreSQL instances.
# The Common Name has the form "<user>@<clusterName>": the host part
# MUST match the clusterName in the PluginConfiguration below.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cluster-example-klio-user
namespace: default
spec:
secretName: cluster-example-klio-user
commonName: klio@cluster-example
duration: 2160h # 90d
renewBefore: 360h # 15d
isCA: false
usages:
- client auth
issuerRef:
name: klio-server-ca-issuer
kind: Issuer
group: cert-manager.io
Apply it and wait for cert-manager to issue everything:
kubectl apply -f klio-certificates.yaml
kubectl wait --for=condition=Ready certificate --all --timeout=120s
For production, use certificates signed by your organization's Certificate Authority instead of a self-signed root. See Authentication for the rules that client certificates must satisfy.
Step 4: Create the Klio server
The Server resource creates a StatefulSet running the Klio server,
along with the persistent volumes holding your backups.
Save the following as klio-server.yaml:
apiVersion: klio.cnpg.io/v1alpha1
kind: Server
metadata:
name: klio-server
namespace: default
spec:
image: ghcr.io/cloudnative-pg/klio:v0.0.20
# TLS certificate presented to clients
tlsSecretName: klio-server-tls
# CA used to verify client certificates
caSecretName: klio-server-ca
tier1:
# Kopia cache. The default Kopia cache is 5 GB of content plus
# 5 GB of metadata, so leave some headroom.
cache:
pvcTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# Base backups and the WAL archive
data:
pvcTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
encryptionKeyFile:
fileReference:
volume:
secret:
secretName: klio-encryption-key-age
path: encryption-key.age
identityFile:
fileReference:
volume:
secret:
secretName: klio-age-identity
path: identity.txt
# Work queue, required whenever tier1 is configured
queue:
pvcTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
Apply it:
kubectl apply -f klio-server.yaml
The volumes above use the default storage class. Set
storageClassName in each pvcTemplate to choose a different one,
and see Storage Requirements
for how to size them for real workloads.
Wait for the server pod to come up:
kubectl get pods -l klio.cnpg.io/klio-server=klio-server -w
The server initializes the backup repository on first start, so give
it a moment to reach Running. If it does not, check the logs:
kubectl logs -l klio.cnpg.io/klio-server=klio-server -f
Step 5: Configure the PostgreSQL cluster
Two resources are needed: a PluginConfiguration telling the Klio
plugin how to reach the server, and a CloudNativePG Cluster
referencing it.
Save the following as cluster-example.yaml:
---
apiVersion: klio.cnpg.io/v1alpha1
kind: PluginConfiguration
metadata:
name: klio-plugin-config
namespace: default
spec:
# The Klio server Service is named after the Server resource
serverAddress: klio-server.default
clientSecretName: cluster-example-klio-user
serverSecretName: klio-server-tls
# Must match the host part of the client certificate Common Name
clusterName: cluster-example
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
namespace: default
spec:
instances: 1
postgresql:
pg_hba:
# Required so that Klio can stream WAL files locally
- local replication all peer map=local
plugins:
- name: klio.cnpg.io
enabled: true
parameters:
pluginConfigurationRef: klio-plugin-config
storage:
size: 1Gi
Apply it:
kubectl apply -f cluster-example.yaml
The PluginConfiguration must exist before the Cluster. If it does
not, the Klio plugin gates reconciliation and the cluster waits
without progressing until you create it.
A few details worth noting:
- The
pg_hbaentry is what allows Klio to open a local replication connection and stream WAL. - Do not set
isWALArchiver. Klio does not rely on PostgreSQL'sarchive_commandat all: it streams WAL records to the server directly, over physical replication, which is more efficient. See WAL Streaming for how this works. clusterNamein thePluginConfigurationmust match the host part of the client certificate Common Name (klio@cluster-exampleabove). Klio refuses the connection on a mismatch.- Reusing the
Clustername asclusterName, as this guide does, is only a convention — the two are independent. See Cluster name override.
Step 6: Verify the setup
Check that the Klio server is ready:
kubectl get server klio-server
kubectl get pods -l klio.cnpg.io/klio-server=klio-server
Check that the PostgreSQL cluster is healthy:
kubectl get cluster cluster-example
Finally, confirm that WAL streaming has started. The plugin creates a
physical replication slot named klio on the primary, so PostgreSQL
itself can tell you it's connected:
kubectl cnpg psql cluster-example -- -x -c \
"SELECT state, sent_lsn, flush_lsn, sync_state
FROM pg_stat_replication WHERE application_name = 'klio'"
state should read streaming. See
Monitoring Klio WAL streamer in PostgreSQL
for what each field means.
You can also check it from the Klio server side:
kubectl logs -l klio.cnpg.io/klio-server=klio-server
You should see lines with the Received completed WAL file message.
Next steps
- Take your first backup and learn how to restore a cluster or perform point-in-time recovery
- The Klio Server — storage tiers, sizing the volumes, Tier 2 object storage, read-only servers, encryption and authentication
- The Klio Plugin — retention policies, Tier 2, WAL prefetch and sidecar customization
- Klio Operator Helm Chart — the full chart configuration reference and the upgrade procedure
- OpenTelemetry and Grafana dashboards — monitoring
The concepts behind what you just deployed:
- PostgreSQL Backup and Recovery — the WAL archive, base backups and point-in-time recovery
- WAL Streaming — why Klio replaces
archive_command - Architectures & Tiers — planning a backup strategy
- Data deduplication in Klio — how Kopia stores base backups efficiently