How to Back Up and Restore Kubernetes with Velero 1.18 (2026)
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.
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
How to Run Postgres on Kubernetes with CloudNativePG 1.30 (2026)
Step-by-step CloudNativePG 1.30.1 guide: install the operator, create a three-instance PostgreSQL 18 cluster, back up to S3 with the Barman Cloud plugin, restore to a point in time, add PgBouncer, monitor with Prometheus, upgrade majors and fix common errors.
15 minOct 31, 2026How to Upgrade an EKS Cluster to 1.37 Safely: Step by Step (2026)
Upgrade Amazon EKS one minor version at a time without an outage: refresh upgrade insights, scan for removed APIs with Pluto, upgrade the control plane with eksctl or the AWS CLI, then VPC CNI, CoreDNS and kube-proxy, then node groups and Karpenter, with the 7-day rollback.
15 minOct 31, 2026How to Set Up Karpenter on EKS: NodePools, Spot and Consolidation (2026)
Step-by-step Karpenter 1.15.1 setup on an existing EKS cluster: CloudFormation IAM and SQS, EKS Pod Identity, Helm install, EC2NodeClass and NodePool YAML, Spot with on-demand fallback, Balanced consolidation and disruption budgets, Cluster Autoscaler migration, and fixes for nodes not launching.
14 min