Short answer: backing up a Kubernetes cluster means backing up three distinct things — the API objects (Deployments, Services, Secrets, CRDs), the persistent volumes, and the application data in its logical format (a pg_dump, not just a block-level snapshot). Velero covers the first two: installed with an S3 bucket at OVHcloud or Scaleway, it costs roughly €1 to €5 per month in storage for a small-business cluster and backs up a namespace in a few minutes. The third is on you, and it's the one that will really save you.
The conversation always starts the same way. The cluster is running, CI is deploying, the volumes sit on replicated block storage at the provider — so "it's backed up." Then someone deletes the wrong namespace with a kubectl delete ns on a Friday evening, or a helm uninstall takes a PVC with it because its reclaimPolicy is set to Delete, and you discover that replicated block storage protects against a disk failure, not against human error.
This article describes the setup we deploy by default on the clusters we operate, and above all the restore procedure we rehearse periodically. Because a backup that has never been restored isn't a backup: it's a line on a dashboard.

Exactly what needs backing up in a cluster
Let's break the problem down. In a Kubernetes cluster serving a small-business application, there are four categories of data, and they aren't backed up the same way.
| Category | Example | Right tool |
|---|---|---|
| API objects | Deployments, Ingress/HTTPRoute, ConfigMaps, Secrets, CRDs | Velero (export to S3) |
| Persistent volumes | user uploads, search index, cache | Velero + CSI snapshots, or File System Backup (Kopia) |
| Databases | PostgreSQL, persistent Redis | dedicated logical backup (pg_dump, WAL archiving) |
| Cluster infrastructure | nodes, network, IAM | Terraform / OpenTofu, not a backup |
Two classic traps hide in that table.
The first: a volume snapshot is not a consistent database backup. A CSI snapshot taken while PostgreSQL is writing gives you, at best, the equivalent of a hard power-off: PostgreSQL will replay its journal at startup and will usually come back fine, but you have no guarantee of point-in-time recovery, and no protection against logical corruption (an UPDATE without a WHERE that makes it to production). The PostgreSQL documentation only considers a file system snapshot valid if the volume is a single one and the snapshot atomic — a condition rarely met in practice on networked storage. For the database, keep continuous WAL archiving, which is precisely what an operator like CloudNativePG handles; we detailed the cost trade-offs in our comparison between managed PostgreSQL and PostgreSQL inside the cluster.
The second: the cluster itself should not be in the backup. If the only way to rebuild your cluster is to restore an etcd snapshot, you have an infrastructure problem, not a backup problem. Nodes, network, security groups and IAM roles should come from a Terraform or OpenTofu repository. Velero rebuilds the workloads, not the control plane.
Installing Velero with an OVHcloud or Scaleway S3 bucket
Velero is a CNCF project, currently in incubation, originally developed by Heptio and then VMware. It works with any S3-compatible object storage, which includes OVHcloud Object Storage (both the legacy and the new S3 offering), Scaleway Object Storage and of course Amazon S3.
Start by creating the bucket and a dedicated set of access keys. At Scaleway, the endpoint looks like https://s3.fr-par.scw.cloud with region fr-par; at OVHcloud, https://s3.gra.io.cloud.ovh.net with region gra for the S3-compatible offering. Always check the exact value in the provider's console — these endpoints have changed several times.
The credentials file, in the classic AWS format:
# velero-credentials
[default]
aws_access_key_id=SCWXXXXXXXXXXXXXXXXX
aws_secret_access_key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Then the installation, using the Velero CLI (available from the project's GitHub releases):
velero install \
--provider aws \
--plugins velero/velero-plugin-for-aws:v1.11.0 \
--bucket studio-cluster-backup \
--secret-file ./velero-credentials \
--backup-location-config \
region=fr-par,s3ForcePathStyle=true,s3Url=https://s3.fr-par.scw.cloud \
--use-node-agent \
--uploader-type kopia \
--default-volumes-to-fs-backup=false \
--namespace velero
Three options deserve an explanation.
--use-node-agentdeploys a DaemonSet that can read the contents of the volumes mounted on each node. It is essential for file-level backup (File System Backup) of volumes whose CSI driver doesn't support snapshots.--uploader-type kopiaselects Kopia rather than Restic. Restic has been deprecated on the Velero side since v1.15; on a fresh cluster, Kopia is the default choice.--default-volumes-to-fs-backup=falseavoids backing up everything at file level by default: we favour CSI snapshots, which are far faster, and switch to file mode case by case with an annotation.
Check that the backup storage location is reachable before going any further:
kubectl -n velero get backupstoragelocation
# NAME PHASE LAST VALIDATED AGE
# default Available 12s 2m
A PHASE: Unavailable means, nine times out of ten, a wrong endpoint, a forgotten s3ForcePathStyle, or keys without write permission on the bucket.
Enabling CSI snapshots
For Velero to request VolumeSnapshot objects from the provider's driver instead of copying files, you need a labelled VolumeSnapshotClass:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: csi-block-snapclass
labels:
velero.io/csi-volumesnapshot-class: "true"
driver: csi.cinder.openstack.org # adapt to the provider's driver
deletionPolicy: Retain
Watch out for one point that is often discovered too late: at many providers, a volume snapshot stays in the same region, sometimes even the same zone, as the original volume. If the goal is to survive the loss of a zone, you need to enable Velero's DataMover (snapshotMoveData: true), which reads the snapshot and pushes it to the object bucket. It's slower, but it's the only configuration that genuinely gets your data out of the cluster.
The schedules we put in place
A single "whole cluster, once a night" schedule isn't enough: namespaces don't share the same value or the same rate of change. We use three Schedule objects.
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: prod-hourly
namespace: velero
spec:
schedule: "17 * * * *"
template:
includedNamespaces:
- app-prod
snapshotVolumes: true
snapshotMoveData: true
ttl: 72h
---
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: full-cluster-daily
namespace: velero
spec:
schedule: "25 2 * * *"
template:
excludedNamespaces:
- kube-system
- velero
snapshotVolumes: true
ttl: 720h
Notes from running this in production:
- Never
0 * * * *. Every scheduled job in the world runs on the hour; shifting by a few minutes avoids contention on object storage. - The
ttldrives retention, and retention drives the bill. 72 hours of hourly backups plus 30 days of daily backups is a reasonable compromise for a small-business application. Secretobjects are included by default. They therefore land in the bucket in plain text — encrypted server-side, but readable by anyone holding the access keys. For sensitive data, Velero can encrypt client-side via the Kopia repository (repositoryMaintenanceand a repository password): this has to be configured explicitly, it isn't the default setting.

What it actually costs
Take a cluster typical of our clients: three nodes, one production namespace, 60 GB of persistent volumes of which 25 GB is a PostgreSQL database and 35 GB uploads.
| Item | Volume | Indicative monthly cost |
|---|---|---|
| Object storage (Velero backups, ~90 GB after Kopia dedup) | 90 GB | €0.70 to €2 |
| Block storage snapshots kept in region | 60 GB | €2 to €5 |
| Egress traffic during a full restore | 60 GB one-off | €0 at OVHcloud and Scaleway |
| Operations time (quarterly restore test) | 2 h / quarter | the real cost |
Scaleway's and OVHcloud's public price lists put object storage at around €0.01 per GB per month in the standard class, and neither provider charges for data egress — a notable difference from AWS S3, where outbound transfer is billed and a 60 GB restore costs a few euros. Always check today's pricing: these numbers move.
In other words: backing up a small-business cluster costs less than €10 per month in infrastructure. The real line item is the engineering hour spent verifying that it works. That's also what we build into the operations contracts described on our infrastructure services page.
Restoring, step by step
Here is the procedure we rehearse, under real conditions, on a staging namespace.
1. List the available backups and inspect the target.
velero backup get
velero backup describe prod-hourly-20261109021700 --details
The --details flag shows the resources included and the state of each volume. A backup in PartiallyFailed is common and often harmless (an unbackupable webhook resource), but you must read the warnings before relying on it.
2. Restore into a fresh namespace, not on top of production.
velero restore create recette-20261109 \
--from-backup prod-hourly-20261109021700 \
--namespace-mappings app-prod:app-recette \
--wait
Namespace remapping is the key to a non-destructive test: you get a full copy of production in app-recette, without touching app-prod.
3. Neutralise anything that must not wake up.
A restored production copy keeps its CronJobs, its outbound hooks and its SMTP credentials. Before making it reachable, suspend the CronJobs and cut the external integrations:
kubectl -n app-recette get cronjob -o name \
| xargs -I{} kubectl -n app-recette patch {} \
-p '{"spec":{"suspend":true}}'
This is also where you check the network exposure objects: an HTTPRoute restored with the same hostname as production will create a routing conflict. We describe these objects in detail in the article on replacing ingress-nginx with the Gateway API.
4. Verify the data, not the pods.
The classic restore-test trap: every pod is Running, victory is declared, and nobody has opened the database. Our three minimum checks:
# row count of the largest table
kubectl -n app-recette exec deploy/postgres -- \
psql -U app -d app -c "select count(*) from orders;"
# timestamp of the most recent record
kubectl -n app-recette exec deploy/postgres -- \
psql -U app -d app -c "select max(created_at) from orders;"
# a known file in the uploads volume
kubectl -n app-recette exec deploy/web -- \
ls -l /app/uploads/2026/
The second command gives you the real RPO (maximum data loss): the gap between max(created_at) and the backup time. If you're targeting a one-hour RPO and the last record dates from four hours before the backup, the problem is in the pipeline, not in the restore.
5. Time it and write it down.
Record the actual duration: that's your RTO. On the clusters we operate, a full restore of a 60 GB namespace with DataMover takes 20 to 45 minutes, most of it transfer from object storage. If your commitment is "back up and running in 30 minutes," you've just discovered it isn't true.
6. Delete the copy.
velero restore delete recette-20261109
kubectl delete ns app-recette

The four mistakes we see most often
reclaimPolicy: Deleteon production StorageClasses. Ahelm uninstallor akubectl delete pvcthen destroys the underlying volume, immediately. Switch any StorageClass that carries data toRetain, even if it means cleaning up orphaned volumes by hand.- Backups in the same account or project as the cluster. If compromising one set of keys lets an attacker delete both the cluster and the backup bucket, you don't have a backup. Use a separate project, write-only keys, and Object Lock when the provider offers it.
- No monitoring of the
Scheduleobjects. A backup failing silently for three weeks is the most common scenario by far. Velero exposes Prometheus metrics:velero_backup_last_successful_timestampandvelero_backup_failure_totalare enough for a "last successful backup older than 26 h" alert. - Secrets that aren't in the backup because they come from elsewhere. With External Secrets Operator or Sealed Secrets, restored
Secretobjects may be empty or invalid. Test the restore with the vault unavailable to measure the dependency — exactly the kind of detail you discover at the same time as the outage, like the private registry authentication covered in our guide to private GitLab images on Kubernetes.
Frequently asked questions
Does Velero replace my PostgreSQL database backup?
No. Velero can snapshot the database volume, which protects you from accidentally deleting the PVC, but it gives you neither point-in-time recovery nor any guarantee of transactional consistency. Keep a dedicated pipeline: continuous WAL archiving with an operator, or the automatic backups of a managed database. In practice, we exclude database volumes from Velero backups (annotation backup.velero.io/backup-volumes-excludes) to avoid paying twice for the same storage.
Should I back up etcd as well?
On a managed cluster (OVHcloud Managed Kubernetes, Scaleway Kapsule, EKS), no: the control plane is the provider's responsibility and you don't have access to it. On a self-managed cluster, a daily etcdctl snapshot save is useful for restoring an entire cluster, but it restores all your mistakes too. Velero remains preferable for targeted rollbacks.
How often should a restore be tested?
Once a quarter at a minimum, and after every major Kubernetes or CSI driver version change. The test takes two hours when it's scripted. We register it as a scheduled job in the pipeline, with the report archived: date, backup used, measured RTO, anomalies.
Can Velero be used to migrate a cluster to another provider?
Yes, and it's one of its most useful applications: back up from the source cluster with snapshotMoveData: true, read the same bucket from the target cluster, then run velero restore create. The friction points are the StorageClasses (different names, to be remapped with a ConfigMap of type change-storage-class-config), the LoadBalancers and the provider-specific annotations. Budget a solid day of fine-tuning, not a single command.
Where should the restore test sit in CI?
In a weekly scheduled job, separate from the application pipeline: restore into a disposable namespace, run the verification queries, delete, publish the RTO as a metric. The pipeline template we use is described in our article on the GitLab CI/CD pipeline for Next.js on Kubernetes.
In summary
- A cluster is backed up on three levels: API objects, persistent volumes, logical data. Velero covers the first two, not the third.
- Install Velero with Kopia (Restic is deprecated), an S3 bucket at OVHcloud or Scaleway, and
snapshotMoveData: trueif you want to survive the loss of a zone. - Three schedules beat one: hourly on production with a short
ttl, daily across the whole cluster with long retention. - The infrastructure cost is low — under €10 per month for 60 GB of data on a small-business cluster; the real cost is the testing time.
- Always restore into a remapped namespace, suspend the CronJobs, then verify the data (row counts, timestamp of the latest record), not the state of the pods.
- Measure RPO and RTO at every test and write them down. An untimed recovery commitment is a hypothesis.
- Switch data-bearing StorageClasses to
reclaimPolicy: Retain, isolate the backup bucket from the cluster's account, and alert onvelero_backup_last_successful_timestamp.
If you'd like your backup pipeline audited, or simply want a real restore timed on your cluster, talk to our team or write to us via the contact page.


