Skip to content

PHP Tuning and Preview Generation

The spec.php block on every Nextcloud-flavored CRD (Nextcloud, NextcloudInstance, NextcloudPool, NextcloudProfile) configures PHP, FPM/Apache worker pools, and preview-generation limits. Sane defaults shipped in every built-in profile prevent the most common "image upload blocks the whole instance" symptom out of the box.

The problem this feature solves

When a user uploads a batch of images, Nextcloud's web/mobile client immediately requests thumbnails. Each thumbnail request runs preview generation synchronously in the HTTP request path (PreviewController calls IPreview::getPreview() directly — no queue). With GD as the default backend, decoding a 24 MP JPEG into uncompressed pixels takes 100–400 MB per worker, and a single thumbnail can tie up that worker for several seconds.

With upstream-default settings (no preview size cap, no concurrency cap, Apache MaxRequestWorkers=256 or FPM pm.max_children=5), a batch upload exhausts workers and the entire instance — login, file list, admin — appears down.

The load-bearing fix is preview limits, not raising worker counts. Bumping pm.max_children without RAM math = OOM kills mid-write = data loss.

Sane defaults shipped in every built-in profile

The production, testing, and development profiles all ship spec.php.preview with:

Field Value Why
maxX / maxY 2048 Half of the upstream NC default of 4096 — dramatically reduces decode RAM.
maxMemory 256 (MB) GD-only memory guard.
maxFilesizeImage 50 (MB) Images larger fall back to a mime icon — they were going to fail anyway.
concurrencyNew 2 Caps concurrent NEW preview generation (NC default is dynamic — max(CPUs, 4)).
concurrencyAll 4 Must be > concurrencyNew (validated).

Profile-specific php.ini settings:

Field production testing development
memoryLimit 512M 256M 128M
uploadMaxFilesize 16G 2G 512M
maxExecutionTime 3600s 1800s 300s
outputBuffering false false false
opcache.enable true true false

Worker-pool tuning (fpm.*, apache.*) is opt-in — defaults apply only to preview limits and php.ini. Bumping worker counts requires deliberate RAM math.

Choosing values

Preview limits

  • Most users: keep the profile defaults.
  • Photography/RAW workflows: consider maxX=2560, maxY=1600 if users browse on large screens.
  • Low-resource pods: drop to maxX=1024, maxY=1024 and concurrencyNew=1.
  • Override the provider list (preview.providers) only if you know what you're doing — disabling the default set may break expected functionality.

PHP-FPM workers (FPM image flavor)

php.fpm.maxChildren × php.memoryLimit should be ≤ 0.8 × resources.limits.memory. The operator surfaces a PhpConfigRamBudgetExceeded warning condition if this ratio is exceeded. Example:

  • Pod limit: 4 GiB
  • Per-worker memory: 256 MiB
  • Safe maxChildren: ~12 (12 × 256 = 3 GiB ≤ 0.8 × 4 GiB)

Use pm = dynamic with maxRequests = 500 to recycle workers and flush PHP memory leaks.

Apache MPM workers (Apache image flavor — default)

The upstream chart and Docker image expose no native hook for tuning Apache MaxRequestWorkers. The operator implements this via a per-instance ConfigMap mounted into /etc/apache2/conf-enabled/zzz-bnerd-mpm.conf. The user-facing API is symmetric to FPM:

spec:
  php:
    runtime: apache  # default
    apache:
      maxRequestWorkers: 64
      maxConnectionsPerChild: 1000

ServerLimit is automatically emitted equal to maxRequestWorkers (Apache requires ServerLimit ≥ MaxRequestWorkers).

Switching to FPM

The Apache image and the FPM image are separate flavors of the upstream chart. The operator's spec.php.runtime does NOT auto-switch — keep these aligned:

spec:
  php:
    runtime: fpm
    fpm:
      maxChildren: 16
      maxRequests: 500
  helm:
    values:
      image:
        flavor: fpm
      nginx:
        enabled: true   # required when image.flavor=fpm

The operator emits a PhpRuntimeMismatch warning condition if these don't agree.

Cascade

spec.php cascades through the four-CRD chain (last wins):

  1. Built-in profile defaults
  2. Custom NextcloudProfile CRD (spec.defaults.php)
  3. NextcloudPool template (spec.template.spec.php)
  4. Nextcloud / NextcloudInstance spec (spec.php)
  5. spec.helm.values (final escape hatch — operator-generated entries collide with user-supplied at this layer)

Override individual fields without copying the whole block — the merge is recursive.

How the operator translates it

The chart has a quirk: nextcloud.phpConfigs mounts to /usr/local/etc/php/conf.d/ when nginx.enabled: false (Apache flavor) but to /usr/local/etc/php-fpm.d/ when nginx.enabled: true (FPM flavor). A single config map serves both, but php.ini files are valid only in the first path; FPM pool config (pm.max_children) is valid only in the second.

The operator branches on spec.php.runtime:

Runtime Output
apache phpConfigs[zz-99-bnerd-php.ini] (php.ini directives, lands in /conf.d/) + Apache MPM ConfigMap via extraVolumes/extraVolumeMounts.
fpm phpConfigs[zz-99-bnerd-pool.conf] (FPM [www] block + php_admin_value[] entries for php.ini directives — since phpConfigs no longer reaches /conf.d/ in FPM mode).

Env-var-supported settings (PHP_MEMORY_LIMIT, PHP_UPLOAD_LIMIT, PHP_OPCACHE_MEMORY_CONSUMPTION) go via nextcloud.extraEnv regardless of flavor — cleaner than ini fragments. Preview config goes via nextcloud.configs (config.php fragment), which mounts to /var/www/html/config/ on both flavors.

Automatic pod restart on change

The upstream chart auto-hashes phpConfigs, configs, and hooks content into pod-template annotations, so changes to those values automatically roll the deployment.

For Apache MPM tuning, the chart cannot see ConfigMap content (only the volume reference), so the operator emits a bnerd.com/apache-mpm-checksum annotation that changes whenever the rendered MPM config changes.

Verify a value change rolls the deployment:

kubectl rollout status deploy/$INSTANCE-nextcloud

Validation surface

The operator surfaces validation findings as status conditions on the NextcloudInstance:

Condition Severity Trigger
PreviewConcurrencyInvalid error concurrencyAll ≤ concurrencyNew (admission rejected)
PhpConfigRamBudgetExceeded warning maxChildren × memoryLimit > 0.8 × resources.limits.memory
PhpRuntimeMismatch warning runtime doesn't agree with helm.values.nginx.enabled / image.flavor
PhpPostMaxSizeIgnored warning postMaxSize differs from uploadMaxFilesize (image coerces them equal)
MultiReplicaWithoutRedisLocking warning replicas > 1 without redis.enabled: true

Warnings don't block reconciliation — they're surfaced for the admin to act on.

Examples

See: - examples/php-tuned-instance.yaml — Apache mode with MPM tuning - examples/php-tuned-fpm-instance.yaml — FPM mode with pm.max_children tuning - examples/custom-volumes.yaml — first-class spec.extraVolumes injection coexisting with operator-managed volumes - examples/production-nextcloud.yaml — full production setup using the structured spec.php block