Skip to content

App Management

Declarative Nextcloud app installation and configuration via spec.apps, and — as of 0.20.0 — how to verify that what you declared actually landed.

Overview

spec.apps lets you version-control which Nextcloud apps an instance has installed, alongside the rest of its spec, instead of running manual occ app:install commands. Well-known apps (e.g. richdocuments, spreed) have their own typed sub-schemas for app-specific configuration; anything else goes under spec.apps.custom as an {appId, enabled} escape hatch. user_oidc is reserved — the operator manages it itself via spec.oidc and it must not be listed under spec.apps.

spec:
  apps:
    richdocuments:
      enabled: true
      wopiUrl: https://collabora.example.com
    spreed:
      enabled: true
    custom:
      contacts:
        enabled: true

See the API reference for the full well-known-apps field list and the custom escape-hatch schema.

How app installation actually works

App management is applied through the Helm chart's before-starting hook: the operator generates php occ app:install <app> || true (and the equivalent for enable/disable) for every declared app, materialized into the hook script that runs before Nextcloud serves traffic. The || true is deliberate and stays — without it, one bad app entry would fail the entire hook and block the instance from becoming Ready over something as fixable as one app. This means the hook is, and remains, best-effort: a failed app:install is silently absorbed by design, not a bug.

Before 0.20.0, that also meant a failed install was invisible — the hook succeeded (because of || true), the instance went Ready, and nothing in status or the event stream said an app was actually missing. Operators found out only when a user reported the feature absent.

Verifying app state

As of 0.20.0, the operator independently verifies what the hook actually achieved, rather than trusting that it ran cleanly. On the maintenance timer, for Ready instances with spec.apps set, the operator runs occ app:list --output json and diffs it against spec.apps — an authoritative, after-the-fact check, not hook-output parsing.

status.apps

status:
  apps:
    desired: 3                        # apps tracked from spec.apps (excludes reserved apps like user_oidc)
    installed: ["richdocuments", "spreed"]   # desired-enabled, confirmed live-enabled
    missing: ["contacts"]                     # desired-enabled, absent from the instance entirely
    disabled: []                              # present live, but enabled/disabled state doesn't match spec.apps (either direction)
    lastCheckedAt: "2026-08-12T02:00:00Z"
  • missing is the core signal this feature exists to surface — a desired-enabled app that occ app:list doesn't report at all, i.e. its install genuinely failed (or was never attempted).
  • disabled covers a live/desired mismatch in either direction: an app you declared enabled that's live-disabled, or one you declared disabled (or omitted, since enabled defaults to true) that's live-enabled anyway.
  • An app you declared disabled that's correctly disabled (or simply not installed) live lands in no bucket — nothing to report.
  • desired counts every tracked app (top-level keys plus spec.apps.custom's own keys, minus user_oidc), independent of how the three buckets happen to sum — a correctly-disabled app is invisible in the diff buckets but still "desired."
kubectl get nci my-instance -n my-namespace -o jsonpath='{.status.apps}' | jq

AppsHealthy condition

status reason Meaning
True AllAppsHealthy Every declared app is installed and matches its desired enabled/disabled state.
False AppsMissing At least one desired-enabled app is absent from the instance. The message names the missing apps.
False PostUpgradeAppsDisabled (0.23.0) A version upgrade left declared apps disabled and the operator's convergence loop is retrying — see Upgrades → Post-upgrade app convergence. Also set (and left False) when a human accepts an incomplete state with k8s.bnerd.com/upgrade-apps-accept.
kubectl wait nci my-instance -n my-namespace --for=condition=AppsHealthy=True --timeout=600s
kubectl get nci my-instance -n my-namespace -o jsonpath='{.status.conditions[?(@.type=="AppsHealthy")]}' | jq

Note the condition only reflects missing apps, not disabled ones — an enabled/disabled mismatch doesn't flip AppsHealthy to False; check status.apps.disabled directly if you also care about that.

Warning events are edge-triggered, not per-tick

An AppsMissing warning event fires only for apps newly entering missing this check — computed as the difference between this tick's missing set and the previous tick's recorded status.apps.missing. An app that was already missing on the prior check does not re-fire the event every subsequent tick; status.apps/AppsHealthy still refresh every tick regardless, so kubectl get/describe always reflects current reality, but your event stream doesn't get spammed by a steady-state failure you haven't fixed yet.

kubectl get events -n my-namespace --field-selector involvedObject.name=my-instance | grep AppsMissing

Check cadence

The check runs on the periodic maintenance timer (TIMER_MAINTENANCE_INTERVAL, default 15 minutes), for any Ready instance with spec.apps set — before the spec.maintenance.windowStart gate, so it runs even for instances with no maintenance window configured at all. This is deliberate: the window gates upgrade/maintenance application, never pure-observation visibility (the same rationale as the UpdateAvailable condition) — the check itself never mutates anything on the live instance, it only reads and reports.

There is currently no on-demand trigger for this specific check — it only runs on the periodic timer's own cadence. k8s.bnerd.com/run-maintenance (see Operations & Annotations) dispatches to a different function (run_maintenance) and does not re-run the apps-health check; if you need a fresher read sooner than the next timer tick, run occ app:list --output=json yourself via NextcloudCommand.

Failure handling

If the occ app:list check itself fails (pod unreachable, non-zero exit, unparseable output), the operator logs a warning and leaves status.apps and AppsHealthy completely untouched for that tick — it never overwrites a last-known-good state with a false "everything's missing" reading, and it never blocks the rest of that maintenance tick's other work (post-upgrade tasks, periodic cleanup, etc.). The next tick simply retries.

The honest caveat: the hook stays best-effort, status.apps is the truth

While an upgrade is in flight, this check does not write AppsHealthy at all — the upgrade flow owns it (see below); status.apps still refreshes every tick.

Upgrades are the exception, since 0.23.0. App state after a version change is actively repaired: the operator runs app:update --all, re-asserts every declared app (including user_oidc, which this page's status.apps deliberately excludes), verifies with app:list, and retries until it converges — see Upgrades → Post-upgrade app convergence. Everything below is about the steady state, where the following still holds.

status.apps/AppsHealthy is a detection mechanism, not a retry mechanism — the operator does not automatically re-attempt a failed app:install because it noticed it's missing. The before-starting hook's || true install attempts still only run when the hook itself re-materializes (a spec change, an instance reconcile that rebuilds the hook), not on every maintenance tick. If an app is stuck in status.apps.missing, the fix is the same manual step as before this feature existed — investigate why the install failed (check the pod's before-starting hook logs, or run the install directly) — this feature's contribution is that you now know it's missing without a user reporting it first, not that the operator fixes it for you.

# Investigate directly via NextcloudCommand rather than kubectl exec
# (see Running occ Commands)
kubectl apply -f - <<'EOF'
apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudCommand
metadata:
  name: install-contacts
  namespace: my-namespace
spec:
  target:
    name: my-instance
  commands:
    - "app:install contacts"
    - "app:list --output=json"
EOF

See Running occ Commands for the full NextcloudCommand reference.

See also