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 TypeSettingPurposeRecommended
Local cachememcache.localPer-server object cacheAPCu
Distributed cachememcache.distributedMulti-server shared cacheRedis
File lockingmemcache.lockingTransactional file locksRedis (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:

SettingValueWhy
opcache.memory_consumption128MB of memory for cached PHP scripts. 128 covers Nextcloud + most apps.
opcache.max_accelerated_files10000Number of PHP files to cache. Nextcloud has thousands of files.
opcache.revalidate_freq60Seconds before checking if a cached file changed. 60 is safe for production.
opcache.save_comments1Required by Nextcloud. Do not disable.
apc.shm_size256MAPCu 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

MethodReliabilityTimingRecommended
AJAXLow — requires user visitsUnpredictableNo
WebcronMedium — external dependencyFixed intervalSmall instances only
CronHigh — system-levelEvery 5 minutesYes

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 RAMPHP allocationMax workers
2 GB1 GB20
4 GB2 GB40
8 GB4 GB80
16 GB8 GB120+

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-indices after 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

Comments