This is the third time I've stood up Nextcloud. The first two collapsed under their own weight: slow file scans, mysterious 504s, a database that I'd let limp along on SQLite because the quick-start guide let me. Each time I told myself I'd do it properly later. This is later.
The honest root cause of both previous failures was the same: I treated Nextcloud like a small app when it's actually a small ecosystem. It wants a real database, it wants Redis for locking and caching, and it wants you to take cron seriously. Skip any of those and it works fine for a fortnight, then degrades exactly when you've started trusting it with things you care about.
the actual stack
No all-in-one image this time. I want each piece visible and replaceable, so it's the FPM image behind its own web server, with Postgres and Redis as separate services on the shared Compose network.
services:
app:
image: nextcloud:30-fpm
restart: unless-stopped
depends_on:
- db
- redis
environment:
- POSTGRES_HOST=db
- REDIS_HOST=redis
volumes:
- ./data:/var/www/html
db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- ./pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: unless-stopped
The FPM split means a separate web container talks to PHP-FPM over the network, which is more moving parts but means the web server is just a web server. When something's slow I can tell whether it's PHP or the proxy, which on the previous attempts I genuinely could not.
the bits everyone skips
Two settings fixed most of the historical pain.
First, memory caching. Pointing Nextcloud at Redis for both the local cache and file locking turns the admin overview page from a wall of warnings into a single reassuring tick. APCu for the local cache, Redis for distributed locking.
Second, background jobs via system cron rather than the AJAX default. The AJAX mode only runs jobs when someone loads a page, which on a single-user instance means basically never, which means previews never generate and cleanup never happens. A proper cron container hitting cron.php every five minutes makes the whole thing feel alive.
*/5 * * * * php -f /var/www/html/cron.php
It's running now, the overview is all green ticks, and the file scan on my photo library finished in minutes rather than the hours it used to take. Whether this one survives is a question for future me, but for the first time I've built it like I expect it to last, which is probably the difference. The previous two were always temporary in my head, and software has a way of living down to your expectations.