Velero is an open source tool that backs up Kubernetes resources and persistent volume data to object storage, and restores them into the same or a different cluster. In this tutorial you will install the Velero CLI on Ubuntu 24.04, deploy Velero to your cluster with an S3-compatible bucket as the backup target, back up a namespace including its volume data, delete it, restore it, and schedule daily backups. The file system backup method used here works on any cluster, including self-managed clusters on VPS or bare metal without cloud snapshot APIs.
Prerequisites
To follow this guide you need:
- A Kubernetes cluster (1.30 or newer) and
kubectlconfigured with cluster-admin rights on an Ubuntu 24.04 workstation. - A default StorageClass that can provision volumes, used by the test application.
- An S3-compatible bucket reserved for Velero (AWS S3, MinIO, or any provider with an S3 API), plus an access key and secret key with read and write permission on that bucket. You need the endpoint URL and region of the provider.
Step 1 - Installing the Velero CLI
The velero CLI installs the server components and drives backups and restores. Pick the version from the Velero releases page and store it in a variable. This guide uses v1.17.0:
VELERO_VERSION=v1.17.0
Download the archive, extract it and install the binary:
curl -fsSLO "https://github.com/vmware-tanzu/velero/releases/download/${VELERO_VERSION}/velero-${VELERO_VERSION}-linux-amd64.tar.gz"
tar -xzf "velero-${VELERO_VERSION}-linux-amd64.tar.gz"
sudo install -m 0755 "velero-${VELERO_VERSION}-linux-amd64/velero" /usr/local/bin/velero
On an arm64 workstation, replace linux-amd64 with linux-arm64. Verify the client:
velero version --client-only
Client:
Version: v1.17.0
Git commit: ...
Step 2 - Preparing the storage credentials
Velero talks to S3-compatible storage through the AWS plugin, which reads credentials in the AWS shared credentials format. Create the file in your home directory:
nano ~/credentials-velero
Add your keys, replacing the placeholders:
[default]
aws_access_key_id=your_access_key
aws_secret_access_key=your_secret_key
Restrict its permissions, since it contains a secret:
chmod 600 ~/credentials-velero
velero install copies these values into the cloud-credentials secret in the velero namespace, so you can delete the local file after the installation.
Step 3 - Installing Velero in the cluster
Each Velero release is tested against a specific version of velero-plugin-for-aws. Check the compatibility table in the plugin's README; for Velero v1.17 it is v1.13.x:
AWS_PLUGIN_VERSION=v1.13.0
Install Velero, replacing your_bucket, your_region and https://your_s3_endpoint:
velero install \
--provider aws \
--plugins "velero/velero-plugin-for-aws:${AWS_PLUGIN_VERSION}" \
--bucket your_bucket \
--secret-file ~/credentials-velero \
--backup-location-config region=your_region,s3ForcePathStyle="true",s3Url=https://your_s3_endpoint \
--use-volume-snapshots=false \
--use-node-agent \
--default-volumes-to-fs-backup \
--wait
What the important flags do:
--backup-location-configpoints Velero at your provider.s3ForcePathStyle="true"is required by most S3-compatible services. On AWS S3 itself, omits3Urlands3ForcePathStyle.--use-volume-snapshots=falsedisables cloud disk snapshots, which need a provider-specific plugin and are not available on most self-managed clusters.--use-node-agentdeploys a DaemonSet that copies volume data from each node to object storage using Kopia.--default-volumes-to-fs-backupincludes the data of every pod volume in backups by default, so you do not have to annotate each pod.
Check that the server and the node agent are running:
kubectl -n velero get pods
NAME READY STATUS RESTARTS AGE
node-agent-6t8xq 1/1 Running 0 1m
node-agent-rm2bz 1/1 Running 0 1m
velero-7d9d6b5c8f-2kqzv 1/1 Running 0 1m
Then confirm that Velero can reach the bucket:
velero backup-location get
NAME PROVIDER BUCKET/PREFIX PHASE LAST VALIDATED ACCESS MODE DEFAULT
default aws your_bucket Available 2026-09-25 10:12:03 +0000 UTC ReadWrite true
PHASE must be Available. Once it is, remove the local credentials file:
rm ~/credentials-velero
Step 4 - Creating a test application with data
To prove that both the Kubernetes objects and the volume contents are restored, create a namespace with a PVC and a pod that mounts it:
nano demo-app.yaml
apiVersion: v1
kind: Namespace
metadata:
name: demo
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
namespace: demo
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: demo
spec:
replicas: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do sleep 3600; done"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data
Apply it, wait for the pod, and write a file to the volume:
kubectl apply -f demo-app.yaml
kubectl -n demo rollout status deployment/app
kubectl -n demo exec deploy/app -- sh -c 'date > /data/backup-test.txt && cat /data/backup-test.txt'
Thu Sep 25 10:15:42 UTC 2026
Step 5 - Backing up the namespace
Create a backup limited to the demo namespace. --wait blocks until it finishes:
velero backup create demo-backup --include-namespaces demo --wait
Backup request "demo-backup" submitted successfully.
Waiting for backup to complete. You may safely press ctrl-c to stop waiting - your backup will continue in the background.
...
Backup completed with status: Completed.
Inspect the result, including the volume backup:
velero backup describe demo-backup --details
Look for Phase: Completed and, under the pod volume backups section, the demo/app-.../data volume marked as Completed. If the phase is PartiallyFailed, read the logs with velero backup logs demo-backup.
Step 6 - Restoring after a disaster
Simulate a disaster by deleting the whole namespace, including the PVC and its data:
kubectl delete namespace demo
kubectl get namespace demo
Error from server (NotFound): namespaces "demo" not found
Restore it from the backup:
velero restore create demo-restore --from-backup demo-backup --wait
Restore completed with status: Completed.
Velero recreates the namespace, the PVC and the Deployment, and the node agent copies the data back into the new volume before the application container starts. Verify that the file is back:
kubectl -n demo rollout status deployment/app
kubectl -n demo exec deploy/app -- cat /data/backup-test.txt
The output should show the same timestamp you wrote in Step 4.
To restore into a different namespace instead, for example to inspect a backup without touching production, map the names:
velero restore create demo-inspect --from-backup demo-backup --namespace-mappings demo:demo-copy --wait
Step 7 - Scheduling daily backups
A manual backup is only useful if someone remembers to run it. A schedule creates backups automatically using cron syntax, in UTC. The following one runs every day at 02:00 and keeps each backup for 30 days (720h), after which Velero deletes it from the bucket:
velero schedule create demo-daily --schedule="0 2 * * *" --include-namespaces demo --ttl 720h0m0s
List your schedules:
velero schedule get
NAME STATUS CREATED SCHEDULE BACKUP TTL LAST BACKUP SELECTOR PAUSED
demo-daily Enabled 2026-09-25 10:25:10 +0000 UTC 0 2 * * * 720h0m0s n/a <none> false
To test the schedule without waiting until 02:00, trigger a backup from it immediately:
velero backup create --from-schedule demo-daily --wait
Backups created by a schedule are named demo-daily-<timestamp> and appear in velero backup get. To protect several namespaces, pass a comma-separated list to --include-namespaces, or omit the flag to back up the whole cluster.
Migrating to another cluster
Because backups live in object storage, you can restore them into a different cluster. Install Velero on the new cluster with the same bucket, ideally with --backup-location-config unchanged, and run velero backup get: after the next sync (about a minute), the backups from the old cluster are listed. Then run velero restore create --from-backup <backup_name> as in Step 6. The new cluster needs StorageClasses with the same names as the old one, or the restored PVCs will stay Pending.
Troubleshooting
The backup location stays Unavailable. Check the server logs with kubectl -n velero logs deploy/velero. Common causes are a wrong endpoint, region or bucket name, keys without permission on the bucket, and a missing s3ForcePathStyle="true" for S3-compatible providers. Some providers also reject the checksum headers sent by recent AWS SDKs; adding checksumAlgorithm="" to --backup-location-config fixes that.
Volume data is missing after a restore. The backup was taken without file system backup. Check that the node-agent pods are running and that velero backup describe --details lists pod volume backups. Volumes of type hostPath are never included.
Restore reports resources that already exist. Velero does not overwrite existing objects by default; it skips them and adds a warning. Delete the existing objects first or restore into another namespace with --namespace-mappings.
Conclusion
You installed Velero with S3-compatible storage, backed up and restored a namespace including its volume data, and scheduled daily backups with a retention period. Next, schedule a backup that covers all your application namespaces, store the bucket with a different provider or region than the cluster, and run a restore test into a separate namespace regularly so you know your backups actually work.
