A single Nextcloud server can serve a surprising number of users, right up to the moment it cannot. For the DevOps engineer at a university we will call Aldergate, that moment was the first Monday of the autumn term, when 12,000 students opened laptops within the same hour and the one application server saturated while the database waited on file locks. This post is the engineer's rebuild of Nextcloud as a highly available cluster on MassiveGRID PaaS, built on Virtuozzo Application Platform (formerly Jelastic), layer by layer, with the reasoning for each choice.

What breaks when Nextcloud is scaled naively

Nextcloud is a PHP application with three kinds of state: the database, the files, and the locks and caches that coordinate concurrent access. Put two application servers behind a balancer without handling all three and you get exactly the failures the Nextcloud documentation warns about: uploads visible on one server and not the other, file-locking errors when two clients touch the same document through different servers, and sessions that log users out when the balancer moves them.

So the cluster design is really four decisions: how application servers share files, where locks and caches live, how the database survives a node loss, and how traffic is spread. The platform has a tool for each.

Layer one: the application servers and the balancer

The engineer started from the existing Nextcloud package environment and, in the topology wizard, increased the NGINX PHP layer from one node to two. The platform added an NGINX load balancer automatically, because scaling a certified application server without a balancer always does, and placed the second node on a different physical host under its anti-affinity rules.

The layer runs in stateful scaling mode, the platform's default for application servers, so the second node was a copy of the first including Nextcloud's config.php, the installed apps and the PHP settings. That is what makes horizontal auto-scaling safe later: a node added by a trigger at 09:00 on the first day of term is a faithful clone, not a bare image.

On the balancer the engineer enabled session affinity, so a user's requests stick to one backend for the life of a session, and installed the Let's Encrypt add-on on the balancer layer, where TLS terminates. The certificate is propagated to any balancer nodes added later.

Layer two: files that every node can see

Nextcloud's data directory has to be identical on every application server, immediately. The engineer considered the platform's File Synchronization add-on, which mirrors a directory between nodes with lsyncd within seconds, and rejected it for this case: a university's uploads are constant and large, and a sync delay of even a few seconds produces the "file not found" errors students would report. Instead the data directory was moved to a GlusterFS replicated volume: three storage nodes, each holding a full copy of every file, mounted on each application server with the Gluster native client. A write is acknowledged only when all bricks have it, so both application servers see the same file at the same moment, and losing a storage node loses nothing.

For smaller deployments a single shared storage container mounted over NFS is simpler and cheaper, and the platform supports that too. Aldergate chose GlusterFS because the storage itself had to be highly available, not just shared.

Layer three: Redis for locking and caching

Nextcloud recommends Redis for transactional file locking and for the distributed memory cache in any multi-server installation. A single Redis node in the environment, reachable over the internal network, is the minimum. The engineer went one step further and deployed the Redis Cluster package, three primaries with a replica each, so a Redis failure does not suddenly make every file lock fail across the university. Nextcloud's config.php points memcache.locking and memcache.distributed at the cluster, and PHP sessions are stored there as well, which means session affinity on the balancer becomes a performance preference rather than a correctness requirement.

Layer four: a database that survives a node

The single MariaDB node became a cluster using the MySQL/MariaDB/Percona Cluster package in Galera mode: three MariaDB nodes with synchronous replication, plus the two ProxySQL nodes the package places in front as the entry point. Nextcloud connects to the ProxySQL hostname, proxy.<environment>.<platform domain>, and never learns which node it is talking to. If a database node fails, ProxySQL stops routing to it within seconds and Nextcloud sees, at most, a retried query.

Galera has a rule that matters for Nextcloud: every table must have a primary key and use InnoDB. Nextcloud's schema meets both, which is why Galera is a supported and common choice for it. The engineer also attached the Database Cluster Recovery add-on, so if the cluster ever loses quorum after a network event, recovery is a dashboard action rather than a manual bootstrap at 07:00 on a Monday.

The result, and the first Monday of term

LayerBeforeAfterSurvives
Entry pointSingle PHP node on the shared load balancerNGINX balancer with Let's Encrypt, public IPBalancer restart via sequential redeploy
Application1 NGINX PHP node2 to 6 stateful nodes, trigger: add at 60% CPU for 5 min, remove at 25% for 15 minLoss of any node; auto-scaling for load
FilesLocal diskGlusterFS replicated volume, 3 bricksLoss of one storage node
Locks and cacheAPCu onlyRedis Cluster, 6 nodesLoss of any Redis node
Database1 MariaDB nodeMariaDB Galera, 3 nodes + 2 ProxySQLLoss of one database node without lost writes

On the first Monday of the following term the trigger added a third application server at 08:52, a fourth at 09:07 and a fifth at 09:20. Each grew vertically in cloudlets as PHP-FPM workers filled, since the platform re-tunes the worker count to the allocation. By 11:00 load had dropped and the layer scaled back to two nodes over the next hour. Nobody in the helpdesk queue mentioned Nextcloud, which is the only metric the engineer wanted. The additional hours of compute for the morning came to a few dollars at MassiveGRID's published rate of $0.003372 per cloudlet-hour, and the permanent cost of the redundant layers is what the university now pays for not having a single point of failure.

Frequently Asked Questions

Is GlusterFS necessary, or is shared storage enough?

A single shared storage container mounted over NFS gives every application server the same files and is the simpler, cheaper choice for most deployments. GlusterFS replicates the data across several storage nodes so the storage layer itself has no single point of failure, which is the right choice when the files must stay available through a storage node loss.

Why Galera rather than primary-secondary for Nextcloud?

Primary-secondary replication is asynchronous and has no automatic failover of the primary, so a primary failure is a manual promotion. Galera is synchronous multi-primary: ProxySQL simply stops routing to a failed node and no committed write is lost. Nextcloud's schema uses InnoDB with primary keys throughout, which Galera requires.

How do Nextcloud's background jobs run on a cluster?

Run the cron job on one application server only, the layer's master node, using the platform's cron file for that container. If auto-scaling removes nodes, the master is always the last to go, so the job keeps running.

Nextcloud with no single point of failure

Start from the Nextcloud package on MassiveGRID PaaS and add a load balancer, GlusterFS, a Redis cluster and a MariaDB Galera cluster from the same marketplace, all billed per cloudlet-hour under a 100% uptime SLA. Free 14-day trial, no credit card.

Nextcloud on PaaS

Further Reading