Skip to content

Redis Topology and pgBouncer

Overview

Two cost-reduction controls ship with the operator's managed-infrastructure layer:

  1. Redis topology (redis.architecture) — single standalone pod (default) vs. master + replicas for HA.
  2. pgBouncer connection pooling (database.postgres.proxy.enabled) — off by default; opt in for production workloads that need connection multiplexing.

Both are configurable through all four CRDs — Nextcloud, NextcloudInstance, NextcloudProfile, and NextcloudPool — following the existing four-CRD cascade:

profile defaults → pool template → Nextcloud spec → NextcloudInstance spec

Instance spec wins on conflict; profile or pool can set a shared baseline.


Managed Redis topology (spec.redis.architecture)

Applies only when Redis is managed by the operator (i.e., spec.redis.enabled: true and no spec.redis.host set). When an external Redis host is provided the architecture field is ignored — you control that cluster yourself.

Values

Value Pods deployed Use case
standalone redis-master-0 only Development, staging, cost-optimized single-tenant
replication redis-master-0 + N redis-replica-* Production HA

Default behavior

The operator applies a code-level default of standalone for new instances. There is no CRD-level default; the choice is made in operator code after profile resolution so profile overrides are not defeated by Kubernetes admission stamping.

Backfill-on-adopt: when a managed instance already has a HelmRelease deployed and architecture is absent from the spec, the operator pins replication on the first reconcile and persists it via a spec patch. This preserves the HA cache configuration of instances created before this field existed, with no pod churn.

replicaCount

When architecture: replication, you may set redis.replicaCount to control how many replica pods the bitnami Redis subchart deploys. When unset the bitnami chart default applies (typically 3).

Nextcloud→Redis host

The Nextcloud application connects to {release}-redis-master in both architectures. Switching topology does not change the Redis host, so no Nextcloud configuration update is needed when changing architecture.

Examples

Minimal — single Redis (cost-optimized)

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: nextcloud-cost-optimized
spec:
  database:
    managed: true
    type: postgresql
  redis:
    enabled: true
    # architecture unset → operator applies standalone default for new instances
  admin:
    username: admin
    password: "change-me"
  ingress:
    host: nextcloud.example.com

HA Redis — explicit replication

redis:
  enabled: true
  architecture: replication
  replicaCount: 3

Via a NextcloudProfile (applies to all instances using the profile)

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudProfile
metadata:
  name: ha-profile
spec:
  defaults:
    redis:
      architecture: replication
      replicaCount: 3

Production profile

The built-in production profile sets:

redis:
  architecture: replication
  replicaCount: 3

Instances using profile: production inherit HA Redis by default.


Managed pgBouncer (database.postgres.proxy.enabled)

Applies only to managed PostgreSQL clusters (database.managed: true, database.type: postgresql).

When enabled, the Percona PG Operator provisions pgBouncer pods alongside the PostgreSQL cluster. The operator routes Nextcloud to the pgBouncer service endpoint (-pgbouncer) instead of the primary PostgreSQL endpoint (-primary).

When disabled (the default), no pgBouncer pods are created and Nextcloud connects directly to the PostgreSQL primary service. This is correct and fully supported; pgBouncer is an optional optimization for workloads with high connection churn.

Fields

Field Type Default Description
database.postgres.proxy.enabled boolean false (operator default) Enable pgBouncer
database.postgres.proxy.replicas integer 2 pgBouncer pod count (only when enabled)
database.postgres.proxy.poolMode string transaction PgBouncer pool mode (session, transaction, statement)
database.postgres.proxy.poolSize integer 25 Default pool size per database-user pair

There is no CRD-level default on proxy.enabled; the false default is applied in operator code after profile resolution.

DB host wiring

The operator bakes the resolved host into the <instance>-nextcloud-db Secret (db-host key) and into the externalDatabase.host Helm value. The behavior differs depending on direction and instance state:

  • Disabling (enabled true→false) on an existing instance: fully automatic on reconcile — the operator removes the bouncer block from the live PerconaPGCluster and rewrites the DB Secret to point at <cluster>-primary. No manual step.
  • Enabling (false→true) on an existing instance: the operator adds the bouncer block to the live cluster immediately. Once pgBouncer is Ready, the 30-second status timer detects the host drift and surgically re-points the HelmRelease (externalDatabase.host) so Flux rolls Nextcloud onto the bouncer — typically within ~30 seconds of bouncer readiness. No manual step required (as of v0.19.1). The k8s.bnerd.com/reconcile annotation triggers the flip immediately if you cannot wait for the next timer tick.
  • New instances with pgBouncer enabled (including the production profile): the host is resolved at create time; the wait-for-DB initContainer gates pod start. No manual step.

Production profile

The built-in production profile sets:

database:
  postgres:
    proxy:
      enabled: true

Instances using profile: production with a managed database get pgBouncer by default.

Example

apiVersion: k8s.bnerd.com/v1alpha1
kind: NextcloudInstance
metadata:
  name: nextcloud-ha
spec:
  profile: production
  database:
    managed: true
    type: postgresql
    postgres:
      replicas: 3
      version: "16"
      proxy:
        enabled: true
        replicas: 2
        poolMode: transaction
        poolSize: 25
  redis:
    enabled: true
    architecture: replication
    replicaCount: 3
  admin:
    username: admin
    password: "change-me"
  ingress:
    host: nextcloud.example.com

Configuring via all four CRDs

Both fields follow the same cascade as all other spec fields.

spec:
  redis:
    enabled: true
    architecture: replication
    replicaCount: 3
  database:
    managed: true
    type: postgresql
    postgres:
      proxy:
        enabled: true
spec:
  redis:
    enabled: true
    architecture: replication
  database:
    managed: true
    type: postgresql
    postgres:
      proxy:
        enabled: true
spec:
  defaults:
    redis:
      architecture: replication
      replicaCount: 3
    database:
      postgres:
        proxy:
          enabled: true
spec:
  template:
    spec:
      redis:
        enabled: true
        architecture: replication
      database:
        managed: true
        type: postgresql
        postgres:
          proxy:
            enabled: true

Transition runbook

HA Redis (replication) to single Redis (standalone)

This is safe. Redis holds only transient Nextcloud cache and distributed file locks. A brief lock-release (Redis restart) is acceptable.

  1. Update the spec:

    redis:
      enabled: true
      architecture: standalone
    
  2. The operator renders redis.architecture: standalone into the HelmRelease on the next reconcile. The Helm upgrade removes the redis-replicas StatefulSet.

  3. The redis-master-0 pod restarts; the Nextcloud application reconnects to {release}-redis-master (unchanged hostname).
  4. Orphaned PVCs: the bitnami Redis chart does not garbage-collect StatefulSet PVCs on scale-down. The redis-data-redis-replicas-* PVCs remain after the transition and must be deleted manually if storage reclamation is needed:

    kubectl get pvc -n <namespace> | grep redis-replicas
    kubectl delete pvc redis-data-<release>-redis-replicas-0 \
                       redis-data-<release>-redis-replicas-1 \
                       redis-data-<release>-redis-replicas-2 \
                       -n <namespace>
    

    These PVCs carry no persistent application data (Redis is used as a transient cache/lock store). Deleting them is safe once the StatefulSet is gone.

pgBouncer: enable on an existing instance (false → true)

  1. Update the spec:

    database:
      postgres:
        proxy:
          enabled: true
    
  2. The operator immediately adds the pgBouncer block to the live PerconaPGCluster. PGO starts provisioning pgBouncer pods.

  3. The operator gates the DB host flip on pgBouncer readiness. The DB Secret and HelmRelease remain pointed at <cluster>-primary until the bouncer is Ready. Nextcloud continues to run correctly on the primary during this window.
  4. Once pgBouncer pods are Ready, the 30-second status timer detects the host drift and automatically re-points the HelmRelease (externalDatabase.host) to <cluster>-pgbouncer. Flux re-renders and rolls Nextcloud onto the bouncer — typically within ~30 seconds of bouncer readiness. No manual step is required.

    To force the flip immediately rather than waiting for the next timer tick:

    kubectl annotate nextcloudinstance <name> k8s.bnerd.com/reconcile=$(date +%s) --overwrite
    
  5. Monitor the HelmRelease and Nextcloud pod readiness:

    kubectl get helmrelease <name>-nextcloud -n <namespace>
    kubectl rollout status deployment <name>-nextcloud -n <namespace>
    # Confirm the host flipped
    kubectl get secret <name>-nextcloud-db -n <namespace> \
      -o jsonpath='{.data.db-host}' | base64 -d && echo
    

Self-driving since v0.19.1

The off→on host flip is fully automatic as of v0.19.1. The 30-second status timer is drift-gated: in steady state it does no writes and causes no HelmRelease churn; only the transition tick (when the bouncer just became Ready and the current host differs from the target host) triggers the surgical patch. The k8s.bnerd.com/reconcile annotation is an optional fallback to force the flip immediately.

pgBouncer: disable on an existing instance (true → false)

This is fully automatic. No manual step required.

  1. Update the spec:

    database:
      postgres:
        proxy:
          enabled: false
    
  2. On the next reconcile (within 30 seconds, or immediately via the annotation below) the operator removes the pgBouncer block from the live PerconaPGCluster and rewrites the DB Secret to point at <cluster>-primary.

  3. The Helm upgrade rolls Nextcloud to the primary endpoint. PGO deprovisions the pgBouncer pods once the cluster spec is updated — idle pgBouncer CPU/memory is reclaimed immediately.

pgBouncer: new instances

New instances with proxy.enabled: true (including those using the production profile) are handled entirely at create time. The wait-for-DB initContainer gates pod start until the primary is accepting connections; no manual step is needed.

Force immediate reconcile

After changing redis.architecture or database.postgres.proxy.enabled, you can trigger an immediate reconcile rather than waiting for the 30-second timer:

kubectl annotate nextcloudinstance <name> k8s.bnerd.com/reconcile=$(date +%s) --overwrite

For the pgBouncer off→on case this is optional: the status timer completes the host flip automatically once the bouncer is Ready (see above).


Connection-pool sizing note

When pgBouncer is disabled, Nextcloud's PHP worker pool connects directly to the PostgreSQL primary. Each PHP worker holds one persistent connection. With the default poolMode: transaction pgBouncer multiplexes connections efficiently; without it, ensure PostgreSQL max_connections is sized for your PHP worker count (pm.maxChildren × pod replicas + headroom). A mismatch shows as FATAL: remaining connection slots are reserved in the Nextcloud log.


Troubleshooting

# Check Redis architecture currently deployed
kubectl get helmrelease <name>-nextcloud -n <namespace> \
  -o jsonpath='{.spec.values.redis}' | jq .

# Check Redis pods
kubectl get pods -n <namespace> -l app.kubernetes.io/name=redis

# Check pgBouncer pods (Percona PGO)
kubectl get pods -n <namespace> \
  -l postgres-operator.crunchydata.com/role=pgbouncer

# Check the DB Secret host to confirm proxy toggle took effect
kubectl get secret <name>-nextcloud-db -n <namespace> \
  -o jsonpath='{.data.db-host}' | base64 -d && echo

# Force reconcile
kubectl annotate nci <name> k8s.bnerd.com/reconcile=$(date +%s) --overwrite

See the Operations & Annotations guide for the full list of operational annotations.