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 kubectl configured 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-config points Velero at your provider. s3ForcePathStyle="true" is required by most S3-compatible services. On AWS S3 itself, omit s3Url and s3ForcePathStyle.
  • --use-volume-snapshots=false disables cloud disk snapshots, which need a provider-specific plugin and are not available on most self-managed clusters.
  • --use-node-agent deploys a DaemonSet that copies volume data from each node to object storage using Kopia.
  • --default-volumes-to-fs-backup includes 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.