Nextcloud Helm
Overview
Nextcloud deployed via HelmRelease (Flux) with optional external PostgreSQL, Redis caching, S3 primary storage. Chart uses Docker images from docker.io/library/nextcloud with apache or fpm flavor. Built-in HTTPRoute (Gateway API v1) support, Ingress, nginx sidecar when using fpm image.
Latest stable: chart 9.2.x → Nextcloud v34. See artifacthub for latest version.
Dependencies:
- PostgreSQL 16.7.4 (bitnami, OCI)
- MariaDB 20.5.5 (bitnami, OCI)
- Redis 21.1.3 (bitnami, OCI)
- Collabora Online 1.1.60
Architecture
Cilium Gateway / Ingress
└── HTTPRoute / Ingress → nextcloud:8080
Nextcloud Deployment (apache image)
├── Sidecar: crond (optional, sidecar mode)
├── InitContainer: DB wait (PostgreSQL or MariaDB)
└── Volume: /var/www/html (PVC)
External PostgreSQL (recommended)
└── nextcloud-db-rw:5432
External Redis (caching)
└── redis:6379
S3 Object Store (primary storage, optional)
└── S3 bucket via env vars
Imaginary (image previews, optional)
└── nextcloud-imaginary:9000
Key Configuration
Image
image:
registry: docker.io
repository: library/nextcloud
flavor: apache # "apache" or "fpm"
tag: "" # generated from appVersion
pullPolicy: IfNotPresent
Two flavors: apache (all-in-one, simpler) or fpm (needs nginx sidecar, more configurable).
Gateway API (HTTPRoute)
Built-in HTTPRoute support — no Ingress needed:
httpRoute:
enabled: true
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
hostnames:
- "nextcloud.example.com"
parentRefs:
- name: cilium-gateway
namespace: cilium-gateway
sectionName: https
rules:
- matches:
- path:
type: PathPrefix
value: "/"
# Automatically adds .well-known/carddav and .well-known/caldav redirects
wellKnown:
enabled: true
The chart's wellKnown.enabled: true (default) automatically adds Gateway API RequestRedirect rules for /.well-known/carddav and /.well-known/caldav → /remote.php/dav.
Database
External PostgreSQL (recommended for production):
internalDatabase:
enabled: false
externalDatabase:
enabled: true
type: postgresql
host: nextcloud-db-rw.nextcloud.svc.cluster.local:5432
user: nextcloud
password: "" # via valuesFrom secret
database: nextcloud
existingSecret:
enabled: true
secretName: nextcloud-db-credentials
usernameKey: db-username
passwordKey: db-password
hostKey: db-host
databaseKey: db-name
Bundled MariaDB (for testing/small deployments):
internalDatabase:
enabled: false
mariadb:
enabled: true
auth:
database: nextcloud
username: nextcloud
password: changeme
primary:
persistence:
enabled: true
size: 8Gi
Redis Caching
External Redis (recommended):
externalRedis:
enabled: true
host: redis-master.redis.svc.cluster.local
port: "6379"
password: "" # via valuesFrom secret
existingSecret:
enabled: false
Bundled Redis:
redis:
enabled: true
auth:
password: changeme
master:
persistence:
enabled: true
SMTP / Mail
nextcloud:
mail:
enabled: true
fromAddress: noreply
domain: example.com
smtp:
host: smtp-relay.brevo.com
secure: ssl
port: 465
authtype: LOGIN
name: noreply@example.com
password: "" # via valuesFrom secret
S3 Primary Object Store
nextcloud:
objectStore:
s3:
enabled: true
host: s3.eu-central-1.amazonaws.com
ssl: true
port: "443"
region: eu-central-1
bucket: nextcloud-data
accessKey: "" # ignored if existingSecret set
secretKey: ""
usePathStyle: false
autoCreate: true
existingSecret: nextcloud-s3-credentials
Imaginary (Image Previews)
High-performance image preview server. Dramatically faster than PHP-based previews:
imaginary:
enabled: true
image:
registry: docker.io
repository: h2non/imaginary
tag: 1.2.4
resources:
requests:
memory: 256Mi
cpu: 100m
Also needs config to enable previews:
nextcloud:
configs:
previews.config.php: |
<?php
$CONFIG = array (
'enable_previews' => true,
'enabledPreviewProviders' => array (
'OC\Preview\Imaginary',
'OC\Preview\Movie',
'OC\Preview\PNG',
'OC\Preview\JPEG',
'OC\Preview\GIF',
'OC\Preview\BMP',
'OC\Preview\MP3',
'OC\Preview\TXT',
'OC\Preview\MarkDown',
'OC\Preview\PDF',
),
);
And Imaginary support config:
nextcloud:
defaultConfigs:
imaginary.config.php: true
Cron Jobs
Two modes — sidecar (crond alongside Nextcloud, requires root) or cronjob (Kubernetes CronJob, can run non-root):
cronjob:
enabled: true
type: cronjob # or "sidecar"
cronjob:
schedule: "*/5 * * * *"
command:
- php
- -f
- /var/www/html/cron.php
- --
- --verbose
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- nextcloud
- key: app.kubernetes.io/component
operator: In
values:
- app
topologyKey: kubernetes.io/hostname
CronJob mode requires persistence.enabled: true (same PVC).
Metrics / Monitoring
Nextcloud built-in OpenMetrics (exposes at /index.php/apps/helm-metrics/metrics):
nextcloud:
openmetrics:
allowedClients:
- "127.0.0.1"
- "10.42.0.0/16"
nextcloud-exporter sidecar service:
metrics:
enabled: true
https: false
info:
apps: true
update: true
serviceMonitor:
enabled: true
interval: 30s
Persistence
persistence:
enabled: true
storageClass: ceph-block
accessMode: ReadWriteOnce
size: 50Gi
# Optional: separate data PVC on different storageClass
nextcloudData:
enabled: false
storageClass: ""
size: 50Gi
Upgrade
- Update
version:in HelmRelease to new chart version - Nextcloud handles its own DB migrations via
occ upgradeon startup - If using
oc_appconfigentries for apps, runocc app:update --allafter upgrade - Default strategy is
Recreate— expect brief downtime - PHP config changes may require manual occ commands after upgrade
- For major Nextcloud version bumps, check UPGRADE.md
Post-Upgrade Verification
# Inside the pod
occ status
occ db:add-missing-indices
occ db:convert-filecache-bigint
occ maintenance:repair
Quick Reference
| Need | Config path |
|---|---|
| Nextcloud admin user | nextcloud.username, nextcloud.password or nextcloud.existingSecret |
| External hostname | nextcloud.host |
| Trusted domains | nextcloud.trustedDomains |
| External DB host | externalDatabase.host |
| External DB credentials | externalDatabase.password or externalDatabase.existingSecret |
| S3 bucket | nextcloud.objectStore.s3 |
| Redis host | externalRedis.host or redis.* |
| SMTP | nextcloud.mail.* |
| Image previews | imaginary.enabled: true + configs |
| Gateway API | httpRoute.* |
| Cron type | cronjob.type: sidecar or cronjob |
| ServiceMonitor | metrics.serviceMonitor.enabled: true |
| Nginx sidecar | nginx.enabled: true (requires fpm image flavor) |
| PHP config overrides | nextcloud.phpConfigs |
| Custom Nextcloud config | nextcloud.configs |
| Upload size limit | nginx client_max_body_size in nginx.config.serverBlockCustom |
OIDC / OpenID Connect Integration
Chart has no native OIDC section in values. OIDC is handled by the user_oidc app (official, apps.nextcloud.com/apps/user_oidc, v8.10.1 for NC34).
Post-install setup
# Install the app (usually bundled, enable via occ)
kubectl exec deploy/nextcloud -- php occ app:enable user_oidc
# Register OIDC provider (ZITADEL, Keycloak, etc.)
kubectl exec deploy/nextcloud -- php occ user_oidc:provider zitadel \
--clientid="<client-id>" \
--clientsecret="<client-secret>" \
--discoveryuri="https://auth.example.com/.well-known/openid-configuration"
Auto-provisioning (default: on)
New users created on first OIDC login. Disable in config.php:
nextcloud:
configs:
20-oidc.config.php: |
<?php
$CONFIG = array (
'user_oidc' => array (
'auto_provision' => false, # Don't auto-create users
'soft_auto_provision' => true, # Accept existing users from other backends
'disable_account_creation' => false, # Don't create accounts for new users
),
);
Via lifecycle hooks
Automate provider registration on deploy:
nextcloud:
hooks:
post-installation:
- |
php occ app:enable user_oidc
php occ user_oidc:provider zitadel \
--clientid="${OIDC_CLIENT_ID}" \
--clientsecret="${OIDC_CLIENT_SECRET}" \
--discoveryuri="https://auth.example.com/.well-known/openid-configuration"
extraEnv:
- name: OIDC_CLIENT_ID
valueFrom:
secretKeyRef:
name: nextcloud-oidc
key: client-id
- name: OIDC_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: nextcloud-oidc
key: client-secret
ZITADEL OIDC app config
Create OIDC app in ZITADEL (Authorization Code + client_secret_basic):
# Redirect URI
https://nextcloud.example.com/index.php/apps/user_oidc/code?flow=1
# Post-logout redirect
https://nextcloud.example.com/
Bearer token validation
For API access via OIDC tokens:
nextcloud:
configs:
21-oidc-bearer.config.php: |
<?php
$CONFIG = array (
'user_oidc' => array (
'userinfo_bearer_validation' => true,
),
);
Key occ commands
# List providers
occ user_oidc:provider
# Show provider details
occ user_oidc:provider zitadel
# Delete provider
occ user_oidc:provider:delete zitadel
# Disable default claims (quota, name, email, groups)
occ config:app:set --value=0 user_oidc enable_default_claims
# Single logout (default: on)
# config.php: 'user_oidc' => ['single_logout' => false]
# PKCE (default: on)
# config.php: 'user_oidc' => ['use_pkce' => false]
Common Mistakes
- Flavor mismatch —
flavor: apachebundles Apache + PHP-FPM in one image.flavor: fpmneedsnginx.enabled: truesidecar. Mixing them gets blank pages. - Recreate strategy — Default
strategy.type: Recreatemeans brief downtime on upgrades. Set toRollingUpdateif zero-downtime is needed (requires careful readiness probe tuning). - Redis password — Bundled Redis uses
auth.password. External Redis usesexternalRedis.password. Mismatch causes Nextcloud to silently degrade to file-based caching. - S3 endpoint trailing slash — Do NOT include trailing slash in S3
hostfield. Nextcloud appends path. - Database upgrade timeout — Large Nextcloud DB migrations can exceed the default liveness probe timeout. Increase
livenessProbe.initialDelaySecondsortimeoutSecondsduring upgrades. - Config files order —
nextcloud.configsfiles are loaded alphabetically. Prefix with numbers if ordering matters (e.g.,10-previews.config.php). - CronJob needs same PVC — When using
cronjob.type: cronjob, the CronJob pod must access the same PVC. Use pod affinity to ensure same node scheduling. - HTTPRoute wellKnown redirects —
httpRoute.wellKnown.enabled: trueaddsRequestRedirectrules for.well-known/carddavand.well-known/caldav. Disable if you handle these differently. - OpenMetrics IP allowlist —
nextcloud.openmetrics.allowedClientsmust include Prometheus pod CIDR. Default includes common K8s CIDRs (10.42.0.0/16, 10.43.0.0/16).