WebDAV¶
The WebDAV backend stores the kopia repository at a WebDAV collection URL with
HTTP basic auth — Nextcloud, Apache mod_dav, or any WebDAV server.
Reach for WebDAV when your target speaks WebDAV but not S3. For Nextcloud you can also reach it as an rclone remote, but native WebDAV is simpler.
Provider prerequisites¶
- A WebDAV collection URL for the repository (e.g.
https://dav.example.com/kopiaor a Nextcloud/remote.php/dav/files/<user>/...path). - Basic-auth credentials with write access to that collection.
Finding your collection URL¶
The URL must point at a folder (collection) that already exists and is writable; the path shape is server-specific:
| Server | Collection URL shape | Notes |
|---|---|---|
| Nextcloud | https://cloud.example.com/remote.php/dav/files/<username>/kopia |
<username> is the login name, and it appears in the path. |
Apache mod_dav |
https://dav.example.com/kopia |
Whatever <Location> the DAV block serves, plus your folder. |
| Caddy (webdav) | https://dav.example.com/kopia |
The route prefix configured for the webdav handler, plus the folder. |
Nextcloud: app password + the exact URL
- Create the target folder (
kopiahere) in the Files app. - Personal settings → Security → Devices & sessions → Create new app
password. Use it as
KOPIA_WEBDAV_PASSWORD— with two-factor enabled the account password will not work for WebDAV at all, and an app password is revocable on its own either way. - The URL is the user-specific DAV path, not the share link and not the
bare hostname:
https://cloud.example.com/remote.php/dav/files/alice/kopia(for loginalice). The deprecated/remote.php/webdav/…form still works but thedav/files/<user>form is canonical.
The Secret shape¶
Loaded with envFrom; the keys reach kopia as environment variables.
| Secret key | Required | What it is |
|---|---|---|
KOPIA_WEBDAV_USERNAME |
yes | Basic-auth username. |
KOPIA_WEBDAV_PASSWORD |
yes | Basic-auth password (an app password where the server supports it). |
KOPIA_PASSWORD |
yes | The repository encryption password. |
stringData:
KOPIA_WEBDAV_USERNAME: "REPLACE_ME"
KOPIA_WEBDAV_PASSWORD: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
Lose the password, lose the backups
KOPIA_PASSWORD encrypts the repository and cannot be recovered if lost. It is
separate from the WebDAV login password. Store it outside the cluster and back
up the Secret. See Encryption.
The Repository¶
---
apiVersion: v1
kind: Secret
metadata:
name: webdav-repo-creds
namespace: backups
type: Opaque
stringData:
# HTTP basic-auth for the WebDAV endpoint. kopia reads these exact env names.
KOPIA_WEBDAV_USERNAME: "REPLACE_ME"
KOPIA_WEBDAV_PASSWORD: "REPLACE_ME"
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: webdav-primary
namespace: backups
spec:
backend:
webDav:
url: https://dav.example.com/kopia # the WebDAV collection URL
auth:
secretRef:
name: webdav-repo-creds
encryption:
passwordSecretRef:
name: webdav-repo-creds
key: KOPIA_PASSWORD
create:
enabled: true
Fields reference (backend.webDav)¶
Note the spec key is webDav (camelCase, capital D).
| Field | Required | Default | Example | What it controls |
|---|---|---|---|---|
url |
yes | — | https://cloud.example.com/remote.php/dav/files/alice/kopia |
The WebDAV collection URL holding the repository — full scheme + path to an existing, writable folder. |
auth.secretRef |
no | — | { name: webdav-repo-creds } |
Names the basic-auth Secret above. Same namespace as the Repository; a ClusterRepository adds namespace:. |
WebDAV has no cloud-IAM federation, so its auth is Secret-only — there is
no workloadIdentity here. A stray auth.workloadIdentity is silently
pruned by the API server (it's not in the schema), not rejected.
Customization — the values you actually change¶
url— the collection URL. Include the full path to the repository folder.create.enabled— initialize the repository if missing.moverDefaults.cache— mover cache sizing (movers).
As a ClusterRepository¶
The same backend.webDav stanza works on a cluster-scoped
ClusterRepository; every
Secret reference must carry an explicit namespace: and the Secret must exist
where the movers run — see Movers.
Try it end-to-end¶
Prove this backend really takes a backup. The same example file carries a tiny
smoke-test (a throwaway PVC + a SnapshotPolicy + a Snapshot) that targets the
webdav-primary repository above, so you can go from "applied" to "a snapshot in
my collection" in one arc.
Fill in the credentials first
The smoke backup only goes green once KOPIA_WEBDAV_USERNAME /
KOPIA_WEBDAV_PASSWORD are real basic-auth credentials (and url points at
an existing, writable collection). With the REPLACE_ME placeholders the
Repository stalls at Failed and the Snapshot stays Pending.
1. Apply the bundle (namespace backups, Secret, Repository, and the
smoke-test objects):
2. Wait for the repository to be Ready — the gate everything else waits on:
$ kubectl -n backups wait --for=condition=Ready repository/webdav-primary --timeout=2m
repository.kopiur.home-operations.com/webdav-primary condition met
3. Take the smoke backup. The Snapshot uses generateName, so create it
(the namespace, Secret, Repository, PVC, and policy already exist and report
unchanged — the Snapshot is the one new object):
$ kubectl create -f deploy/examples/backends/webdav.yaml
snapshot.kopiur.home-operations.com/smoke-now-abc12 created
4. Watch it succeed:
$ kubectl -n backups get snapshots -w
NAME PHASE ORIGIN SNAPSHOT AGE
smoke-now-abc12 Pending manual 2s
smoke-now-abc12 Running manual 7s
smoke-now-abc12 Succeeded manual k1f1ec0a8 38s
(Output illustrative.) The Snapshot has no fixed Succeeded condition; to
wait on it in a script, key on the phase:
$ kubectl -n backups wait --for=jsonpath='{.status.phase}'=Succeeded \
snapshot/smoke-now-abc12 --timeout=5m
5. Deep proof — the data really moved. status.stats shows non-zero
bytesNew/filesNew, and status.snapshot.kopiaSnapshotID is the kopia
snapshot ID in your collection:
$ kubectl -n backups get snapshot smoke-now-abc12 -o jsonpath='{.status.stats}'
{"sizeBytes":4096,"bytesNew":1280,"filesNew":2,"filesUnchanged":0}
$ kubectl -n backups get snapshot smoke-now-abc12 -o jsonpath='{.status.snapshot.kopiaSnapshotID}'
k1f1ec0a8
(Both outputs illustrative — sizes and the ID vary.) Non-zero bytesNew is the
proof the backup uploaded real content over WebDAV.
6. Clean up the smoke-test when you're done:
$ kubectl -n backups delete snapshot --all # finalizer also deletes the kopia snapshot
$ kubectl -n backups delete snapshotpolicy smoke
$ kubectl -n backups delete pvc smoke-data
Deleting a Snapshot deletes its snapshot
A produced Snapshot defaults to deletionPolicy: Delete, so removing the CR
runs kopia snapshot delete via a finalizer. Use Retain (or Orphan) to keep
the data — see Backups → deletionPolicy.
From here the full lifecycle is backend-independent — only the Repository
differs. Put it on a cron with a SnapshotSchedule
(Backups & schedules,
Example 01) and restore by
picking a Snapshot (Restores,
Example 03).
Troubleshooting¶
401 Unauthorized— wrong basic-auth username/password, or the server requires an app password (Nextcloud with two-factor) rather than the account password.404/409— the collection URL doesn't exist or isn't a writable collection. Create the folder and pointurlat it. On Nextcloud, also check the username in the path matches the login the credentials belong to.- TLS errors — the WebDAV backend speaks HTTPS; use a valid certificate (the
tlsoverrides available on S3 are not part ofbackend.webDav). For an internal server with a private CA, terminate with a publicly-trusted cert or use a different backend. - Slow backups — WebDAV is the chattiest backend (one HTTP request per blob operation, no multipart). Fine for modest datasets; for large or high-churn sources prefer an object store.
See also¶
- Repositories & backends — concepts: scope, encryption, creation.
- Movers, RBAC & credentials — where the credential Secret must live.
- Sibling backends: S3 · rclone (for Nextcloud and many others).