Skip to content

Deletion & Cleanup

This guide describes how the operator tears down a Nextcloud instance and everything it provisioned — the HelmRelease, S3 data and bucket, the managed database, secrets, PVCs and the namespace — and how to verify that nothing is left behind.

Deletion is destructive and irreversible by default

Deleting a Nextcloud removes its data (S3 objects, database, volumes). Verify backups and any data-retention requirements before you delete. There is no undo unless you configure reclaimPolicy: Retain on the pool — see reclaimPolicy: Retain below.

What gets deleted

Deleting the logical Nextcloud (nc) resource cascades to its NextcloudInstance (nci), whose deletion handler runs an ordered, blocking cleanup. The nci (and its namespace) only disappear once every step has succeeded — the finalizer keeps the resource in Terminating and the operator retries until cleanup completes.

Step Resource Removed when
1 HelmRelease (cascades pods, services, ingress) always
2 S3 objects + bucket bucket was auto-created by the operator (status.appliedS3Config.autoCreated: true)
2.5 S3Backup (data backup) spec.backups.data.deleteOnCleanup: true
3 Managed PostgreSQL (PerconaPGCluster) spec.database.deleteOnCleanup: true or the instance owns its namespace
4 Secrets (admin, db, redis, s3, mail, …) always
4.5 Orphaned PVCs only when the namespace is preserved (not owned)
5 Namespace the instance owns it (k8s.bnerd.com/instance label matches)

Cleanup flags

  • S3 — only auto-created buckets are emptied and deleted. A bucket you supplied yourself (spec.s3.bucket / credentialsSecret) is preserved; the operator never deletes user buckets. All objects, including non-current versions and delete markers, are paginated and removed before the bucket is dropped.
  • spec.database.deleteOnCleanup (default false) — delete the managed PerconaPGCluster. See the note below about owned namespaces.
  • spec.backups.data.deleteOnCleanup (default false) — delete the S3Backup resource (and its repository) instead of preserving it.

Owned namespaces always remove the database

When the instance owns its namespace (the normal case for operator-provisioned instances), namespace deletion removes the managed database and all PVCs regardless of deleteOnCleanup — there is nothing left to preserve once the namespace is gone. To keep a database, deploy it outside the instance's namespace and reference it. The operator deletes the PerconaPGCluster before tearing down the namespace and waits for its teardown to finish, so the namespace does not stall in Terminating on Percona finalizers.

PVCs

All PVCs the operator provisions (Nextcloud data, Redis, PostgreSQL data and pgBackRest) live inside the instance namespace, so deleting the namespace (Step 5) cascades and removes them. The explicit PVC sweep (Step 4.5) only runs in the edge case where the namespace is preserved (e.g. an instance pointed at a shared namespace via spec.instanceRef) — there it best-effort deletes PVCs labelled for this instance's HelmRelease (app.kubernetes.io/instance=<name>-nextcloud) and its PostgreSQL cluster (postgres-operator.crunchydata.com/cluster=<name>-pg).

reclaimPolicy: Retain

NextcloudPool.spec.lifecycle.reclaimPolicy (default Delete) controls what happens to live data when the assigned Nextcloud is deleted. Setting it to Retain preserves the tenant's data for manual inspection or migration instead of destroying it.

What is preserved vs. removed

Resource Delete (default) Retain
S3 bucket (auto-created) Emptied and deleted Preserved — recorded in audit log only (no K8s label)
Managed PostgreSQL (PerconaPGCluster) Deleted Preserved — inside retained namespace; audit log line emitted
PVCs (Nextcloud data, Redis, pg data) Deleted via namespace cascade Preserved — inside retained namespace
Owned namespace Deleted Preserved — receives label k8s.bnerd.com/reclaim=retained + annotation
HelmRelease (cascades pods, services, ingress) Deleted Deleted
Operator-owned Secrets (admin, db, redis, …) Deleted Deleted
NextcloudInstance (nci) object Removed Removed

In short: control-plane resources are always cleaned up; live data is preserved. Only the namespace receives the K8s label — the S3 bucket is traceable via the audit log only.

How retained resources are marked

Only the owned namespace receives a Kubernetes label and annotation:

  • Label: k8s.bnerd.com/reclaim=retained
  • Annotation: k8s.bnerd.com/reclaim-note: released-for-manual-reclamation

The PVCs and managed PostgreSQL cluster live inside the retained namespace, so they are discoverable via the namespace label. The S3 bucket is an external resource — it carries no Kubernetes label. All three are recorded in the operator audit log with an INFO-level line:

S3 reclaim audit: instance=<ns>/<name> bucket=<bucket> reclaim=retained released-for-manual-reclamation (Retain policy — bucket preserved)
PG reclaim audit: instance=<ns>/<name> pgcluster=<name>-pg reclaim=retained released-for-manual-reclamation (Retain policy — managed DB preserved)
Namespace reclaim audit: instance=<ns>/<name> namespace=<ns> reclaim=retained released-for-manual-reclamation (Retain policy — namespace preserved)

The audit log is the authoritative record for the S3 bucket name — since the bucket carries no tag, the log is the only machine-readable trace linking it to the deleted instance.

S3Backup is orthogonal

reclaimPolicy does not gate the S3Backup resource. The backup follows its own flag: spec.backups.data.deleteOnCleanup (default false). Under Retain, the S3 data bucket is preserved AND the backup is also preserved (unless deleteOnCleanup: true is set independently).

Fail-safe to Delete

Any failure to resolve the pool or its lifecycle policy — pool label missing, pool CR unreadable, field absent or invalid — causes the operator to fail safe to Delete. Retain is only applied when it is unambiguously and explicitly configured. A lookup failure can never silently preserve data or widen deletion.

Configuring reclaimPolicy

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudPool
metadata:
  name: production
spec:
  replicas: 5
  lifecycle:
    reclaimPolicy: Retain  # default: Delete

Runbook: manually reclaiming retained resources

After a Retain-policy deletion, the live resources remain in the cluster. The owned namespace is labeled k8s.bnerd.com/reclaim=retained — use that to locate everything that was preserved. The S3 bucket name is recorded only in the operator audit log.

1. Find retained namespaces

kubectl get ns -l k8s.bnerd.com/reclaim=retained

2. Inspect resources inside a retained namespace

The PVCs, managed PostgreSQL cluster, and remaining workloads live inside the retained namespace. The namespace label is the entry point — not the resources themselves:

kubectl get all,pvc -n <retained-namespace>

3. Find the S3 bucket name via the audit log

The S3 bucket carries no Kubernetes tag. Its name is recorded in the operator log:

kubectl logs -n <operator-ns> deploy/nextcloud-operator \
  | grep -E "S3 reclaim audit.*reclaim=retained"

The log line includes the bucket name: bucket=<bucket-name>.

4. Manual cleanup sequence

Once you have verified or migrated the data, clean up in this order:

  1. Delete S3 bucket — use your S3 client (the operator has released ownership):

    aws s3 rb s3://<bucket-name> --force
    # or via mc (MinIO client):
    mc rb --force <alias>/<bucket-name>
    

  2. Delete the managed PostgreSQL cluster:

    kubectl delete perconapgcluster <name>-pg -n <retained-namespace>
    

  3. Delete residual PVCs (if any survive the pg cluster teardown):

    kubectl get pvc -n <retained-namespace>
    kubectl delete pvc --all -n <retained-namespace>
    

  4. Delete the retained namespace:

    kubectl delete ns <retained-namespace>
    

Wait for Percona finalizers

Deleting the PerconaPGCluster first and waiting for it to finish prevents the namespace from stalling in Terminating on Percona's finalizers. Check with kubectl get perconapgcluster -n <ns> before deleting the namespace.


Recreate safety

You can delete a Nextcloud and immediately recreate one with the same name without producing a duplicate namespace. The operator binds each instance to the Nextcloud's uid; on recreate it detects the previous instance still terminating and waits for it to finish before provisioning a fresh one. The old, orphaned instance is never re-blocked by its same-named successor and always completes its own cleanup.

Audit trail

S3 teardown emits one consolidated, INFO-level audit line per bucket:

S3 cleanup audit: instance=<namespace>/<instance> bucket=<bucket> objects=<N> versions=<M> status=deleted

status is deleted, absent (bucket already gone), or failed. PVC sweeps log PVC cleanup audit: instance=<namespace>/<instance> deleted pvc=<name>. Capture these from the operator logs for compliance.

Runbook: delete a Nextcloud instance

  1. Pre-flight — confirm the instance is safe to delete: no active users, data-retention period expired, backups verified if required. Note the bucket name from kubectl get nci <inst> -n <ns> -o jsonpath='{.status.appliedS3Config.bucket}'.
  2. Delete the resource:
    kubectl delete nc <name> -n <namespace>
    
  3. Monitor progress:
    kubectl get nc <name> -n <namespace>          # eventually NotFound
    kubectl get ns | grep <instance-namespace>     # shows Terminating, then gone
    kubectl logs -n <operator-ns> deploy/nextcloud-operator | grep -E "CLEANUP|S3 cleanup audit"
    
  4. Verify cleanup is complete:
    kubectl get ns | grep -c <instance-namespace>          # → 0
    kubectl get pvc -A | grep <instance-namespace>          # → no rows
    # S3 bucket no longer lists (using your S3 client of choice)
    

Troubleshooting

Namespace stuck in Terminating

A namespace usually finishes terminating within a few minutes. If it stalls:

kubectl describe namespace <name> | grep -iA3 finalizers
kubectl get nci -n <name> -o jsonpath='{.items[*].metadata.finalizers}'

Most often a NextcloudInstance cleanup step is still failing (e.g. a managed DB whose operator is unreachable, or an S3 endpoint that is down) and the operator is retrying — check the operator logs for the blocking step. See Troubleshooting → Finalizer blocks namespace deletion.

Last resort only

Force-clearing a namespace's finalizers abandons whatever the finalizer owner was cleaning up and can orphan storage. Only do this if the finalizer's owner is permanently gone:

kubectl patch namespace <name> -p '{"metadata":{"finalizers":null}}'

Instance refuses to delete (assigned to an active Nextcloud)

A pool-assigned instance is protected from direct deletion. Delete the Nextcloud instead, or use the force-delete escape hatch — see Operations → Force Delete.

S3 bucket not deleted

The operator only deletes auto-created buckets. If you supplied the bucket, delete it yourself. If an auto-created bucket survives, check the operator logs for the S3 cleanup audit: … status=failed line and the preceding error (credentials, endpoint reachability, or bucket policy).

S3 partial delete failure (delete_objects Errors)

The S3 DeleteObjects API returns HTTP 200 even when individual keys fail — failures are reported in the Errors list of the response, not as an HTTP error. The operator explicitly checks this list. If any objects failed to delete, it logs the per-key error codes and raises S3PartialDeleteError, which causes the on-delete handler to block deletion and lets kopf retry — ensuring no orphaned non-empty bucket is left behind.

delete_objects reported 3 error(s) for bucket my-bucket: data/file1.bin: AccessDenied, …

Retry window — S3 credentials Secret is preserved: When the S3 bucket teardown fails, the operator skips deleting the S3 credentials Secret so the next kopf retry can still authenticate and finish emptying the bucket. Once bucket deletion succeeds, the Secret is removed on the next cleanup pass.

To diagnose a stuck instance, check the operator logs for the bucket name and the S3 error codes:

kubectl logs -n <operator-ns> deploy/nextcloud-operator \
  | grep -E "delete_objects|S3 cleanup audit|S3 bucket delete"

Common error codes from S3:

Code Likely cause
AccessDenied Bucket policy or IAM permission blocks deletion
NoSuchKey Key already gone (safe to retry; will clear on next pass)
InternalError Transient S3 endpoint issue; kopf will retry

Orphaned PVCs

If an instance did not own its namespace and PVCs remain, delete them by instance label:

kubectl get pvc -n <namespace> -l app.kubernetes.io/instance=<name>-nextcloud
kubectl get pvc -n <namespace> -l postgres-operator.crunchydata.com/cluster=<name>-pg

See also