# pv-migrate: moving Kubernetes PVC data with rsync and rclone

> pv-migrate is a Go CLI and kubectl plugin that copies PersistentVolumeClaim data between namespaces, clusters and object storage. It is a side project, and the README says so plainly.

**utkuozdemir/pv-migrate** — CLI tool to easily migrate or backup/restore Kubernetes persistent volumes

- Repository: https://github.com/utkuozdemir/pv-migrate
- Stars: 2,422 · Forks: 110
- Language: Go
- License: Apache-2.0
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/utkuozdemir-pv-migrate

## Why renaming a Deployment is easy and moving a PVC is not

A Deployment rename is a manifest edit. You write the same object under a new name or namespace, apply it, and Kubernetes reconciles the rest. A PersistentVolumeClaim does not work that way. The object is metadata; the bytes sit in whatever storage backend the StorageClass provisioned, and the README states there is no built-in way to move them. That gap is the problem pv-migrate addresses.

The audience is narrow and practical. The README lists the cases it was built for: a 50Gi database PVC that needs more space on a StorageClass without volume expansion, a claim that has to move from namespace ns-a to ns-b, a workload moving between cloud providers, and a volume that needs a different StorageClass, for example from a ReadWriteOnce class like local-path to a ReadWriteMany one like NFS. In each case the storage class is not editable and the data has to be copied into a newly created claim.

The tool is a CLI and a kubectl plugin. It runs a data mover (rsync or rclone) inside the cluster, so nothing passes through your laptop unless you explicitly pick the local strategy. That design choice is what separates it from kubectl cp style workflows: the transfer happens pod to pod, inside the cluster network.

## How the data actually moves: strategies, fallbacks and a fresh SSH key per run

pv-migrate does not implement its own file transfer. It schedules pods that run rsync or rclone, and its job is to set up the connectivity between them, then report what happened. The README lists the strategies and the order they are tried: mount both PVCs in a single pod (mount), a ClusterIP service (clusterip), a LoadBalancer service (loadbalancer), a NodePort service (nodeport, opt-in) and port-forward through the local machine (local, opt-in). The quick start says the default run tries the cheapest strategy first.

For PVC-to-PVC copies the transport is rsync over SSH, and the README states a freshly generated Ed25519 or RSA key pair is used for every run. That is a meaningful detail: there is no long-lived key to rotate, and no shared secret stored between migrations. The trade-off is that each run pays the cost of generating keys and starting a server pod.

Bucket backup works differently. It uses rclone, with S3-compatible storage, Azure Blob, GCS or any custom rclone remote as the backend. The README points to docs/backup-restore.md for the object layout, raw rclone config mode and permission caveats. Failure output is also part of the mechanism: the README says a failed migration prints the exit code the data mover returned with the meaning that mover's own documentation attaches to it, the last lines of the failed pod's log, and what the cluster reported about the resources involved. That is a deliberate choice to avoid swallowing the underlying error, and it means the tool's error messages are only as good as the mover's exit codes.

## Installing pv-migrate and running the first migration

The README directs installation to docs/install.md and does not repeat the steps inline, so the exact package manager commands are not documented in the main file. What is confirmed is that the project ships a kubectl plugin manifest (.krew.yaml in the repository root) and publishes a CLI container image at utkuozdemir/pv-migrate on Docker Hub, alongside images for the rsync, sshd and rclone sides. The Dockerfile builds a scratch image containing only the binary and CA certificates, with ENTRYPOINT ["/pv-migrate"].

The quick start in the README is a single command. It copies the contents of old-pvc into new-pvc in the current namespace:

```bash
pv-migrate --source old-pvc --dest new-pvc
```

Run it and expect a progress bar in the terminal, as shown in the demo GIF. The default run tries the cheapest strategy first, so on a cluster where both claims can be mounted in one pod you should see the mount strategy chosen rather than a service-based one.

Bucket backup and restore use subcommands instead of flags. The README gives this example for S3:

```bash
pv-migrate backup \
  --source app-data \
  --backend s3 \
  --bucket pv-backups \
  --name app-data-2026-04-11

pv-migrate restore \
  --dest app-data-restore \
  --backend s3 \
  --bucket pv-backups \
  --name app-data-2026-04-11
```

The --name value identifies the backup object, so the restore must use the same name to find it. The README also describes running pv-migrate backup from a CronJob and handling retention with bucket lifecycle rules, which keeps scheduling out of the tool itself.

## Where pv-migrate stops being the right tool

The README carries an explicit warning: this is a side project the maintainer works on in spare time, and issues or PRs may take a long time to be looked at, or not be reached at all. That is not a footnote. For a tool that touches production data on a migration path, support expectations matter as much as the code, and the project sets those expectations low by its own description.

The second limitation is structural. pv-migrate moves data by running pods, so it needs a cluster that will schedule them and, depending on the strategy, expose connectivity. Strategies are tried in order with fallback, but nodeport and local are opt-in, which means the fallback chain is shorter than the list suggests unless you enable them. The local strategy routes traffic through your machine, which the README frames as the exception rather than the default.

There is also the question of what happens to the destination's existing contents. The tool copies one claim into another; the README does not document a rollback, and it does not describe a dry-run mode. If you point it at a destination that already holds data, the documentation gives no guarantee about what the copy does to files that are already there. Treat the destination as something you provision fresh and verify before relying on it.

Finally, the bucket workflow is not a snapshot. It reads the PVC through rclone while the claim is live, so consistency depends on the application writing to it, not on the tool. The README does not claim otherwise.

## pv-migrate against the alternatives you already have

The obvious alternative is kubectl cp or a plain rsync invocation from your own machine. The difference is the path the bytes take. kubectl cp streams through the API server and your workstation, which is slow for large volumes and puts the transfer at the mercy of your local network. pv-migrate runs the mover inside the cluster, so the data path is pod to pod unless you choose local.

The second alternative is the storage vendor's own migration tooling, or a CSI volume clone. A clone is a storage-layer operation and is typically fast, but it is bound to a single storage backend and its supported operations; it does not help when the destination is a different StorageClass or a different cluster on another provider. pv-migrate is backend-agnostic precisely because it copies files rather than blocks, and the README's cross-provider use case depends on that. The cost is that it is a file-level copy: it sees a mounted filesystem, not a volume, so anything that only exists below the filesystem layer is out of scope.

The third option is Velero, which this article does not cover, and which solves a different problem: cluster-wide backup and restore of Kubernetes objects and volumes. pv-migrate does one claim at a time and does not manage the surrounding manifests. If your goal is a full cluster backup, this is not that tool.

## Licence, upgrade cost and what the repository tells you about maintenance

pv-migrate is Apache-2.0, and the LICENSE file sits at the repository root. For most internal use that is a permissive licence with a patent grant; the usual obligations around notices and attribution apply. This is a description of the licence identifier, not legal advice, and anyone redistributing the binary or the images should read the file itself.

Upgrade cost is low in the ordinary case. The CLI is a single static binary, and the container image is built from scratch with only the binary and a CA bundle, so there is no base image to patch. The interesting upgrade surface is the data mover images (rsync, sshd, rclone), which are separate Docker Hub repositories. Those are the components that actually touch your data, and they are versioned separately from the CLI.

On maintenance: the repository is not archived, and the most recent push recorded is 2026-09-24. Releases v3.6.0 and v3.6.1 both landed on 2026-08-02, and v3.6.2 on 2026-09-02. The README's own warning about spare-time maintenance is the honest signal here, and it should be read alongside the release cadence rather than instead of it. The go.mod pins Go 1.27.1 and Kubernetes client libraries at v0.37.0, which means keeping up with upstream Kubernetes releases is a recurring task for the maintainer, not a one-off.

## Conclusion

Adopt pv-migrate when you need to move PVC contents between namespaces, clusters or storage classes and you can accept a data mover pod running inside the cluster. Do not adopt it if you need a supported, contract-backed tool: the README calls it a side project the maintainer works on in spare time, and says issues or PRs may take a long time or never be answered. Before the first real run, verify which strategies your cluster allows by reading docs/migrate.md, and check whether the destination PVC already holds data, since the default run copies contents into it.

## FAQ

### Does deleting a PVC delete the PV?

The README does not address what happens to a PersistentVolume when its claim is deleted. It focuses on copying the data of a claim into another claim, or backing it up to and restoring it from bucket storage, so this question is outside what the project documents.

### What is the difference between a PV and a PVC?

The README explains that for a PVC the Kubernetes object is only the metadata, while the data lives in the storage backend, and that there is no built-in way to move it. That distinction is the reason pv-migrate exists.

### What is PV and PVC in OpenShift?

The README does not discuss OpenShift specifically. It describes PersistentVolumeClaims in Kubernetes terms and lists migration cases such as namespace moves, storage class changes and cross-provider moves, which apply to any Kubernetes distribution that schedules the pods it creates.

### What is PVC and PV in Kubernetes?

The README treats the PVC as the Kubernetes object holding the metadata while the data itself lives in the storage backend, which is why copying a claim is not a manifest change. pv-migrate moves that data with rsync or rclone.

## Sources

- [Issues](https://github.com/utkuozdemir/pv-migrate/issues)
- [License: Apache-2.0](https://github.com/utkuozdemir/pv-migrate/blob/main/LICENSE)
- [README](https://github.com/utkuozdemir/pv-migrate/blob/main/README.md)
- [Releases](https://github.com/utkuozdemir/pv-migrate/releases)
- [utkuozdemir/pv-migrate on GitHub](https://github.com/utkuozdemir/pv-migrate)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/utkuozdemir-pv-migrate
