Nextcloud¶
Nextcloud is the household's file storage, calendar
and contacts. It uses Nextcloud's own Helm chart, configured through
valuesObject; the manifests the chart does not render (the database, the
HTTPRoute, the network policy, the ExternalSecrets, the Authentik
blueprint) come from a second source pointing at this repository.
At a glance¶
| URL | cloud.k8s.wlkr.ch |
| Authentication | Authentik OIDC through the first-party user_oidc app; local login hidden |
| Chart | nextcloud, from https://nextcloud.github.io/helm/ |
| Storage | 50Gi rook-ceph-block PVC, ReadWriteOnce |
| Database | CloudNativePG Cluster nextcloud-db |
| Secrets | kv/nextcloud/config in OpenBao |
| Files | nextcloud/ |
Configuration¶
The values in nextcloud/application.yaml that are not self-explanatory:
| Setting | Why |
|---|---|
internalDatabase.enabled: false, externalDatabase.existingSecret |
The bundled Bitnami subchart is replaced by a CloudNativePG Cluster at sync wave -1, per the contract. ArgoCD's health check for Cluster makes the wave wait for a working database before the chart's install job connects |
redis.enabled: false |
Another Bitnami subchart. One replica needs no shared cache; the chart configures APCu |
OVERWRITEPROTOCOL, OVERWRITEHOST, OVERWRITECLIURL, TRUSTED_PROXIES in extraEnv |
TLS terminates at the Gateway and requests arrive from Cilium's Envoy, so without these Nextcloud builds http:// links and the login redirect loops. Env vars rather than a proxy.config.php: the image ships reverse-proxy.config.php reading exactly these, and config files load alphabetically, so a proxy.config.php of our own would lose |
strategy: Recreate, replicaCount: 1 |
The data PVC is ReadWriteOnce; a RollingUpdate surges a pod that cannot mount it |
Background jobs as a sidecar, not a CronJob |
Same volume, same reason |
startupProbe with failureThreshold: 60 |
Upgrades run database migrations at startup, longer than a liveness probe tolerates |
prometheus.serviceMonitor, not metrics.serviceMonitor |
The chart's values file documents the latter; the template reads the former. The wrong key renders nothing, silently. metrics.enabled does control the exporter |
nextcloud.openmetrics.allowedClients |
The default is k3s's CIDRs, which reject Prometheus while the ServiceMonitor looks healthy |
nextcloud.existingSecret |
Without it the chart writes admin / changeme into its own Secret and points the install and the exporter at it. Nothing fails at sync time; check the block is present on every values change |
allow_local_remote_servers, set by the startup hook |
auth.k8s.wlkr.ch resolves to the Gateway's private address and Nextcloud's SSRF guard rejects it (violates local access rules) while curl from the same container works. Lifting it applies to every outbound request; the compensating controls are the network policy and that federation and external storage are off. Pointing at the in-cluster Service instead would break the iss check |
The database wiring, the same for every application here:
externalDatabase:
enabled: true
type: postgresql
existingSecret:
enabled: true
secretName: nextcloud-db-app
usernameKey: username
passwordKey: password
hostKey: host
databaseKey: dbname
Usage¶
OIDC¶
A before-starting hook configures user_oidc on every start, after any
install or upgrade, so one hook covers both and re-asserts the configuration
if someone changes it in the UI. Every command is idempotent:
php occ app:install user_oidc || php occ app:enable user_oidc
php occ user_oidc:provider Authentik \
--clientid="$NEXTCLOUD_OIDC_CLIENT_ID" \
--clientsecret="$NEXTCLOUD_OIDC_CLIENT_SECRET" \
--discoveryuri="https://auth.k8s.wlkr.ch/application/o/nextcloud/.well-known/openid-configuration" \
...
php occ config:system:set hide_login_form --type boolean --value true
The user_oidc:provider call is not fatal: blueprint discovery is
asynchronous, so on a first deploy the provider may not exist yet. The login
form stays visible, the pod logs a warning, and the next restart configures it.
The provider itself is nextcloud/authentik-blueprint.yaml, a ConfigMap
targeted at the authentik namespace, so provider and client change in one
commit. The platform mounts it as an optional projected volume, because this
arrives four stages after Authentik must be Healthy. The client credentials
stay in the platform repository (make bao-secrets generates them), so a
workload cannot take itself out from behind SSO on its own.
Use auth.k8s.wlkr.ch, never auth.infra.k8s.wlkr.ch: the *.infra zone
resolves only on the local network, and a client must use one name
consistently or the iss claim fails. See
Two hostnames.
Secrets¶
kv/nextcloud/config holds four values, generated by make bao-secrets in the
platform repository. It is its own path rather than keys under
kv/authentik/config because a bao kv put replaces a path wholesale.
| Key | Read by |
|---|---|
username, password |
Nextcloud, as the break-glass admin; the metrics exporter authenticates as it too |
oidc-client-id, oidc-client-secret |
Nextcloud and Authentik, through two ExternalSecrets |
Break-glass¶
hide_login_form hides the fields; it does not disable local accounts. The
admin from make bao-secrets still works at:
Same reasoning as Grafana's break-glass admin.
Health check¶
kubectl -n nextcloud get pods,pvc
kubectl -n nextcloud get cluster nextcloud-db
kubectl -n nextcloud exec deploy/nextcloud -c nextcloud -- php occ status
occ is the administrative CLI, including occ maintenance:repair and
occ db:add-missing-indices, which the admin overview will eventually ask for.
Pitfalls¶
Sync stuck on the database¶
The default-deny policy must allow the cnpg-system namespace to reach the
database pod on port 8000, the instance manager's status endpoint the operator
polls, and the policy must sit at sync wave -2 with the namespace. At the
default wave it lands after the Cluster at -1, and a wave that never
finishes never applies the rule that would unblock it.
-
Symptom. Postgres serves the application and
kubectl get clusterreads1/1, but the phase isInstance Status Extraction Error: HTTP communication issue. The Application isSyncedwith healthUnknownand every entry instatus.resourceslacks a health field: a genuinely unhealthy workload names the failing resource. -
Confirm the drop from the node running the database:
-
Check which waves ran. If
CiliumNetworkPolicyis missing whileClusterreadsRunning, the policy is behind the thing it should unblock: -
Fix. Put the policy at wave
-2, then terminate the stuck operation. The next sync applies the policy first and theClustergoes Healthy.