This is the third time I've stood up Nextcloud at home, and the first time I've done it without cutting the corners that killed the previous two. The first attempt was the all-in-one snap, which worked until it didn't and gave me no obvious way to reason about it. The second was a single container with SQLite, which is fine right up until you have more than one user and a phone syncing photos, at which point it locks up under the slightest concurrency.
So this time, properly. Separate database, separate cache, a real reverse proxy, and the cron job that everyone skips and then wonders why their instance feels broken.
the shape of it
Four pieces, each doing one job:
- Postgres for the database, not SQLite, not MySQL out of habit.
- Redis for file locking and caching.
- Nextcloud itself in php-fpm.
- Caddy out front for TLS, because I am tired of writing nginx config and renewing certs by hand.
the bits that actually mattered
The Postgres move alone fixed most of what I'd been blaming on Nextcloud. The transactional file lock contention I'd seen with SQLite simply vanished. If you take one thing from this: do not run a multi-user Nextcloud on SQLite. It's there for testing, and that's all.
Redis was the second win. Nextcloud will use a database-backed lock by default, and it works, but it's slow and it puts load where you don't want it. Pointing it at Redis is a few lines in config.php:
'memcache.local' => '\\OC\\Memcache\\APCu',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' => [
'host' => '/run/redis/redis.sock',
'port' => 0,
],
The third thing, and the one I'd ignored twice, is the background cron job. Nextcloud defaults to "AJAX" cron, which only runs maintenance when someone has the web UI open. That means previews never generate, the trash never empties, and federation scans never run, and then you decide Nextcloud is bloated when really it just never got to do its housekeeping. A proper systemd timer hitting cron.php every five minutes:
[Unit]
Description=Nextcloud cron
[Service]
User=www-data
ExecStart=/usr/bin/php -f /var/www/nextcloud/cron.php
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
Then occ background:cron to tell it to expect that, and the admin warnings clear.
caddy out front
Caddy earns its place here. The entire TLS story is two lines, it gets a certificate, it renews it, and I never think about it again:
cloud.example.com {
reverse_proxy localhost:9000
}
I lost an evening to the usual reverse-proxy footguns, the trusted-proxies setting and the overwriteprotocol so Nextcloud stops generating http:// URLs behind TLS, but those are documented and findable once you know to look.
was it worth it
Honestly, yes. It's been a fortnight and it has not fallen over once, the Android client syncs photos without sulking, and the file picker is responsive in a way the SQLite build never was. The difference between "Nextcloud is slow and flaky" and "Nextcloud is fine" turned out to be entirely about giving it the supporting cast it expects rather than trying to run it as one lonely container. The documentation says all of this. I just had to fail twice before I read it.