VPS Snaps

How to back up a Kubernetes cluster with Velero

Velero copies a cluster's Kubernetes objects into an object storage bucket, and its persistent volumes either as snapshots or by copying their files with Kopia. Install it with velero install, schedule backups with velero schedule create, and restore with velero restore create --from-backup. Everything below was checked against the Velero v1.18 documentation (current release v1.18.4) and velero-plugin-for-aws v1.14.4.

9 min readUpdated Checked against official documentation

What Velero backs up

Each backup has two parts. Kubernetes objects, from Deployments to Secrets and PersistentVolumeClaims, are read through the Kubernetes API and uploaded to your bucket as a tarball. Volume data is handled in one of three ways:

MethodHow it worksWhere the data ends up
Provider snapshotsA volume snapshotter plugin asks the cloud to snapshot each disk, for example Amazon EBS through the AWS pluginWith the cloud provider, in the disk's region
CSI snapshotsVelero creates CSI VolumeSnapshots. Needs --features=EnableCSI and a CSI driver that supports snapshotsIn the storage system, unless you also move the data with --snapshot-move-data
File System BackupA node-agent DaemonSet reads each pod volume's files and uploads them with KopiaIn your bucket, deduplicated and incremental

On a self-managed cluster without a snapshot-capable CSI driver, File System Backup is the practical choice, and it keeps volume data in the same bucket. The examples below use it.

A Velero backup is not atomic: objects created or changed while it runs may be missed. File System Backup copies live files, so a database volume can be caught mid-write; use a backup hook (below) or snapshots for databases. hostPath volumes are not supported; local persistent volumes are.

Install the CLI

The velero CLI runs on your workstation and uses your kubeconfig, like kubectl. On Linux, download the release and its checksums, verify, and install:

Terminal
curl -fsSLO https://github.com/velero-io/velero/releases/download/v1.18.4/velero-v1.18.4-linux-amd64.tar.gz
Terminal
curl -fsSLO https://github.com/velero-io/velero/releases/download/v1.18.4/CHECKSUM
Terminal
sha256sum --check --ignore-missing CHECKSUM
Terminal
tar -xvf velero-v1.18.4-linux-amd64.tar.gz
Terminal
sudo install velero-v1.18.4-linux-amd64/velero /usr/local/bin/velero
Terminal
velero version --client-only

Use arm64 in the file names on ARM machines. On macOS, brew install velero installs it.

Prepare the bucket and credentials

Velero needs a private bucket, ideally one per cluster, and an access key that can read, write, list and delete objects in it. S3-compatible services work through the AWS plugin. To create a bucket and key, see the guides for AWS S3, Cloudflare R2, Backblaze B2, Wasabi and DigitalOcean Spaces.

credentials-velero
[default]
aws_access_key_id=YOUR_ACCESS_KEY_ID
aws_secret_access_key=YOUR_SECRET_ACCESS_KEY

velero install stores this file in a Secret named cloud-credentials in the velero namespace. Keep the keys somewhere safe outside the cluster: restoring into a new cluster needs them.

Install Velero in the cluster

velero install targets your current kubectl context, so check it with kubectl config current-context first. Then:

Terminal
velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.14.4 \
  --bucket my-cluster-backups \
  --prefix prod \
  --secret-file ./credentials-velero \
  --backup-location-config region=us-east-1,s3ForcePathStyle="true",s3Url=https://s3.example.com \
  --use-volume-snapshots=false \
  --use-node-agent \
  --wait
  • --provider aws selects the AWS object store plugin, which speaks the S3 API.
  • --plugins is the plugin image. Plugin v1.14.x pairs with Velero v1.18.x; check the compatibility table in the plugin's README when you upgrade.
  • --bucket is the bucket. --prefix is optional and puts everything under a folder, so several clusters can share one bucket.
  • --secret-file is the credentials file above.
  • --backup-location-config holds the S3 settings: the region, your provider's endpoint as s3Url, and s3ForcePathStyle="true" for path-style URLs, which self-hosted services such as MinIO need.
  • --use-volume-snapshots=false skips the snapshot location, since this setup copies files instead.
  • --use-node-agent installs the node-agent DaemonSet that File System Backup needs.
  • --wait returns only when the Velero deployment is ready.

On AWS itself, drop s3Url and s3ForcePathStyle. To use EBS snapshots there, replace --use-volume-snapshots=false with --snapshot-location-config region=us-east-1.

The plugin's README lists Backblaze B2 and Ceph as rejecting uploads with XAmzContentSHA256Mismatch unless you add checksumAlgorithm="" to --backup-location-config, which stops the plugin sending its default CRC32 checksum.

Check that the pods run and that Velero can reach the bucket:

Terminal
kubectl -n velero get pods
Terminal
velero backup-location get

The PHASE column must say Available. Unavailable means the bucket, endpoint, region or keys are wrong, and kubectl -n velero logs deployment/velero says which.

Choose which volumes to copy

File System Backup copies only the pod volumes you select. There are two ways to select them:

  • Opt-in, the default: annotate pods with backup.velero.io/backup-volumes and the names of the volumes to copy, as written in the pod spec.
  • Opt-out: add --default-volumes-to-fs-backup to velero backup create, or to velero install to make it the default. Every pod volume is copied except service account tokens, Secrets, ConfigMaps, hostPath volumes, and volumes you list in backup.velero.io/backup-volumes-excludes.

An annotation added to a running pod disappears when its Deployment or StatefulSet replaces the pod, so put it in the pod template. Changing the template restarts the pods:

deployment.yaml
spec:
  template:
    metadata:
      annotations:
        backup.velero.io/backup-volumes: data

For a database, add a pre-backup hook that writes a dump into the volume, so Velero copies a clean file alongside the live data files. The command is a JSON array and runs without a shell unless you start one, and hooks time out after 30 seconds unless you set a timeout:

statefulset.yaml
spec:
  template:
    metadata:
      annotations:
        backup.velero.io/backup-volumes: data
        pre.hook.backup.velero.io/container: postgres
        pre.hook.backup.velero.io/command: '["/bin/sh", "-c", "pg_dump -U postgres -Fc -f /var/lib/postgresql/data/velero.dump postgres"]'
        pre.hook.backup.velero.io/timeout: 10m

Back up

Terminal
velero backup create shop-first --include-namespaces shop --default-volumes-to-fs-backup --wait
  • shop-first is the backup's name.
  • --include-namespaces shop limits the backup to one namespace. Leave it out to back up all namespaces and cluster-scoped objects; --exclude-namespaces skips the ones you list.
  • With a namespace filter, cluster-scoped objects such as StorageClasses and ClusterRoles are left out, apart from related ones like the PersistentVolumes of included claims. Add --include-cluster-resources=true to include every cluster-scoped object.
  • --default-volumes-to-fs-backup copies every pod volume. Leave it out if you annotate instead.
  • --wait blocks until the backup finishes.

Then check it:

Terminal
velero backup describe shop-first --details

Look for Phase: Completed; PartiallyFailed means some items or volumes failed. The pod volume backups section lists each volume Kopia copied, so a volume missing there was never selected. velero backup logs shop-first prints the full log. Both commands fetch from the bucket through temporary signed URLs, so your machine must be able to reach the bucket's endpoint.

Schedule backups with retention

Terminal
velero schedule create shop-daily --schedule="0 3 * * *" --include-namespaces shop --default-volumes-to-fs-backup --ttl 168h0m0s
  • --schedule takes a five-field cron expression in UTC, here 03:00 daily. @every 6h also works, and CRON_TZ=Europe/Berlin 0 3 * * * sets a time zone.
  • --ttl 168h0m0s keeps each backup for seven days; the default is 30 days. Expired backups are deleted from the bucket with their snapshots, and File System Backup data is cleared later by repository maintenance.
  • The velero backup create filters work here too.

Scheduled backups are named after the schedule plus a timestamp, such as shop-daily-20261003030000. Trigger one now to test the schedule, then list your schedules:

Terminal
velero backup create --from-schedule shop-daily
Terminal
velero schedule get

Delete backups with velero backup delete NAME. kubectl delete backup removes only the Kubernetes object and leaves the files in the bucket.

Velero encrypts File System Backup data with a static, common key unless you change it, so anyone who can read the bucket can read the data. Keep the bucket private. To use your own password, set the repository-password key of the velero-repo-credentials Secret before the first backup; changing it later cuts Velero off from older backups.

Restore

Velero never overwrites what is in the cluster: existing objects are skipped, and ServiceAccounts are merged. To test a backup on a live cluster, restore into a new namespace:

Terminal
velero restore create shop-test --from-backup shop-first --namespace-mappings shop:shop-restore-test --wait

--namespace-mappings old:new renames the namespace on the way in. Claims are created fresh and provisioned by their StorageClass, and an init container holds each pod until its data is copied back. Check the result, then delete the test namespace:

Terminal
velero restore describe shop-test --details

Look for Phase: Completed and read the warnings and errors; already exists only means an object was skipped. velero restore logs shop-test has the full log.

For a real recovery, delete the broken namespace, then restore it:

Terminal
kubectl delete namespace shop
Terminal
velero restore create --from-backup shop-first --wait

--from-schedule shop-daily restores a schedule's latest successful backup instead. --include-namespaces and --include-resources restore part of a backup, and --existing-resource-policy=update updates existing objects instead of skipping them, on a best-effort basis.

Restore into a new cluster

Point kubectl at the new cluster and run the same velero install command, with the same bucket, prefix, credentials, plugin and namespace. Then make the location read-only, so the new cluster cannot delete or add backups while you work:

Terminal
kubectl patch backupstoragelocation default --namespace velero --type merge --patch '{"spec":{"accessMode":"ReadOnly"}}'

Velero syncs the backup list from the bucket every minute. When velero backup get shows your backup, restore it with velero restore create --from-backup as above. Patch accessMode back to ReadWrite once the new cluster takes over.

If the new cluster's StorageClasses have different names, restored claims stay Pending. Map old names to new ones before restoring, with a ConfigMap in the velero namespace; the labels must be exactly these:

change-storage-class.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: change-storage-class-config
  namespace: velero
  labels:
    velero.io/plugin-config: ""
    velero.io/change-storage-class: RestoreItemAction
data:
  local-path: do-block-storage
Terminal
kubectl apply -f change-storage-class.yaml

The new cluster must run the same Kubernetes version or newer. Cloud snapshots cannot move between providers, or between regions with the AWS and Azure plugins; File System Backup data can, because it lives in the bucket. If you set your own repository password, set the same one here before restoring.

Common errors

SymptomFix
velero backup-location get shows UnavailableWrong bucket, endpoint, region or keys. kubectl -n velero logs deployment/velero names the problem.
XAmzContentSHA256Mismatch in the logsThe storage rejects the plugin's checksum. Add checksumAlgorithm: "" under spec.config with kubectl -n velero edit backupstoragelocation default.
Backups stay in NewThe Velero server is not running. Check kubectl -n velero describe pods and the deployment's logs.
A backup is stuck InProgress after Velero restartedVelero cannot resume it. Delete it with kubectl -n velero delete backup NAME and run it again.
No volume data in the backupNo node agent (--use-node-agent), or the volume was not selected: annotate it or use --default-volumes-to-fs-backup.
Restored pods stay in InitThey are waiting for their volume data. Check kubectl -n velero get podvolumerestores -l velero.io/restore-name=NAME; the node agent must run in this cluster.
Restored claims stay PendingTheir StorageClass does not exist in this cluster. Map it with the change-storage-class ConfigMap.
SignatureDoesNotMatch from velero backup logsThe storage must accept signature version 4 signed URLs. On Ceph, use a native Ceph account.

Set the schedule from the data loss you can accept: see RPO and RTO explained. A bucket with Object Lock keeps a compromised cluster from deleting its own backups: see protecting backups from ransomware.

Frequently asked questions

Does Velero back up etcd?
No. Velero reads objects through the Kubernetes API and handles volume data with snapshots or File System Backup. It does not copy etcd's data files.
How long does Velero keep backups?
30 days by default. Set --ttl on velero backup create or velero schedule create, for example --ttl 168h0m0s for seven days.
Can I restore a Velero backup to a different cluster?
Yes. Install Velero in the new cluster against the same bucket and prefix, wait for the backup list to sync, and run velero restore create. The new cluster needs the same Kubernetes version or newer.
Is Velero's file system backup safe for databases?
Not on its own. It copies live files, so a database can be caught mid-write. Add a pre-backup hook that dumps the database into the volume, or use snapshots.
Does Velero work with S3-compatible storage?
Yes, through the AWS plugin with s3Url and region in the location config. Velero's docs say these services are reported working by users, not tested regularly by the Velero team.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: