Getting started
This is the end-to-end walkthrough: from a cluster with nothing installed to a verified backup and a verified restore. It holds your hand at every step and shows you exactly what to look for, so you know each one worked. Budget about 15 minutes.
If you only want the install reference, meaning every Helm value, the scopes, and the certificate options, see Installation. This page is the guided first run.
Want the long version, with the reasoning behind every value, a NAS track, and the kubectl kopiur plugin woven in? That is the Complete walkthrough.
What you need
Section titled “What you need”- A Kubernetes cluster, 1.24 or later, and
kubectlpointed at it. - Helm 3 or 4.
- A storage backend kopia can reach. This guide uses S3 or an S3-compatible store, such as AWS S3, MinIO, RustFS or Ceph RGW. Any of the eight backends works the same way; only the
Repositorychanges. - A PersistentVolumeClaim with some data in it to back up. The walkthrough assumes one named
app-datain a namespace calleddemo.
Step 1 — Install the operator
Section titled “Step 1 — Install the operator”Install the chart into its own namespace. By default the operator manages the webhook’s serving certificate itself, so no cert-manager is required. If you prefer cert-manager or a hand-supplied certificate, see Installation → Webhook TLS.
$ helm install kopiur oci://ghcr.io/home-operations/charts/kopiur \ --namespace kopiur-system --create-namespaceVerify the operator is up and the 9 CRDs are registered:
$ kubectl -n kopiur-system rollout status deploy/kopiur-controller$ kubectl -n kopiur-system rollout status deploy/kopiur-webhook$ kubectl get crd | grep kopiur.home-operations.comsnapshotpolicies.kopiur.home-operations.com ...snapshots.kopiur.home-operations.com ...snapshotschedules.kopiur.home-operations.com ...clusterrepositories.kopiur.home-operations.com ...maintenances.kopiur.home-operations.com ...repositories.kopiur.home-operations.com ...repositoryreplications.kopiur.home-operations.com ...snapshotreplications.kopiur.home-operations.com ...restores.kopiur.home-operations.com ...Nine CRDs and two ready Deployments means the operator is live.
Step 2 — Give it credentials
Section titled “Step 2 — Give it credentials”The mover Job that runs kopia reads two things from a Secret: your backend access keys and the repository encryption password.
That Secret must live in the same namespace as the data you back up, so demo here, because the mover loads it with envFrom, which only reads the local namespace. See Movers, RBAC & credentials for the full reasoning.
Fill in your backend keys and a generated KOPIA_PASSWORD in this section of the bundle:
apiVersion: v1kind: Secretmetadata: name: repo-creds # Must live in the SAME namespace as the data you back up — the mover Job # loads it with `envFrom`, which is namespace-local. namespace: demotype: OpaquestringData: # Backend auth keys are read by well-known names from the referenced Secret. AWS_ACCESS_KEY_ID: "REPLACE_ME" AWS_SECRET_ACCESS_KEY: "REPLACE_ME" # Repository encryption password. Generate it (e.g. `openssl rand -base64 24`) # and store it OUTSIDE the cluster too — lose it and the backups are # unrecoverable; kopia cannot decrypt without it. KOPIA_PASSWORD: "REPLACE_ME"This is the moment to apply the bundle. Once the REPLACE_ME values are filled in, kubectl apply -f deploy/examples/getting-started.yaml creates the namespace, PVC, Secret and every custom resource in one shot. The rest of the steps just watch each piece come up.
Step 3 — Create the Repository
Section titled “Step 3 — Create the Repository”Tell Kopiur where to store snapshots. create.enabled: true lets the operator initialize a brand-new kopia repository in the bucket. Drop it, or set false, to require that one already exists.
apiVersion: kopiur.home-operations.com/v1alpha1kind: Repositorymetadata: name: primary namespace: demospec: backend: s3: bucket: my-kopia-bucket # Optional: share one bucket across repos by giving each its own prefix. prefix: demo/ # Omit for AWS (derived from region); REQUIRED for MinIO / RustFS / Ceph # RGW and any other S3-compatible endpoint. endpoint: s3.us-east-1.amazonaws.com region: us-east-1 auth: secretRef: name: repo-creds # the AWS_* keys from the secret stage encryption: passwordSecretRef: name: repo-creds key: KOPIA_PASSWORD # which key in the Secret holds the password # Initialize a brand-new kopia repository if the prefix is empty. Drop this # block (or set false) to require that one already exists. create: enabled: trueOnce the bundle is applied, wait for Ready. This is the gate everything else waits on:
$ kubectl -n demo get repository primary -wNAME PHASE BACKEND AGEprimary Initializing S3 5sprimary Ready S3 12sIf it sticks in Pending or Failed, read the reason. Kopiur tells you exactly what is wrong:
$ kubectl -n demo describe repository primary # see Conditions + EventsThe common causes are wrong keys, an unreachable endpoint, or a bucket that does not exist while create.enabled: false. See Troubleshooting.
Step 4 — Write the recipe (SnapshotPolicy)
Section titled “Step 4 — Write the recipe (SnapshotPolicy)”Now describe what to back up and how long to keep it. Retention is GFS, meaning grandfather-father-son, and it is the only thing that prunes successful backups.
apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotPolicymetadata: name: app-data namespace: demospec: repository: name: primary # kind defaults to Repository (same namespace) sources: - pvc: name: app-data # the PVC to snapshot # GFS retention is the ONLY thing that prunes successful backups. retention: keepDaily: 7 # "restore any day from the last week" keepWeekly: 4 # "...any week from the last month"It was applied with the bundle. Confirm it is registered:
$ kubectl -n demo get snapshotpolicyNAME REPOSITORY AGEapp-data primary 3sA SnapshotPolicy runs nothing yet, because it is the recipe. Next we invoke it.
Step 5 — Take your first backup (and watch it work)
Section titled “Step 5 — Take your first backup (and watch it work)”Trigger one snapshot by creating a Snapshot that references the recipe:
apiVersion: kopiur.home-operations.com/v1alpha1kind: Snapshotmetadata: generateName: app-data-manual- # let the API server pick a unique name namespace: demospec: policyRef: name: app-data # the recipe to invoke once, right nowThis is the one resource you create rather than apply. It uses generateName, so every invocation gets a fresh, unique name such as app-data-manual-abc12, and kubectl apply cannot track a server-named object.
Run create against the bundle. The namespace, Secret and other resources already exist, so kubectl reports them unchanged, and the Snapshot is the one new object it mints:
$ kubectl create -f deploy/examples/getting-started.yamlsnapshot.kopiur.home-operations.com/app-data-manual-abc12 created
$ kubectl -n demo get snapshots -wNAME PHASE ORIGIN SNAPSHOT AGEapp-data-manual-abc12 Pending manual 2sapp-data-manual-abc12 Running manual 6sapp-data-manual-abc12 Succeeded manual k8f3c1a90 41sSucceeded with a SNAPSHOT ID means the data is in your repository. Inspect the details:
$ kubectl -n demo get snapshot app-data-manual-abc12 -o jsonpath='{.status.stats}'{"sizeBytes":...,"bytesNew":...,"filesNew":...}If it stays Pending with no Job, the mover is blocked on a precondition, usually credentials. The cause is on the Snapshot’s conditions and as an Event. See Movers → Troubleshooting.
Step 6 — Restore it (the half people forget to test)
Section titled “Step 6 — Restore it (the half people forget to test)”A backup you have never restored is a hope, not a backup. Restore the snapshot you just made into a new PVC, so you can compare it without touching the original.
This Restore section was applied with the bundle, but its source.snapshotRef.name is a placeholder until you point it at the Snapshot name from Step 5, here app-data-manual-abc12:
apiVersion: kopiur.home-operations.com/v1alpha1kind: Restoremetadata: name: app-data-verify namespace: demospec: source: # Object-store backends name an explicit snapshot: paste the Snapshot CR # name from `kubectl kopiur snapshots list` (or the manual Snapshot above). snapshotRef: name: REPLACE_ME target: # A fresh PVC, so the verification never touches the original volume. The # operator creates it; capacity is REQUIRED — it won't guess a size. pvc: name: app-data-restored capacity: 1Gi accessModes: - ReadWriteOnceSet that name, re-apply the bundle, which is idempotent so only the changed Restore updates, then watch it come up:
$ kubectl apply -f deploy/examples/getting-started.yaml$ kubectl -n demo get restore -wNAME PHASE AGEapp-data-verify Resolving 2sapp-data-verify Restoring 8sapp-data-verify Completed 37sCompleted means the data landed in app-data-restored. Mount that PVC in a pod and confirm your files are there. That is the real proof the round trip works.
Step 7 — Put it on a schedule
Section titled “Step 7 — Put it on a schedule”Manual backups prove the pipeline; a SnapshotSchedule makes it routine. It creates Snapshot objects on a cron, with deterministic jitter so replicas agree and load spreads.
apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotSchedulemetadata: name: app-data-nightly namespace: demospec: policyRef: name: app-data # the recipe to invoke each firing schedule: # `H` = a deterministic per-schedule minute (hashed from the schedule's UID), # so many schedules saying "nightly at 2" don't all fire at 02:00 sharp. cron: "H 2 * * *" # On top of H, spread each firing over a window. jitter: 30m # GitOps-friendly: applying this manifest does NOT take an immediate backup. runOnCreate: falseIt was applied with the bundle. Confirm it is registered, and check the firing it pinned:
$ kubectl -n demo get snapshotscheduleNAME CONFIG SCHEDULE SUSPENDED AGEapp-data-nightly app-data H 2 * * * false 3s
# the controller pins the next firing into status:$ kubectl -n demo get snapshotschedule app-data-nightly \ -o jsonpath='{.status.nextSchedule.at}'2026-06-07T02:17:00ZThat is a complete, recurring, restore-tested backup. 🎉
What just happened (and where to go next)
Section titled “What just happened (and where to go next)”You created a Repository (where), a SnapshotPolicy (what), invoked it with a Snapshot (one snapshot), proved it with a Restore, and automated it with a SnapshotSchedule (when).
Maintenance, the periodic kopia maintenance that reclaims space, was set up for you automatically the moment the repository existed. You do not have to do anything for it.
From here:
- Concepts is the why behind what you just did: dedup, the identity model, the three-resource split, and one-shared-repository guidance.
- Repositories & backends points Kopiur at Azure, GCS, B2, a NAS through filesystem, SFTP or WebDAV, or rclone, and shares one repository across namespaces with
ClusterRepository. - Backups & schedules covers multi-PVC selectors, hooks such as quiescing a database before snapshotting, retention tuning, and
deletionPolicy. - Restores covers point-in-time restore, deploy-or-restore for GitOps, and restoring snapshots Kopiur did not create.
- Maintenance covers what runs, when, and how shared repositories coordinate.
- Examples is the complete, apply-ready manifest ladder covering the patterns above.
- Troubleshooting is for when a step above does not go green.
Tearing down the walkthrough
Section titled “Tearing down the walkthrough”$ kubectl -n demo delete snapshotschedule app-data-nightly$ kubectl -n demo delete restore app-data-verify$ kubectl -n demo delete snapshot --all # finalizer also deletes the snapshots$ kubectl -n demo delete snapshotpolicy app-data$ kubectl -n demo delete repository primary