Kubernetes Backup and Restore with Velero
Velero is an open-source tool that backs up and restores Kubernetes cluster resources and persistent volumes. It runs a server inside the cluster and a CLI on your workstation, and stores backups in S3-compatible object storage.
In this guide we install Velero on a Vietnix Cloud Kubernetes cluster, store backups in Vietnix Cloud S3, then back up and restore a sample application — including its persistent volumes.
How it works
- The Velero server runs as a Deployment in the
veleronamespace. - Each backup (resource metadata plus volume data) is stored in an S3 bucket.
- Persistent volumes are captured with CSI snapshots, and with the built-in data mover the snapshot data is moved to the bucket.
- Velero can also perform file system backups (Kopia) for volumes that do not support snapshots.
Prerequisites
- A Kubernetes cluster in Vietnix Cloud — Create Cluster and Connect Cluster.
- An S3 bucket and an access key — Manage Bucket and Manage Access Key.
- kubectl and Helm installed on your workstation (see Connect Cluster).
- The Velero CLI installed on your workstation. See the Velero basic install guide.
- A StorageClass backed by a CSI driver that supports snapshots. On Vietnix Cloud use
cinder.csi.openstack.organd set an explicit volume type (see Step 1). - Enough block storage (Cinder) quota in your project to create the persistent volumes you want to back up, since CSI snapshots are taken from Cinder volumes.
Step 1: Create a default StorageClass
If your cluster does not already have a default StorageClass, create one that uses the Vietnix Cloud CSI provisioner:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: default
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: cinder.csi.openstack.org
parameters:
type: nvmer3
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
kubectl apply -f storage-class.yaml
kubectl get storageclass
parameters.type selects the Cinder volume type. On Vietnix Cloud this is nvmer3. If your project uses a different volume type, change it accordingly — otherwise volume creation may fail with gigabytes_default ... quota is 0G.
Step 2: Check the CSI snapshot support
Velero uses the Kubernetes CSI snapshot APIs, so the VolumeSnapshot CRDs and the snapshot controller must be present. Check them:
kubectl api-resources | grep volumesnapshot
volumesnapshotclasses vsclass,vsclasses snapshot.storage.k8s.io/v1 false VolumeSnapshotClass
volumesnapshotcontents vsc,vscs snapshot.storage.k8s.io/v1 false VolumeSnapshotContent
volumesnapshots vs snapshot.storage.k8s.io/v1 true VolumeSnapshot
If the output lists VolumeSnapshot, VolumeSnapshotClass, and VolumeSnapshotContent, you can skip the rest of this step. Otherwise, install the external-snapshotter:
git clone https://github.com/kubernetes-csi/external-snapshotter/
cd external-snapshotter
# Check out the release compatible with your Kubernetes version
git checkout v8.6.0
kubectl apply -f client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
kubectl apply -f deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
Choose the external-snapshotter release that matches your Kubernetes version — see the compatibility matrix in the external-snapshotter repository. The example above (v8.6.0) targets recent Kubernetes versions.
Step 3: Create a VolumeSnapshotClass
Create a VolumeSnapshotClass for the Cinder CSI driver, with the label Velero uses to select it:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: cinder-snapclass
labels:
velero.io/csi-volumesnapshot-class: "true"
driver: cinder.csi.openstack.org
deletionPolicy: Delete
parameters:
force-create: "true"
kubectl apply -f snapshotclass.yaml
kubectl get volumesnapshotclass
NAME DRIVER DELETIONPOLICY AGE
cinder-snapclass cinder.csi.openstack.org Delete 1s
Step 4: Prepare the S3 credentials
Create a credentials file with your Vietnix Cloud S3 access key and secret key (see S3 Storage overview for the endpoint):
[default]
aws_access_key_id=<YOUR_ACCESS_KEY>
aws_secret_access_key=<YOUR_SECRET_KEY>
Step 5: Install Velero with Helm
Create a values file that points Velero at your S3 bucket. Replace the bucket name with the bucket you created for backups:
deployNodeAgent: true
snapshotsEnabled: false
initContainers:
- name: velero-plugin-for-aws
image: velero/velero-plugin-for-aws:v1.14.2
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /target
name: plugins
configuration:
features: EnableCSI
backupStorageLocation:
- name: default
provider: aws
bucket: <YOUR_BUCKET_NAME>
prefix: velero
default: true
config:
region: vn-hcm-1
s3Url: https://s3.vn-hcm-1.vietnix.cloud
s3ForcePathStyle: "true"
signatureVersion: "v4"
Install the chart:
helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm repo update
helm install velero vmware-tanzu/velero \
--namespace velero \
--create-namespace \
--set-file credentials.secretContents.cloud=credentials-velero \
-f velero-values.yaml
Where:
deployNodeAgent: truedeploys the node agent that is required for the data mover and file system backups.snapshotsEnabled: falsestops the chart from creating a defaultVolumeSnapshotLocation, which is not used for CSI snapshots.features: EnableCSIenables CSI snapshot support on the Velero server. Without it, Velero falls back to the object-storage plugin's native snapshotter and cannot move snapshot data.provider: awsis used because Velero talks to S3-compatible storage through the AWS plugin.s3Urlandregionare the Vietnix Cloud S3 endpoint and region.s3ForcePathStyle: "true"is required for S3-compatible endpoints.
CSI snapshot support is built into Velero (since v1.14), but you must enable the EnableCSI feature flag on the server — this is why configuration.features: EnableCSI is set in the values file. Without it, Velero uses the object-storage plugin's native snapshotters and refuses to move snapshot data.
Step 6: Verify the installation
kubectl get pods -n velero
kubectl get backupstoragelocations -n velero
velero version
The Velero server, the node agent, and the default backup storage location should all be ready:
NAME READY STATUS RESTARTS AGE
node-agent-xxxxx 1/1 Running 0 1m
node-agent-yyyyy 1/1 Running 0 1m
node-agent-zzzzz 1/1 Running 0 1m
velero-xxxxxxxxxx-aaaaa 1/1 Running 0 1m
NAME PHASE LAST VALIDATED AGE DEFAULT
default Available 57s 4m true
Client:
Version: v1.18.2
Server:
Version: v1.18.1
Enable the CSI feature on the Velero client so that velero backup describe shows CSI snapshot details:
velero client config set features=EnableCSI
Step 7: Deploy a sample application
To demonstrate a backup, deploy a small application that stores its data on a persistent volume:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
labels:
app: demo-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
labels:
app: demo-app
spec:
replicas: 1
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: web
image: nginx:stable
ports:
- containerPort: 80
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
command: ["/bin/sh", "-c"]
args:
- echo "Hello from the Velero demo" > /usr/share/nginx/html/index.html && exec nginx -g 'daemon off;'
volumes:
- name: data
persistentVolumeClaim:
claimName: demo-pvc
---
apiVersion: v1
kind: Service
metadata:
name: demo-app
labels:
app: demo-app
spec:
selector:
app: demo-app
ports:
- port: 80
kubectl apply -f demo-app.yaml
kubectl get deployments,pods,pvc
Wait until the pod is running and the volume is bound:
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/demo-app 1/1 1 1 30s
NAME READY STATUS RESTARTS AGE
pod/demo-app-647dc64f7-ztwfq 1/1 Running 0 30s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/demo-pvc Bound pvc-72252ff1-9785-4345-a760-a59da46897ba 1Gi RWO default 30s
Step 8: Create a backup
Back up every resource that carries the app=demo-app label:
velero backup create demo-b1 \
--selector app=demo-app \
--snapshot-volumes --snapshot-move-data
Where:
--selector app=demo-appselects the sample application resources.--snapshot-volumescaptures persistent volumes with CSI snapshots.--snapshot-move-datamoves the snapshot data to the S3 bucket.
Check the backup and the data movement:
kubectl get backups -n velero
kubectl get datauploads -n velero
velero backup describe demo-b1 --details
NAME STATUS ERRORS WARNINGS CREATED EXPIRES STORAGE LOCATION SELECTOR
demo-b1 Completed 0 0 2026-09-15 11:42:06 +0700 +07 29d default app=demo-app
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
demo-b1-xxxxx Completed 68s 27 27 default 77s node-2
The backup Phase should be Completed, and the DataUpload objects should be Completed. In velero backup describe, the CSI snapshot is shown under Backup Item Operations and Backup Volumes:
Phase: Completed
Backup Item Operations:
Operation for persistentvolumeclaims default/demo-pvc:
Backup Item Action Plugin: velero.io/csi-pvc-backupper
Phase: Completed
Progress description: Completed
Backup Volumes:
CSI Snapshots:
default/demo-pvc:
Data Movement:
Data Mover: velero
Uploader Type: kopia
Moved data Size (bytes): 27
Result: succeeded
Step 9: Restore the application
Simulate an accidental deletion:
kubectl delete -f demo-app.yaml
Restore from the backup:
velero create restore demo-restore1 --from-backup demo-b1
kubectl get restores -n velero
kubectl -n velero get datadownloads
NAME AGE
demo-restore1 2m
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE
demo-restore1-x5k2p Completed 112s 27 27 default 2m node-2
After a few minutes the application and its persistent data are restored:
kubectl get deployments,pods,pvc
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/demo-app 1/1 1 1 2m
NAME READY STATUS RESTARTS AGE
pod/demo-app-647dc64f7-ztwfq 1/1 Running 0 2m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/demo-pvc Bound pvc-96e97482-3e47-4d35-ab93-9ecb15c3a00d 1Gi RWO default 2m
Step 10: Schedule backups
Velero uses cron expressions to schedule backups. To run a backup every hour:
velero schedule create demo-hourly \
--schedule="0 * * * *" \
--selector app=demo-app \
--snapshot-volumes --snapshot-move-data
velero schedule get
NAME STATUS CREATED SCHEDULE BACKUP TTL LAST BACKUP SELECTOR PAUSED
demo-hourly Enabled 2026-09-15 11:51:34 +0700 +07 0 * * * * 0s n/a app=demo-app false
Step 11 (Optional): File system backup
If a volume cannot be captured with a CSI snapshot, Velero can copy its data from the file system instead:
velero backup create demo-b2 \
--selector app=demo-app \
--default-volumes-to-fs-backup
kubectl -n velero get podvolumebackups
velero backup describe demo-b2 --details
NAME STATUS STARTED BYTES DONE TOTAL BYTES STORAGE LOCATION AGE NODE UPLOADER
demo-b2-cj5j4 Completed 3s 27 27 default 4s kopia
Backup Volumes:
Pod Volume Backups - kopia:
Completed:
default/demo-app-7c9f6d8b5-x2lmn: data (size: 27, incremental size: 27)
Cleanup
Delete the schedule and backups (optional):
velero schedule delete demo-hourly --confirm
velero backup delete demo-b1 --confirm
Remove the sample application and Velero:
kubectl delete -f demo-app.yaml
helm uninstall velero --namespace velero
Uninstalling Velero does not delete the backups stored in your S3 bucket. Delete the velero/ prefix in the bucket manually if you no longer need them.