How to Back Up and Restore Kubernetes with Velero 1.18 (2026)

Oct 31, 2026 · 13 min · Ajay Kumar

This guide is for anyone who runs stateful workloads on Kubernetes and wants backups they have actually restored from. By the end you will have Velero installed with Helm, writing to S3 (or any S3-compatible store), backing up persistent volumes through CSI snapshots plus the data mover, running on a schedule, and you will have restored a namespace into a new name and into a second cluster. Everything here is pinned to Velero v1.18.4 (released 28 September 2026), velero-plugin-for-aws v1.14.4 and Helm chart velero 12.2.2; the Velero README lists 1.18 as tested on Kubernetes 1.33.7, 1.34.1 and 1.35.0. You need kubectl and helm access to a cluster with a CSI driver that supports snapshots, and an S3 bucket you control. If you want the theory first (RPO, RTO, immutable copies), read the companion post on backup and disaster recovery in 2026.

How Velero backs up a cluster

Velero does two separate jobs. It exports Kubernetes objects through the API server into a tarball in object storage (the BackupStorageLocation, or BSL). For volume data it has three options: native cloud snapshots through a provider plugin, CSI snapshots, or a file-level copy by the node agent. Since Velero 1.14 the CSI plugin is built in, so there is nothing extra to install.

The option I recommend for most clusters is CSI snapshot data movement: Velero takes a CSI snapshot, mounts it in a short-lived data mover pod, and uploads the contents with Kopia into the same bucket. A plain cloud snapshot stays in the disk's own region and account, which is useless if that account is what you lost. Moved data is just objects in a bucket, so it survives the cluster, can be locked and replicated, and restores into a different cluster or region.

Velero 1.18: backup with the data mover, restore into another cluster Source cluster velero backup create exports API objects --snapshot-move-data one CSI snapshot per PVC Data mover node-agent starts a pod per DataUpload Kopia uploader snapshot deleted after upload S3 bucket (BSL) backups/ objects kopia/ volume data one repo per ns lock and replicate this bucket Target cluster same bucket, prefix accessMode ReadOnly sync every 1m same StorageClass names must exist Restore objects created in priority order DataDownload fills new PVCs, then pods start Velero 1.18 defaults: data mover prepare timeout 30 min, operation timeout 4 h, TTL 30 days. Restic backups were removed in 1.17; old Restic backups stay restorable until 1.19. Never restore into a cluster running an older Kubernetes version than the source.
The path a backup takes with CSI snapshot data movement, and how a second cluster restores it. Timeouts, sync interval and TTL are the defaults stated in the Velero v1.18 documentation.

Step 1: Install the Velero CLI (v1.18.4)

Match the CLI to the server version. Download from the GitHub release rather than a package manager that might lag:

VELERO_VERSION=v1.18.4
OS=linux          # or darwin
ARCH=amd64        # or arm64
curl -fsSLO "https://github.com/vmware-tanzu/velero/releases/download/${VELERO_VERSION}/velero-${VELERO_VERSION}-${OS}-${ARCH}.tar.gz"
tar -xzf "velero-${VELERO_VERSION}-${OS}-${ARCH}.tar.gz"
sudo install "velero-${VELERO_VERSION}-${OS}-${ARCH}/velero" /usr/local/bin/velero

velero version --client-only
# Client:
#   Version: v1.18.4

The CLI talks to the cluster through your current kubeconfig context, so check kubectl config current-context before every restore. Restoring into the wrong cluster is the classic Velero mistake.

Step 2: Create the S3 bucket and IAM permissions

The AWS plugin README prefers a bucket unique to each cluster; I use one bucket with a prefix per cluster, which also works as long as the prefixes never overlap. Turn on versioning; you will want it for Object Lock later.

BUCKET=acme-velero-backups
REGION=eu-west-1
aws s3api create-bucket --bucket "$BUCKET" --region "$REGION" \
  --create-bucket-configuration LocationConstraint="$REGION"   # omit this flag in us-east-1
aws s3api put-bucket-versioning --bucket "$BUCKET" \
  --versioning-configuration Status=Enabled

The IAM policy below is the one in the plugin README. The ec2: actions are only used by the native EBS snapshotter; if you rely purely on CSI snapshots with the data mover, the S3 statements are what Velero itself needs (the EBS CSI driver takes the snapshots with its own role).

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ec2:DescribeVolumes", "ec2:DescribeSnapshots", "ec2:CreateTags",
                 "ec2:CreateVolume", "ec2:CreateSnapshot", "ec2:DeleteSnapshot"],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:DeleteObject", "s3:PutObject", "s3:PutObjectTagging",
                 "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"],
      "Resource": ["arn:aws:s3:::acme-velero-backups/*"]
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": ["arn:aws:s3:::acme-velero-backups"]
    }
  ]
}

On EKS, give that policy to a role bound to Velero's service account instead of creating access keys. The Helm chart names the service account velero-server when the release is called velero:

aws iam create-policy --policy-name velero --policy-document file://velero-policy.json

eksctl create iamserviceaccount \
  --cluster prod-eu1 --namespace velero --name velero-server \
  --role-name velero-prod-eu1 --role-only \
  --attach-policy-arn arn:aws:iam::111122223333:policy/velero \
  --approve

If you prefer the other workload identity routes, the GitHub Actions OIDC guide covers the same trust model from the CI side.

Step 3: Install Velero with Helm (chart 12.2.2)

The chart's default image is still v1.18.2, so pin the tag explicitly. The plugin goes in as an init container.

# values-velero.yaml (chart velero 12.2.2)
image:
  repository: velero/velero
  tag: v1.18.4

initContainers:
  - name: velero-plugin-for-aws
    image: velero/velero-plugin-for-aws:v1.14.4
    imagePullPolicy: IfNotPresent
    volumeMounts:
      - mountPath: /target
        name: plugins

configuration:
  backupStorageLocation:
    - name: default
      provider: aws
      bucket: acme-velero-backups
      prefix: prod-eu1            # one prefix per cluster
      default: true
      config:
        region: eu-west-1
  features: EnableCSI             # still required in 1.18
  defaultSnapshotMoveData: true   # move every CSI snapshot into the bucket
  uploaderType: kopia

snapshotsEnabled: false           # no native EBS VolumeSnapshotLocation; CSI only
deployNodeAgent: true             # runs data mover and fs-backup pods

credentials:
  useSecret: false                # IRSA instead of static keys

serviceAccount:
  server:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/velero-prod-eu1

podSecurityContext:
  fsGroup: 1337                   # chart's IRSA example: lets the pod read the projected token
helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm repo update
kubectl create namespace velero
# node-agent mounts hostPath, so Pod Security Admission must allow it
kubectl label namespace velero pod-security.kubernetes.io/enforce=privileged
helm install velero vmware-tanzu/velero --version 12.2.2 \
  --namespace velero -f values-velero.yaml

kubectl -n velero get pods
velero backup-location get
# NAME      PROVIDER   BUCKET/PREFIX                  PHASE       LAST VALIDATED   ACCESS MODE   DEFAULT
# default   aws        acme-velero-backups/prod-eu1   Available   ...              ReadWrite     true

Do not continue until the phase says Available. For MinIO, Ceph or another S3-compatible store, swap the location config and use a credentials secret. The README's known-issues table says Ceph and Backblaze B2 need checksumAlgorithm: "", and Google Cloud Storage should use the GCP plugin instead.

configuration:
  backupStorageLocation:
    - name: default
      provider: aws
      bucket: velero
      default: true
      config:
        region: minio
        s3ForcePathStyle: "true"
        s3Url: http://minio.minio.svc.cluster.local:9000
        checksumAlgorithm: ""     # only for Ceph RGW, B2 and similar
credentials:
  useSecret: true
  secretContents:
    cloud: |
      [default]
      aws_access_key_id=velero
      aws_secret_access_key=change-me

Step 4: Make CSI snapshots work

Velero needs the snapshot CRDs, a snapshot controller, and one VolumeSnapshotClass per CSI driver labelled for Velero. On EKS, install the snapshot-controller managed add-on; AWS's docs note that EKS Auto Mode does not include it and that volumes must use the ebs.csi.aws.com provisioner, not the old in-tree one.

# volumesnapshotclass-ebs.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: ebs-velero
  labels:
    velero.io/csi-volumesnapshot-class: "true"   # one per driver
driver: ebs.csi.aws.com
deletionPolicy: Retain   # Velero flips it to Delete when it removes the backup
aws eks create-addon --cluster-name prod-eu1 --addon-name snapshot-controller
kubectl apply -f volumesnapshotclass-ebs.yaml
kubectl get volumesnapshotclass -l velero.io/csi-volumesnapshot-class=true

Step 5: Take your first backup with the data mover

Back up one namespace, wait for it, and watch the DataUpload resources that move each volume.

velero backup create shop-manual-1 \
  --include-namespaces shop \
  --snapshot-move-data \
  --wait

kubectl -n velero get datauploads -l velero.io/backup-name=shop-manual-1 -o wide
velero backup describe shop-manual-1 --details

describe should end with Phase: Completed and list each PVC under the backup volumes section. Per the data movement docs, each DataUpload reports bytes done, total bytes and, new in 1.18, incremental bytes once it finishes. A DataUpload that cannot provision its temporary volume within 30 minutes is cancelled; the whole operation times out after 4 hours.

Step 6: Use file system backup for volumes without snapshots

NFS, EFS, local volumes and anything without CSI snapshot support need file system backup (FSB), where node-agent reads the volume from the node's /var/lib/kubelet/pods. It is opt-in by default: annotate the pod with the volume names.

kubectl -n shop annotate pod/uploads-0 backup.velero.io/backup-volumes=uploads

# Or opt out instead: back up every pod volume with FSB except excluded ones
velero backup create shop-fsb-1 --include-namespaces shop \
  --default-volumes-to-fs-backup --wait
kubectl -n velero get podvolumebackups -l velero.io/backup-name=shop-fsb-1

The FSB docs list the limits: hostPath volumes are not supported, and the pod must be running. FSB is a file copy of a live filesystem, so for databases use the database's own dump or an operator-native backup and let Velero carry the manifests. Since 1.17, Restic is gone for new backups; --uploader-type=restic is rejected at install.

Step 7: Schedule backups

Schedules are a Backup template plus a cron expression. Use CRON_TZ so daylight saving does not move your RPO, and set a TTL shorter than any Object Lock retention on the bucket.

velero schedule create apps-daily \
  --schedule="CRON_TZ=UTC 0 2 * * *" \
  --include-namespaces 'shop,payments' \
  --snapshot-move-data \
  --ttl 720h0m0s

velero schedule get
velero backup create --from-schedule apps-daily   # run one now

If you manage Velero with Argo CD, keep the Schedule as YAML in Git. Leave useOwnerReferencesInBackup false: the backup reference warns that owner references make the garbage collector and Velero's sync fight over backups when the schedule is deleted.

apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: apps-daily
  namespace: velero
spec:
  schedule: "CRON_TZ=UTC 0 2 * * *"
  useOwnerReferencesInBackup: false
  template:
    includedNamespaces: ["shop", "payments"]   # 1.18 also accepts globs like "team-*"
    snapshotMoveData: true
    storageLocation: default
    ttl: 720h0m0s

Step 8: Restore into a new namespace (the restore drill)

The cheapest restore test is a namespace mapping on the same cluster. It proves the backup is readable and the volumes come back without touching production.

velero restore create shop-drill-$(date +%Y%m%d) \
  --from-backup shop-manual-1 \
  --namespace-mappings shop:shop-drill \
  --wait

velero restore describe shop-drill-$(date +%Y%m%d) --details
kubectl -n velero get datadownloads
kubectl -n shop-drill get pods,pvc

Velero skips objects that already exist by default; --existing-resource-policy=update tries to update them instead, best effort. Watch for Ingresses, Gateways and webhooks in the drill namespace that compete with production for the same hostnames.

Step 9: Restore to a new cluster

Install Velero on the target cluster with the same bucket and prefix, but mark the location read-only so the new cluster cannot expire or delete anything. The migration docs recommend exactly this.

# values-velero-dr.yaml: only the differences from values-velero.yaml
configuration:
  backupStorageLocation:
    - name: default
      provider: aws
      bucket: acme-velero-backups
      prefix: prod-eu1
      default: true
      accessMode: ReadOnly
      config:
        region: eu-west-1
kubectl config use-context dr-eu2
helm install velero vmware-tanzu/velero --version 12.2.2 --namespace velero \
  -f values-velero.yaml -f values-velero-dr.yaml

velero backup get          # backups appear after the next sync, 1 minute by default
velero restore create --from-backup apps-daily-20261030020000 --wait

Before you restore, create StorageClasses with the same names as the source (a namespace-scoped backup does not include StorageClasses unless you pass --include-cluster-resources, and a source class is often wrong for the target anyway), and make sure the target runs the same or a newer Kubernetes version. If you changed the velero-repo-credentials secret on the source, copy it across or the Kopia repository will not open. When the drill is done, flip the target to read-write only if it is taking over:

kubectl -n velero patch backupstoragelocation default \
  --type merge -p '{"spec":{"accessMode":"ReadWrite"}}'

Fixing a PartiallyFailed backup or restore

PartiallyFailed means Velero finished but at least one item errored. The backup is usable for everything that did not fail, which is exactly why it is dangerous to ignore. Find the errors first:

velero backup describe shop-manual-1 --details | sed -n '/Errors/,/Warnings/p'
velero backup logs shop-manual-1 | grep -E 'level=(error|warning)'
velero restore describe shop-drill-20261031 --details
velero restore logs shop-drill-20261031 | grep -E 'level=error'

# One tarball with server logs, plugin logs and the Velero CRs, for an issue or a colleague
velero debug --backup shop-manual-1 --restore shop-drill-20261031

Then match the error to the table below. On restores, "already exists" warnings are usually harmless; admission webhook rejections and failed DataDownloads are not.

Troubleshooting

Symptom Likely cause Fix
velero backup-location get shows Unavailable Wrong region, bucket, prefix or credentials; IRSA annotation missing kubectl -n velero logs deploy/velero, check the role ARN annotation and podSecurityContext.fsGroup, confirm the bucket region
Backup PartiallyFailed, PVC listed with no snapshot No VolumeSnapshotClass labelled for that driver, or in-tree provisioner Create the labelled class (Step 4); migrate volumes to the CSI provisioner
DataUpload Canceled after about 30 minutes Temporary volume from the snapshot never provisioned (snapshot controller missing, quota, AZ) Check kubectl get volumesnapshot -A and events; raise dataMoverPrepareTimeout only after fixing the cause
node-agent DaemonSet has 0 pods, events say violates PodSecurity Pod Security Admission blocks hostPath in the velero namespace Label the namespace pod-security.kubernetes.io/enforce=privileged; on OpenShift add the privileged SCC
PodVolumeBackup failed for a volume hostPath volume, or pod not running at backup time Exclude it, or switch that volume to CSI snapshots
Backup stuck InProgress after Velero restarted Velero cannot resume interrupted backups kubectl -n velero delete backup NAME, then rerun
Restore warnings already exists, nothing changed Default policy skips existing objects Restore into a mapped namespace, or --existing-resource-policy=update
Restored PVCs stuck Pending on the new cluster StorageClass name from the source does not exist Create a StorageClass with the same name before restoring
XAmzContentSHA256Mismatch on Ceph or B2 Checksum header not supported by the store Set checksumAlgorithm: "" in the BSL config
Restore error from an admission webhook Policy engine or operator webhook rejects restored objects Restore the policy CRDs first, or exclude and reapply; see the troubleshooting docs

FAQ

Is Velero still maintained in 2026?

Yes. Velero ships minor releases roughly twice a year with patch releases between; 1.18.0 came out in March 2026 and 1.18.4 in September 2026. The AWS plugin follows in lockstep, with v1.14.x paired with Velero 1.18.x in the plugin's compatibility table.

Does Velero back up etcd?

No. Velero reads objects through the Kubernetes API, which is why it works on managed clusters where you cannot reach etcd. On self-managed clusters, keep etcd snapshots as well, because they capture the whole cluster state in one consistent file.

Should I use CSI snapshots or file system backup?

Use CSI snapshots with --snapshot-move-data wherever the storage driver supports snapshots, because the copy is crash-consistent and leaves the cloud account. Use file system backup for NFS, EFS and other volumes without snapshot support. For databases, prefer the database's own backup tooling and use Velero for the surrounding manifests.

Can Velero restore to a different cloud or region?

Moved data and file system backups can, because the volume contents live in object storage. Native cloud snapshots cannot: the Velero docs say the AWS and Azure plugins do not migrate snapshot data between regions. Plan for different StorageClass names and load balancer settings on the target.

When I set Velero up on a cluster, the first thing I check is not the backup at all: it is whether a restore into a mapped namespace works today, with the DataDownloads completing and the app answering a request. After that I look at whether the bucket lives in a different account from the cluster, whether its retention outlasts the Velero TTL, and whether someone gets paged when a scheduled backup ends in anything other than Completed. The Velero 1.18 documentation, the AWS plugin README and the release notes are the sources to check before you upgrade.

Related: How to Set Up Argo CD for GitOps: Step-by-Step Guide (2026), How to Use External Secrets Operator with Vault on Kubernetes (2026) and Backup and Disaster Recovery in 2026: RPO vs RTO, 3-2-1-1-0, Real Costs.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related