Tutorial

How to Speed Up Nextcloud on a VPS

By Amith Kumar25 July 202614 min read
Self-hostingNextcloud
How to Speed Up Nextcloud on a VPS
Verified 25 Jul 2026 · Ubuntu 24.04 LTS · nginx 1.24.0, MariaDB 10.11.14, Redis 7.0.15

What you will have

This tutorial shows you how to speed up Nextcloud on a VPS by adding the pieces a fresh install leaves off: an APCu memory cache, Redis for file locking and the distributed cache, a tuned PHP OPcache and php-fpm pool, background jobs on a real system cron, and HTTP/2 on the wire.

It is written for the person running a working Nextcloud that feels heavy, where the admin overview is covered in yellow warnings and the interface stalls under any load. You will clear those warnings and measure the result so you know the gain is real.

Here is the destination, shown first, with the honest numbers from the instance we tuned while writing this. Every figure below was measured on our lab box, a single small 2 vCPU slice, not estimated.

# one Nextcloud login page, before and after, on the same slice
ab -n 200 -c 10 -k https://your-domain/login
The honest before and after on one slice: ab -n 200 -c 10 -k against the same Nextcloud login page reporting 29.79 requests per second before tuning and 41.90 after, with mean response time falling from 336ms to 239ms, roughly 40 percent more page throughput plus every recommended setup check going green

On that box the same login page went from 29.79 requests per second to 41.90 under identical load, with time-to-first-byte dropping from about 68 milliseconds to about 52, and every recommended setup check moving from warning to green. That page throughput gain is real, and it is honest to say up front that it is a solid improvement rather than a tenfold one. On Nextcloud the bigger wins are structural: the database stops carrying file locks, the cache stops going to disk, and the housekeeping jobs actually run.

What this tutorial covers
  • Baselining the un-tuned instance so the before and after numbers are honest.
  • Tuning the PHP OPcache and sizing the php-fpm pool for the box.
  • Adding an APCu local cache and Redis for transactional file locking plus the distributed cache.
  • Moving background jobs from unreliable AJAX to a real system cron.
  • Turning on HTTP/2 and gzip, then measuring again and keeping it healthy.

Every layer here does the same kind of thing: it takes work Nextcloud was repeating on each request, or offloading to the wrong place, and moves it somewhere faster. APCu holds the app list and config in memory, Redis holds the file locks and the distributed cache, OPcache keeps the compiled PHP resident, and HTTP/2 stops the browser queuing on Nextcloud's many small assets.

Before you begin

This tutorial tunes an instance that already runs, so it assumes you have one to work on.

Before you start
  • A working Nextcloud on an Ubuntu 24.04 slice, served by nginx and PHP-FPM over HTTPS, following how to install Nextcloud on a VPS. This tutorial builds directly on that stack (nginx, PHP 8.3-FPM, MariaDB) and the same occ command line.
  • SSH access to the slice as a normal user cleared for sudo, and the habit of running occ as the www-data web user.
  • About an hour, plus the patience to load-test the instance at the start and the finish so the gain is measured rather than assumed.

Every command here is run over SSH as an ordinary sudo user on the slice. Where a step needs root it calls sudo, and where it touches Nextcloud's own files it runs as www-data, the same conventions as the install tutorial.

How to speed up Nextcloud on a VPS

The whole job is five steps, and it helps to see them before you start. You will speed up Nextcloud on a VPS by measuring first, then adding the caches and cron a fresh install skips:

  1. Baseline the instance so every later number has something to beat.
  2. Tune the PHP OPcache and size the php-fpm pool for the box.
  3. Add an APCu local cache and wire Redis in for file locking and the distributed cache.
  4. Move background jobs off AJAX onto a real system cron.
  5. Turn on HTTP/2 and gzip, then measure again.

Work in that order. The caches give the biggest structural change, and measuring at both ends is the only way to know the tuning did what you think it did.

Baseline the un-tuned instance

You cannot claim a speedup you did not measure, so measure the slow instance before you touch it. A fresh Nextcloud tells you plainly what is missing: run the built-in checks and you will see no memory cache, database file locking, and a background cron that has never run.

# measure BEFORE you tune anything
sudo -u www-data php occ config:system:get memcache.local
sudo -u www-data php occ setupchecks | grep -iE 'memcache|locking|cron'
ab -n 200 -c 10 -k https://your-domain/login
curl -sk -w 'ttfb=%{time_starttransfer}s\n' -o /dev/null https://your-domain/login
Baselining the un-tuned instance: occ reporting no memory cache configured, the database used for transactional file locking and a background cron that ran 56 years ago, then ab showing 29.79 requests per second and curl a time-to-first-byte of about 68ms, the honest starting numbers

On our lab box the checks reported no memory cache configured, the database in use for transactional file locking, and a last background job that "ran 56 years ago" because AJAX cron only fires when someone loads a page. The login page managed 29.79 requests per second under ten concurrent readers, with a time-to-first-byte of about 68 milliseconds. That is the starting line, and those three warnings are the honest reason the instance feels heavy.

Checkpoint Write down your baseline: the requests per second from ab, the TTFB from curl, and which checks are yellow in Settings then Administration then Overview. You will compare against these exact numbers at the end.

Tune the PHP OPcache and pool

Every Nextcloud request executes PHP, and OPcache keeps that code compiled in shared memory so the work happens once. Ubuntu ships OPcache enabled, but its default ceiling of 10,000 files is the catch: Nextcloud ships more PHP files than that, so under real use OPcache starts evicting and recompiling, which is the slow path. Raise the ceilings, and raise PHP's memory limit to the 512 MB Nextcloud recommends, in one small drop-in.

; /etc/php/8.3/fpm/conf.d/99-nextcloud.ini
memory_limit = 512M
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 16000
opcache.revalidate_freq = 60
Tuning PHP and OPcache: Nextcloud ships 12,557 PHP files, above the 10,000 default OPcache ceiling, so the drop-in raises memory_limit to 512M and OPcache to a 256MB pool with a 16,000-file ceiling, after which the live OPcache reads 256MB with a 98.26 percent hit rate and no evictions

The 16000 file ceiling clears the 12,557 files Nextcloud ships with headroom to spare, and revalidate_freq=60 tells OPcache to check for changed files only once a minute instead of on every request. After a restart and a little traffic, our OPcache held its scripts in a 256 MB pool at a 98 percent hit rate, with no evictions.

sudo systemctl restart php8.3-fpm
curl -sk https://your-domain/ocp.php   # a small status probe, deleted after

The other half of the runtime is the php-fpm pool. The default pool allows only five worker processes, so the sixth concurrent request waits in line. Size it to the box: a real Nextcloud worker uses roughly 68 MB, so on a 2 GB slice a dozen workers fits with room for MariaDB and Redis.

sudo sed -i 's/^pm.max_children = 5/pm.max_children = 12/' \
     /etc/php/8.3/fpm/pool.d/www.conf
grep -E '^pm(\.|)' /etc/php/8.3/fpm/pool.d/www.conf
Sizing the php-fpm pool for the box: a real Nextcloud worker uses about 68MB, so the default five max_children is raised to twelve, which fits a 2GB slice with headroom for MariaDB and Redis so concurrent requests are not queued behind five workers
Checkpoint After the restart, the OPcache probe should show your raised memory and file ceilings and a hit rate above 95 percent, and pm.max_children should read 12. Do not set it so high that twelve workers times their memory could exhaust the slice.

Add APCu and Redis

OPcache handles code, but Nextcloud also asks the same questions on every request: the app list, the config, mimetype maps, user lookups. Without a memory cache those go to disk or the database each time. APCu answers them from local memory, and Redis does two more jobs a single-server APCu cannot: it holds the transactional file locks, and it serves as the distributed cache. Install all three first.

sudo apt install -y redis-server php-apcu php-redis
redis-cli ping
php -m | grep -iE 'apcu|redis'
ss -tlnp | grep 6379
Installing the caches: apt installing redis-server 7.0.15 plus php-apcu and php-redis, redis-cli ping returning PONG, php -m confirming the apcu and redis extensions are loaded, and ss showing Redis listening on 127.0.0.1:6379 on localhost only

A PONG and a Redis on 127.0.0.1:6379 means it is running and reachable on localhost only, which is where it should stay. Now tell Nextcloud to use them, with occ. APCu becomes the local cache, and Redis becomes both the locking store and the distributed cache.

cd /var/www/nextcloud
sudo -u www-data php occ config:system:set memcache.local --value='\OC\Memcache\APCu'
sudo -u www-data php occ config:system:set memcache.locking --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis'
sudo -u www-data php occ config:system:set redis host --value=127.0.0.1
sudo -u www-data php occ config:system:set redis port --value=6379 --type=integer
Wiring the caches into Nextcloud with occ: memcache.local set to APCu for the local cache, and both memcache.locking and memcache.distributed set to Redis with the host and port, so file locking and the distributed cache move off the database into memory
The Nextcloud System info page at cloud.boxlab.space after wiring the caches, showing the tuned runtime: PHP 8.3.6 with a 512 MB memory limit and a 60 second OPcache revalidate frequency, the apcu, redis and other extensions loaded, and MariaDB 10.11.14, framed in the ServerCake browser chrome over real HTTPS

Now prove Redis is genuinely doing the work, because a misconfigured cache fails quietly back to the database. Watch it live while a file operation takes a lock, then read the hit counter and the checks.

redis-cli monitor   # while a WebDAV upload takes a lock
redis-cli info stats | grep keyspace_hits
sudo -u www-data php occ setupchecks | grep -iE 'locking|memcache'
Proving Redis is really used: redis-cli monitor streaming real GET and SET commands against Nextcloud cache keys with a transactional file-lock key set during a WebDAV upload, keyspace_hits climbing into the thousands, and occ setupchecks showing Transactional File Locking and Memcache both green

On our box redis-cli monitor streamed real GET and SET traffic against Nextcloud's cache keys, the file-lock keys appeared during a WebDAV upload, keyspace_hits climbed into the thousands, and the two setup checks that were yellow, the memory cache and the transactional file locking, both turned green. That locking move matters most under concurrent writes: file locks that were contending in the database now live in memory.

Checkpoint In the admin overview the "Memcache" and "Transactional File Locking" checks should now be green, and redis-cli info keyspace should show a growing number of keys. If the checks stay yellow, re-check that php-redis is loaded and that Redis answered PONG.

Move background jobs to a system cron

By default Nextcloud runs its housekeeping through the browser, firing one job whenever someone loads a page. That is the AJAX cron, and it is why a quiet instance shows a background job that has not run in years. Switch it to a real system cron so the jobs run on a schedule whether anyone is looking or not. Register a systemd timer that runs cron.php as www-data every five minutes, then tell Nextcloud to expect it.

sudo -u www-data php occ background:cron
sudo systemctl enable --now nextcloudcron.timer
systemctl list-timers nextcloudcron.timer --no-pager | head -2
sudo -u www-data php occ setupchecks | grep -i 'cron last run'
Moving background jobs to a real system cron: occ setting the mode to cron, a systemd timer enabled to run cron.php every five minutes shown by systemctl list-timers, and the Cron last run setup check reporting the last execution ran seconds ago instead of decades
The Nextcloud Basic settings page at cloud.boxlab.space with Background jobs set to Cron (Recommended), the green banner reading Last job ran 3 minutes ago, proof the systemd system cron is firing on schedule rather than the unreliable in-browser AJAX cron

The timer entry confirms the next run is minutes away, and once it has fired, the "Cron last run" check reports the last execution as seconds ago rather than decades. That single change is what keeps previews, notifications and cleanup jobs moving without leaning on visitor traffic to trigger them.

Do not skip this The memory cache and the background cron are the two settings most responsible for a self-hosted Nextcloud feeling slow and stale. Both are three commands, and both show up immediately in the admin overview as a check going green.

Turn on HTTP/2 and gzip

The caches speed up the work the server does. HTTP/2 and gzip speed up getting the result to the browser. Nextcloud loads a lot of small JavaScript and CSS files, and on HTTP/1.1 the browser queues them a few at a time; HTTP/2 multiplexes them over one connection. Enable it on the TLS listener and add gzip for text responses, then reload.

# in your server block: add http2 to the listen line
listen 443 ssl http2;
gzip on;
gzip_types application/javascript text/javascript application/json text/css image/svg+xml;
sudo nginx -t && sudo systemctl reload nginx
curl -sk --http2 -o /dev/null -w 'version=%{http_version}\n' https://your-domain/login
curl -sk -H 'Accept-Encoding: gzip' -o /dev/null -w 'raw=%{size_download}\n' https://your-domain/login
Turning on HTTP/2 and gzip: nginx -t reporting the config test successful, curl --http2 confirming the connection negotiates HTTP version 2 over the real certificate, and gzip shrinking the login HTML from 17,734 to 6,560 bytes on the wire, about 63 percent smaller

A version=2 back from curl confirms HTTP/2 is negotiated over the real certificate. With gzip on, the login page's HTML dropped from 17,734 bytes to 6,560 on the wire, about 63 percent smaller, and every text asset gets the same treatment. Nextcloud's JavaScript bundles are already minified, so the transfer win is real but modest; the multiplexing is what your browser feels most.

Measure the payoff

Now run the exact baseline command again, on the same slice, and read the result.

# the same load test, now tuned
ab -n 200 -c 10 -k https://your-domain/login
curl -sk -w 'ttfb=%{time_starttransfer}s\n' -o /dev/null https://your-domain/login
sudo -u www-data php occ setupchecks | grep -c '✓'
Measuring the payoff: the same ab -n 200 -c 10 -k load test now serving 41.90 requests per second with a 239ms mean and a 306ms 95th percentile, a time-to-first-byte of about 52ms down from 68ms, and occ setupchecks reporting 60 green checks with no errors and no warnings
The genuine Nextcloud 34.0.2 admin Overview at cloud.boxlab.space after tuning, the Security and setup warnings section with the memory cache, file locking, cron and PHP getenv warnings all cleared, only optional informational notes remaining, and the version panel showing 34.0.2 up to date, the payoff over real HTTPS

The login page that managed 29.79 requests per second at baseline served 41.90 after, with the mean response time falling from 336 to 239 milliseconds and time-to-first-byte from about 68 to about 52. The bigger change is the last line: setupchecks reported 60 green checks with no errors and no warnings, where before it flagged the cache, the locking and the cron. The admin overview reflects the same clean result.

Be clear-eyed about what moved and why. The page throughput gain, roughly 40 percent, comes from APCu and OPcache sparing PHP repeated work, plus the wider pool. A single WebDAV request stays about the same, because on two vCPUs one request is limited by the processor, not the cache. What the Redis locking buys you is protection under concurrency: the database is no longer the thing that seizes up when several people sync at once. On Nextcloud that structural safety is worth more than a headline number.

Keep it fast and healthy

Two habits keep the setup honest. First, watch that Redis stays healthy but do not let it evict. This is the one place Nextcloud differs sharply from a web page cache: your Redis holds file locks, so an eviction policy that drops keys under memory pressure could drop a lock and corrupt a sync. Leave the policy at noeviction, the default, and simply give Redis enough memory.

redis-cli info memory | grep used_memory_human
redis-cli config get maxmemory-policy
sudo -u www-data php occ status | grep -E 'version|maintenance'
Keeping it healthy: redis-cli showing just over a megabyte used and the maxmemory-policy at noeviction, which Nextcloud needs because file locks live in this Redis and must never be evicted, plus occ status confirming version 34.0.2 with maintenance mode off

Our Redis used just over a megabyte for a personal instance, sat at the noeviction policy, and occ status confirmed the version with maintenance mode off. Second, keep the instance current: occ status and occ app:update --all are the same tools the install tutorial introduced, and a tuned instance is exactly the kind you want to keep patched.

Know too when tuning is not the answer. If the interface is still slow after all of this, and the uncached, per-request work is the bottleneck even with OPcache and Redis in place, that is the signal to give the slice more CPU. On a VPS a bigger box is a resize you click, not a migration you dread, which is the quiet payoff of owning the whole stack. If you also run a cache-heavy site alongside it, our Redis guide covers the same store in more depth.

Frequently asked questions

How much can I really speed up Nextcloud on a VPS with caching? On our small 2 vCPU slice the login page went from 29.79 to 41.90 requests per second, roughly 40 percent, with time-to-first-byte falling from about 68 to about 52 milliseconds. That is honest for Nextcloud: unlike a static page cache, these layers mostly spare PHP repeated work and protect the database rather than skipping it entirely. The larger, harder-to-benchmark wins are the file locking moving off the database and the background jobs finally running, both of which keep the instance stable under real concurrent use.
Do I need both APCu and Redis, or is one enough? Both, and they do different jobs. APCu is a local, in-process memory cache, ideal for the config, app list and small lookups Nextcloud repeats on every request, and it is faster than a network call for that. Redis handles the two jobs a single-server local cache cannot: transactional file locking, which must be shared and consistent, and the distributed cache. Setting memcache.local to APCu and both memcache.locking and memcache.distributed to Redis is the standard, recommended combination.
Why must I not set a Redis eviction policy for Nextcloud? Because this Redis holds your file locks, not just cache entries. An eviction policy such as allkeys-lru tells Redis to delete keys when it runs low on memory, and if it deletes a lock, two processes could write the same file at once and corrupt a sync. This is the opposite of the advice for a pure web page cache, where eviction is fine. Leave Nextcloud's Redis at the default noeviction and give it enough memory, which for a personal instance is very little.
My WebDAV and file sync still feel slow after tuning. Why? A single file operation is usually limited by the CPU and the database query behind it, not by the caches, so on a small slice one request will not get dramatically faster from caching alone. What the tuning changes is behaviour under load: with Redis locking and a wider php-fpm pool, many simultaneous syncs no longer contend on the database. If single operations are still slow on an idle box, the fix is more CPU or a faster disk for the data directory, which on a VPS is a resize rather than a rebuild.

Running Nextcloud on your own slice means the performance is yours to tune, layer by layer, with no per-seat pricing and no black box deciding what gets cached. When you are ready to give a heavy instance room to breathe, a ServerCake slice gives you a clean Ubuntu 24.04 box in India, billed in rupees with GST, ready for exactly the caching, locking and cron stack above.


Give a heavy instance room to breathe

Run a fast Nextcloud on a slice

Caching, locking and cron only help when you control the whole stack. Spin up a ServerCake slice, a clean Ubuntu 24.04 box in India billed in rupees with GST, and run Nextcloud with the APCu and Redis caching, PHP tuning and system cron from this tutorial, proven end to end.

Get early access

This guide is provided for educational purposes and offered as is, without warranty of any kind. The commands change the configuration of a server you control, and some can lock you out if run out of order. Test on a non-production machine, keep a second session open, and take a snapshot or backup before you begin. You are responsible for changes made to your own systems; ServerCake accepts no liability for any loss, downtime, lockout, or damage arising from following this guide or running the accompanying script.

Spotted something out of date or have a question?

Built by the ServerCake team

Cloud that speaks India.

ServerCake is India's KYC-verified cloud: VMs and managed databases priced in ₹, billed with GST, on India-resident infrastructure. Reserve your spot for early access.

Reserve your spot