Migrating from VolSync
Translate VolSync ReplicationSource and ReplicationDestination objects into kopiur SnapshotPolicy, SnapshotSchedule and Restore manifests. They are printed as apply-ready YAML, and --apply creates them with server-side apply. All global flags apply.
Two VolSync movers are supported, and their data semantics are very different. The command detects which one each object uses, by looking for spec.restic or spec.kopia, and a namespace holding both translates in one run.
| Mover | Where it comes from | What migration means |
|---|---|---|
| restic | upstream VolSync | Config translation ONLY: the repository formats are incompatible, so the kopiur repository starts empty. |
| kopia | the perfectra1n/volsync fork | The repository is a kopia repository: kopiur adopts it in place, so all existing snapshots are preserved and history continues. |
$ kubectl kopiur migrate volsync -n media --resolve-secrets --apply| Flag | Effect |
|---|---|
--name NAME |
Translate one ReplicationSource (default: every one in the namespace). |
--repository NAME [--repository-kind …] |
Point the translated policies at an EXISTING kopiur repository. |
--resolve-secrets |
Instead, parse each repository Secret and EMIT a kopiur Repository derived from it. restic: + credential Secrets, with a REPLACE_ME kopia password you must set (a kopia repo needs its own new password); --apply refuses while any placeholder remains. kopia (fork): the existing repository is adopted: the Secret is referenced in place, no placeholder, so --apply works in one shot. |
--include-destinations |
Also translate ReplicationDestinations into Restores. restic (and kopia without an identity): deploy-or-restore fromPolicy + onMissingSnapshot: Continue. kopia with sourceIdentity or username/hostname: a raw-identity restore (source.identity), no policy pairing needed. |
--strict |
Exit 1 (emitting nothing) when any field has no kopiur equivalent. A minimal fork-kopia source is fully mappable and passes. |
--apply |
Server-side-apply the translated objects. |
-f, --filename PATH |
Read VolSync objects from a YAML file, a directory, or - (stdin) instead of the cluster. No kubeconfig required. Repeatable. See Offline / GitOps mode. |
--secrets PATH |
In offline mode, resolve repository Secrets from plaintext Secret YAML on disk (file/dir/-). Repeatable; needs --resolve-secrets. |
--from-cluster-secrets |
In offline mode, fetch the referenced Secrets from the live cluster instead of --secrets. |
--out-dir DIR |
Write one YAML file per ReplicationSource (plus _shared.yaml for derived Repositories/Secrets) into DIR instead of stdout. |
--force |
Allow --out-dir to overwrite existing files. |
Every VolSync field the translator reads is accounted for on stderr. It is either mapped, with the kopiur destination named; UNMAPPABLE, with the reason and what to do instead, for example that restic’s retain.within has no kopia equivalent; or ignored, with the reason it is not needed, for example that pruneIntervalDays is unnecessary because kopiur manages maintenance by default. Nothing is dropped silently.
Offline / GitOps mode
Section titled “Offline / GitOps mode”If your VolSync objects live as YAML in a Git repository rather than only in the cluster, point migrate volsync at the files with -f/--filename. That takes a file, a directory containing *.yaml and *.yml files, or - for stdin.
With -f, the command reads nothing from the cluster unless you ask it to, so it needs no kubeconfig:
$ kubectl kopiur migrate volsync -f ./apps/media/volsync.yaml --repository nas --out-dir ./apps/media/kopiurwrote 2 file(s) to ./apps/media/kopiur: _shared.yaml media-app.yaml--out-dir writes one file per ReplicationSource, named after it, plus a _shared.yaml holding any derived Repository and credential Secrets shared across sources. Those files are ready to commit. Without --out-dir the manifests stream to stdout as before.
You can also pipe the cluster’s objects through it, and a kind: List is unwrapped automatically:
$ kubectl get replicationsource -A -o yaml | kubectl kopiur migrate volsync -f - --repository nasCredentials offline have three options, because your SOPS-encrypted Secrets cannot be read straight from Git:
| You want | Use |
|---|---|
Point policies at a kopiur Repository you author/migrate separately, with no Secret reads |
--repository NAME |
Derive the Repository from plaintext Secret YAML on disk |
--resolve-secrets --secrets ./secrets.yaml |
| Read VolSync from files but fetch Secrets from the live cluster (e.g. Flux already decrypted them) | --resolve-secrets --from-cluster-secrets |
--apply is rejected with offline input. Review the emitted files and kubectl apply -f them yourself.
Each object’s namespace comes from its own metadata.namespace. -n only supplies a fallback for objects with no namespace, and it never filters offline input.
Try it offline
Section titled “Try it offline”deploy/examples/tryit/volsync-offline.yaml is a GitOps-shaped bundle: two fork-kopia ReplicationSources in the apps namespace, plus the repository Secrets they reference. Nothing here touches a cluster, so you can run the whole arc on your laptop with no kubeconfig.
# Two fork-kopia ReplicationSources — one file per app comes out, named after# each source (blog.yaml, forum.yaml). `repository` names the Secret below.apiVersion: volsync.backube/v1alpha1kind: ReplicationSourcemetadata: name: blog namespace: appsspec: sourcePVC: blog-data trigger: schedule: "0 2 * * *" kopia: repository: blog-kopia-repo # -> Secret blog-kopia-repo (below) copyMethod: Direct retain: daily: 7 weekly: 4---apiVersion: volsync.backube/v1alpha1kind: ReplicationSourcemetadata: name: forum namespace: appsspec: sourcePVC: forum-data trigger: schedule: "30 3 * * *" kopia: repository: forum-kopia-repo # -> Secret forum-kopia-repo (below) copyMethod: Direct retain: daily: 14 monthly: 61. Point the policies at an existing kopiur Repository. This is the simplest path, with no Secret reads at all:
$ kubectl kopiur migrate volsync -f deploy/examples/tryit/volsync-offline.yaml --repository nas --out-dir ./migratedwrote 2 file(s) to ./migrated: blog.yaml forum.yamlYou get one file per source, migrated/blog.yaml and migrated/forum.yaml. Each holds a SnapshotPolicy and a SnapshotSchedule pointing at the nas repository, ready to commit.
2. Or derive the Repository from the Secrets with --resolve-secrets. The Secrets live in the same bundle: -f reads only the ReplicationSources and --secrets reads only the Secrets, so one file feeds both flags. In a real repository, decrypt your SOPS Secrets to a scratch file first.
# The repository Secrets, as plaintext for offline `--resolve-secrets --secrets`.# In a real GitOps repo these are SOPS-encrypted; decrypt them to a scratch file# for the migrate run, or skip them entirely with `--repository`/# `--from-cluster-secrets`. KOPIA_REPOSITORY picks the backend to ADOPT;# KOPIA_PASSWORD + AWS_* are referenced in place by the emitted Repository.apiVersion: v1kind: Secretmetadata: name: blog-kopia-repo namespace: appstype: OpaquestringData: KOPIA_REPOSITORY: "s3://my-kopia-bucket/blog/" KOPIA_PASSWORD: "REPLACE_ME" # the EXISTING repo password — reuse it AWS_ACCESS_KEY_ID: "REPLACE_ME" AWS_SECRET_ACCESS_KEY: "REPLACE_ME" AWS_S3_ENDPOINT: "s3.us-east-1.amazonaws.com"---apiVersion: v1kind: Secretmetadata: name: forum-kopia-repo namespace: appstype: OpaquestringData: KOPIA_REPOSITORY: "s3://my-kopia-bucket/forum/" KOPIA_PASSWORD: "REPLACE_ME" # the EXISTING repo password — reuse it AWS_ACCESS_KEY_ID: "REPLACE_ME" AWS_SECRET_ACCESS_KEY: "REPLACE_ME" AWS_S3_ENDPOINT: "s3.us-east-1.amazonaws.com"$ kubectl kopiur migrate volsync -f deploy/examples/tryit/volsync-offline.yaml \ --resolve-secrets --secrets deploy/examples/tryit/volsync-offline.yaml --out-dir ./migrated --forcewrote 3 file(s) to ./migrated: _shared.yaml blog.yaml forum.yamlNow migrated/_shared.yaml carries the two derived Repository objects, each adopting the fork repository in place, with no create block and the Secret referenced where it sits. The per-source files reference them. Fork-kopia has no password placeholder, so the result is ready to apply.
3. Review and apply. --apply is refused offline by design, so commit the files, or apply them yourself:
$ kubectl apply -f ./migratedrestic sources (upstream VolSync)
Section titled “restic sources (upstream VolSync)”$ kubectl kopiur migrate volsync -n media --repository nas --applyWith --resolve-secrets, the translator parses the restic Secret’s RESTIC_REPOSITORY URL into a kopiur Repository backend, one of s3, b2, azure, gcs or filesystem. It carries the backend credentials into a new <secret>-kopiur-creds Secret under kopia’s environment names, because restic’s differ for B2 and Azure.
The kopia password is emitted as a REPLACE_ME placeholder you must replace. A restic password cannot initialize a kopia repository’s encryption.
kopia sources (perfectra1n/volsync fork)
Section titled “kopia sources (perfectra1n/volsync fork)”$ kubectl kopiur migrate volsync -n media --resolve-secrets --applyWhat the translation does, and why it is safe to switch over:
- Identity continuity, which is the part that matters most. The fork records snapshots as
<sanitized-name>@<sanitized-namespace>:/data, or under your explicitusername,hostnameandsourcePathOverride. Every translatedSnapshotPolicypinsspec.identityandsources[0].sourcePathOverrideto exactly that identity, so the next kopiur snapshot continues the same history and retention sees old and new snapshots as one series. The pinned identity is shown in the accounting, on the(fork snapshot identity)line. Check it matcheskopia snapshot listbefore you apply. - Secrets are referenced in place, never copied. The
Repository’sencryption.passwordSecretRefpoints at the existing VolSync Secret’sKOPIA_PASSWORD, and for S3, whoseAWS_*environment names already match, the backendauth.secretRefdoes too. Only key names kopia does not read get a small derived<secret>-kopiur-credsrename Secret: B2 (B2_ACCOUNT_IDandB2_APPLICATION_KEYbecomeB2_KEY_IDandB2_KEY), legacy Azure (AZURE_ACCOUNT_KEYbecomesAZURE_STORAGE_KEY), WebDAV (WEBDAV_USERNAMEandWEBDAV_PASSWORDbecomeKOPIA_WEBDAV_*), and SFTP known-hosts data. KOPIA_REPOSITORYURL forms all translate:s3://,gcs://,azure://,b2://,filesystem://(where the repository PVC is inferred frommoverVolumes),sftp://,webdav://andrclone://. So do the fork’s Secret-key overrides, such asKOPIA_S3_BUCKETbeating the URL bucket,AWS_S3_ENDPOINT, and*_DISABLE_TLS. Quirks are preserved rather than corrected: onlys3://bucket/prefixcarries a prefix, with the fork’s trailing slash. Forgcs,azureandb2the fork always ignored the URL’s path portion, so the repository is adopted at the bucket or container root and the dropped path is called out in the accounting.gdrive://has no kopiur backend, so write thatRepositoryby hand.- Retention maps one-to-one:
retain.latestbecomeskeepLatest,retain.yearlybecomeskeepAnnual, and the rest map by name. So docompression,parallelism(which becomesupload.maxParallelFileReads),additionalArgs(which becomesextraArgs), and the cache size limits (which becomemover.cache.*).
A few fork fields have no kopiur equivalent. They are called out in the accounting rather than dropped silently. actions.beforeSnapshot and actions.afterSnapshot run in the fork’s mover pod, while kopiur hooks run in the workload pod, so rewrite them for that context. policyConfig raw policy files are replaced by the typed SnapshotPolicy fields. shallow restore windows have no analog; use asOf, offset or snapshotID instead.
Suggested cut-over
Section titled “Suggested cut-over”- Suspend or delete the fork’s
ReplicationSource, so the two operators do not snapshot at the same time, and keep its Secret. - Run
kubectl kopiur migrate volsync -n <ns> --resolve-secretsand review the accounting, especially the pinned identity line. - Re-run with
--apply. Wait for theRepositoryto reachReadyand for the old snapshots to appear:kubectl kopiur snapshots list -n <ns>. - Prove continuity with
kubectl kopiur snapshot now --policy <name> --wait, then confirm the new snapshot lists under the same identity. - Delete the fork’s
KopiaMaintenanceobjects and, when you are satisfied, the VolSync install. Do not delete the repository Secret.
Try it end-to-end
Section titled “Try it end-to-end”Translate a fork-kopia VolSync source into kopiur manifests, review the accounting, apply it, and confirm the repository is adopted.
1. Dry-run the translation. Nothing is changed, and the accounting prints on stderr:
$ kubectl kopiur migrate volsync -n media --resolve-secretskopia sources: existing repository reused; all snapshots and history are kept. Keep the referenced VolSync Secret(s); retire the fork's KopiaMaintenance objects.
mapped spec.sourcePVC -> SnapshotPolicy.spec.sources[0].pvc.name mapped spec.kopia.retain -> SnapshotPolicy.spec.retention mapped spec.trigger.schedule -> SnapshotSchedule.spec.schedule.cron ...# (the kopiur Repository + SnapshotPolicy + SnapshotSchedule YAML on stdout)Review the accounting before applying, especially the pinned (fork snapshot identity) line. Every field the translator reads is mapped, UNMAPPABLE or ignored, and nothing is dropped silently.
2. Apply when the accounting looks right. Fork-kopia adopts the repository in place, so --apply works in one shot with no REPLACE_ME to fill:
$ kubectl kopiur migrate volsync -n media --resolve-secrets --apply...applied Repository/mediaapplied SnapshotPolicy/mediaapplied SnapshotSchedule/media3. Confirm the repository is healthy (deep) with doctor:
$ kubectl kopiur doctor -n media | grep repositories ok repositories ready4. See the adopted history. The fork’s existing snapshots show up as origin: discovered:
$ kubectl kopiur snapshots list --origin discovered -n mediaNAME POLICY ORIGIN PHASE SNAPSHOT-ID SIZE FILES START AGEmedia-20260601-020000 - discovered Succeeded 9f8e7d6c5b4a 4.7 GiB 982 2026-06-01T02:00:00Z 10d