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=1600if users browse on large screens. - Low-resource pods: drop to
maxX=1024, maxY=1024andconcurrencyNew=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:
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):
- Built-in profile defaults
- Custom NextcloudProfile CRD (
spec.defaults.php) - NextcloudPool template (
spec.template.spec.php) - Nextcloud / NextcloudInstance spec (
spec.php) 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:
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