Nextcloud Slow Performance: Fix Guide
The Problem
Unlike most self-hosted apps where Docker Compose and default settings get you 90% of the way, Nextcloud out of the box is noticeably sluggish. Page loads take 2-5 seconds, file syncing feels slower than Dropbox, and the web interface becomes painful with more than a few users. This is not a bug — Nextcloud ships with conservative defaults that prioritize compatibility over speed.
The good news: every major performance bottleneck has a well-documented fix. This guide covers them in order of impact, starting with the changes that make the biggest difference.
Fix 1: Add Redis Caching (Biggest Impact)
The single most impactful change. Without Redis, Nextcloud uses the database for session locking, file locking, and temporary data — all things a database is bad at under concurrent load.
Add Redis to Docker Compose
services:
redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
Configure Nextcloud to Use Redis
Edit config.php inside the Nextcloud container (usually at /var/www/html/config/config.php):
'memcache.local' => '\\OC\\Memcache\\APCu',
'memcache.distributed' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' => [
'host' => 'redis',
'port' => 6379,
'timeout' => 0.0,
],
Or set via environment variables in the official Nextcloud Docker image:
services:
nextcloud:
image: nextcloud:33.0.0-apache
environment:
- REDIS_HOST=redis
- REDIS_HOST_PORT=6379
What Each Cache Layer Does
| Cache Type | Setting | Purpose | Recommended |
|---|---|---|---|
| Local cache | memcache.local | Per-server object cache | APCu |
| Distributed cache | memcache.distributed | Multi-server shared cache | Redis |
| File locking | memcache.locking | Transactional file locks | Redis (critical) |
Why memcache.locking matters most: Without Redis locking, every file operation acquires a database lock. With 5+ concurrent sync clients, the database becomes the bottleneck and file operations queue behind each other. Redis handles locks in-memory with microsecond latency.
Fix 2: Enable OPcache (Required)
PHP re-compiles every script on every request unless OPcache is enabled. This adds 50-200ms to every page load for no reason.
Check Current OPcache Status
docker exec nextcloud php -i | grep opcache.enable
If it returns opcache.enable => Off, OPcache is disabled.
Enable OPcache
Create or edit a PHP configuration file. In the official Nextcloud Docker image, mount a custom ini file:
volumes:
- ./php-custom.ini:/usr/local/etc/php/conf.d/zzz-custom.ini
Contents of php-custom.ini:
; OPcache — required for acceptable performance
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.save_comments = 1
opcache.revalidate_freq = 60
; APCu for local caching
apc.enabled = 1
apc.shm_size = 256M
apc.entries_hint = 8192
; Memory limit
memory_limit = 1G
Key settings explained:
| Setting | Value | Why |
|---|---|---|
opcache.memory_consumption | 128 | MB of memory for cached PHP scripts. 128 covers Nextcloud + most apps. |
opcache.max_accelerated_files | 10000 | Number of PHP files to cache. Nextcloud has thousands of files. |
opcache.revalidate_freq | 60 | Seconds before checking if a cached file changed. 60 is safe for production. |
opcache.save_comments | 1 | Required by Nextcloud. Do not disable. |
apc.shm_size | 256M | APCu shared memory. Increase if you see APCu fragmentation warnings. |
Restart the container after changes:
docker restart nextcloud
Fix 3: Switch Background Jobs to Cron
Nextcloud’s default background job method is AJAX — it runs one background job per page visit. This is unreliable and slow. Switch to system cron.
Set Up Cron
Add to the host’s crontab:
crontab -e
Add this line:
*/5 * * * * docker exec -u www-data nextcloud php /var/www/html/cron.php
Tell Nextcloud to Use Cron
In the Nextcloud admin panel: Settings → Administration → Basic settings → Background jobs → Select Cron.
Or via occ:
docker exec -u www-data nextcloud php occ background:cron
Why This Matters
| Method | Reliability | Timing | Recommended |
|---|---|---|---|
| AJAX | Low — requires user visits | Unpredictable | No |
| Webcron | Medium — external dependency | Fixed interval | Small instances only |
| Cron | High — system-level | Every 5 minutes | Yes |
Background jobs handle file cleanup, notification delivery, activity logging, preview generation, and app updates. When these fall behind, the web interface slows down because on-demand work piles up.
Fix 4: Database Optimization
Add Missing Indexes
Nextcloud occasionally introduces new database indexes in updates but does not add them automatically. Missing indexes cause slow queries.
docker exec -u www-data nextcloud php occ db:add-missing-indices
Convert BigInt Columns
Older Nextcloud installations may have 32-bit integer columns that should be 64-bit:
docker exec -u www-data nextcloud php occ db:convert-filecache-bigint
This runs during maintenance mode and may take several minutes on large instances.
MySQL/MariaDB Tuning
If using MySQL or MariaDB, ensure these settings are in your database configuration:
[mysqld]
transaction_isolation = READ-COMMITTED
binlog_format = ROW
innodb_file_per_table = 1
innodb_buffer_pool_size = 256M
innodb_flush_log_at_trx_commit = 2
innodb_log_buffer_size = 32M
Critical: transaction_isolation = READ-COMMITTED is required for data integrity under concurrent sync clients. Nextcloud explicitly warns about this in the admin panel if it is not set.
Scale innodb_buffer_pool_size with your instance — 256M is a starting point, use 1G+ for large instances with 100+ users.
PostgreSQL Alternative
PostgreSQL generally performs better than MySQL for Nextcloud workloads due to better handling of concurrent writes. If starting fresh, consider PostgreSQL:
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: changeme_strong_password
volumes:
- nextcloud-db:/var/lib/postgresql/data
restart: unless-stopped
Fix 5: Pre-Generate Previews
By default, Nextcloud generates image thumbnails on demand — when you browse a folder, it creates previews for every image in real time. With hundreds of photos, this makes folder browsing unbearably slow.
Install the Preview Generator App
docker exec -u www-data nextcloud php occ app:install previewgenerator
Generate All Existing Previews
docker exec -u www-data nextcloud php occ preview:generate-all
This runs once and may take hours for large photo libraries. Run it during off-hours.
Add Ongoing Pre-Generation to Cron
*/10 * * * * docker exec -u www-data nextcloud php occ preview:pre-generate
This generates previews for newly uploaded files every 10 minutes, so they are ready before anyone browses to them.
Optimize Preview Settings
Add to config.php to reduce preview storage and generation time:
'enable_previews' => true,
'preview_max_x' => 2048,
'preview_max_y' => 2048,
'preview_max_scale_factor' => 1,
Setting preview_max_scale_factor to 1 prevents Nextcloud from generating upscaled previews, which wastes disk space and CPU time.
Fix 6: PHP-FPM Worker Tuning
If you are using the nextcloud:fpm Docker image (or nextcloud:fpm-alpine), the default PHP-FPM configuration is too conservative for most workloads.
Calculate Max Workers
Formula: (Available RAM for PHP) / (50 MB per worker)
| Server RAM | PHP allocation | Max workers |
|---|---|---|
| 2 GB | 1 GB | 20 |
| 4 GB | 2 GB | 40 |
| 8 GB | 4 GB | 80 |
| 16 GB | 8 GB | 120+ |
Configure PHP-FPM
Mount a custom FPM pool config:
volumes:
- ./fpm-custom.conf:/usr/local/etc/php-fpm.d/zzz-custom.conf
Contents of fpm-custom.conf (for a 4 GB server):
[www]
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 500
For high-traffic instances (100+ users), switch to static mode for predictable performance:
[www]
pm = static
pm.max_children = 80
pm.max_requests = 500
Fix 7: Set a Maintenance Window
Nextcloud defers resource-intensive maintenance tasks. Without a defined window, these tasks run at unpredictable times and compete with user requests.
Add to config.php:
'maintenance_window_start' => 1,
This tells Nextcloud to run heavy maintenance jobs starting at 1:00 AM UTC. Adjust to your off-peak hours.
Full Optimized Docker Compose
Here is a complete optimized Nextcloud stack incorporating all fixes:
services:
db:
image: postgres:17-alpine
container_name: nextcloud-db
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: changeme_strong_password
volumes:
- nextcloud-db:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: nextcloud-redis
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nextcloud:
image: nextcloud:33.0.0-apache
container_name: nextcloud
ports:
- "8080:80"
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD=changeme_strong_password
- REDIS_HOST=redis
- REDIS_HOST_PORT=6379
- NEXTCLOUD_TRUSTED_DOMAINS=nextcloud.example.com
volumes:
- nextcloud-data:/var/www/html
- ./php-custom.ini:/usr/local/etc/php/conf.d/zzz-custom.ini
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
nextcloud-db:
nextcloud-data:
With php-custom.ini:
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.save_comments = 1
opcache.revalidate_freq = 60
apc.enabled = 1
apc.shm_size = 256M
memory_limit = 1G
upload_max_filesize = 16G
post_max_size = 16G
max_execution_time = 3600
max_input_time = 3600
Checklist
Run through this after applying fixes to verify everything is working:
# 1. Check Redis is connected
docker exec -u www-data nextcloud php occ config:list system | grep memcache
# 2. Check OPcache is enabled
docker exec nextcloud php -i | grep "opcache.enable"
# 3. Check background jobs are set to cron
docker exec -u www-data nextcloud php occ background:cron
# 4. Add missing database indexes
docker exec -u www-data nextcloud php occ db:add-missing-indices
# 5. Run maintenance repair
docker exec -u www-data nextcloud php occ maintenance:repair
# 6. Check for warnings in admin panel
# Visit Settings → Administration → Overview
The Overview page in admin settings shows a health check with specific warnings. After applying these fixes, most performance-related warnings should disappear.
Prevention
- Always deploy Redis from the start — it is not optional for acceptable performance
- Set up cron before inviting users — AJAX background jobs cause slow degradation over time
- Run
occ db:add-missing-indicesafter every Nextcloud upgrade - Monitor PHP-FPM process count — if all workers are busy, increase
pm.max_children - Pre-generate previews before uploading large photo libraries
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments