Skip to content

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/kopia or 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

  1. Create the target folder (kopia here) in the Files app.
  2. Personal settings → Security → Devices & sessionsCreate 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.
  3. 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 login alice). The deprecated /remote.php/webdav/… form still works but the dav/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):

$ kubectl apply -f deploy/examples/backends/webdav.yaml

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 point url at 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 tls overrides available on S3 are not part of backend.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