Setting Up Nextcloud Properly
Nextcloud is the flagship self-hosted application: file sync, calendar, contacts, and a large app ecosystem. It also has a reputation for being slow, which is mostly a configuration problem rather than a Nextcloud problem.
This covers deploying it so it is not.
The three things that decide performance
Before any of the deployment detail, these are what separate a fast instance from the one people complain about.
A real database. Not SQLite.
A memory cache. Redis, usually.
Cron as a system job. Not AJAX mode.
Everything else is secondary.
Deploying
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- ./db:/var/lib/postgresql/data
networks: [internal]
restart: unless-stopped
secrets: [db_password]
redis:
image: redis:7-alpine
command: redis-server --save "" --appendonly no
networks: [internal]
restart: unless-stopped
app:
image: nextcloud:31-apache
depends_on: [db, redis]
environment:
POSTGRES_HOST: db
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
REDIS_HOST: redis
NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
PHP_MEMORY_LIMIT: 1G
PHP_UPLOAD_LIMIT: 16G
volumes:
- ./html:/var/www/html
- /srv/nextcloud-data:/var/www/html/data
ports:
- "127.0.0.1:8080:80"
networks: [internal]
restart: unless-stopped
secrets: [db_password]
cron:
image: nextcloud:31-apache
entrypoint: /cron.sh
depends_on: [db, redis]
volumes:
- ./html:/var/www/html
- /srv/nextcloud-data:/var/www/html/data
networks: [internal]
restart: unless-stopped
secrets:
db_password:
file: ./secrets/db_password
networks:
internal:
internal: true
Several deliberate choices there.
PostgreSQL, per our PostgreSQL guide. MariaDB works too; SQLite does not, for anything real.
A separate cron container running /cron.sh. This is the supported way to get real background jobs in a container deployment, and it is the step most compose files omit.
PHP_UPLOAD_LIMIT: 16G. The default rejects large files with an unhelpful error.
Data on /srv/nextcloud-data, outside the application directory. Upgrades replace the application and never touch your files, and your backup target is obvious.
internal: true on the network, with only the app publishing a port, and that bound to loopback. Our container security guide covers why that matters.
Password from a file rather than an environment variable, since environment variables are visible in docker inspect and in the process list. Our sops and age guide covers managing the file itself.
After installation
The installer does not do these, and the admin overview will complain until you do.
Background jobs
Settings, Administration, Basic settings, set to Cron.
The compose file above already runs the cron container. If you are not using containers:
# /etc/systemd/system/nextcloud-cron.service
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/bin/php -f /var/www/nextcloud/cron.php
# /etc/systemd/system/nextcloud-cron.timer
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
AJAX mode runs maintenance only when someone loads a page. On a quiet instance the work simply piles up. Our cron versus timers comparison covers why a timer is the better choice here, and the timer builder generates both files.
Caching and locking
// config/config.php
'memcache.local' => '\OC\Memcache\APCu',
'memcache.distributed' => '\OC\Memcache\Redis',
'memcache.locking' => '\OC\Memcache\Redis',
'redis' => [
'host' => 'redis',
'port' => 6379,
],
memcache.locking is the important one. Transactional file locking prevents two clients corrupting a file by syncing simultaneously, and without a proper lock backend it falls back to the database and is slow.
Behind a proxy
'trusted_proxies' => ['172.18.0.0/16'],
'overwriteprotocol' => 'https',
'overwritehost' => 'cloud.example.com',
'overwrite.cli.url' => 'https://cloud.example.com',
Without overwriteprotocol, Nextcloud generates http:// URLs behind a TLS-terminating proxy and the browser blocks them as mixed content. This is the single most common “Nextcloud is broken behind my reverse proxy” cause.
Our reverse proxy explainer and the proxy builder cover the proxy side, including the WebSocket headers that Nextcloud Talk needs.
The security scan
Run the built-in check in Administration, Overview. It flags missing headers, a missing .well-known redirect for CalDAV and CardDAV discovery, and PHP settings.
The .well-known redirects are worth doing, because without them calendar and contact clients fail to autodiscover and people conclude the feature does not work:
# Caddy
redir /.well-known/carddav /remote.php/dav 301
redir /.well-known/caldav /remote.php/dav 301
Backups
This is the part that matters, and Nextcloud has a specific requirement: the database and the files must be consistent with each other.
Backing up files alone gives you data Nextcloud cannot index. Backing up the database alone gives you a catalogue pointing at nothing.
#!/usr/bin/env bash
set -euo pipefail
docker compose exec -T app php occ maintenance:mode --on
docker compose exec -T db pg_dump -Fc -U nextcloud nextcloud \
> /backup/nextcloud-$(date +%F).dump
restic backup /srv/nextcloud-data ./html/config /backup/nextcloud-$(date +%F).dump
docker compose exec -T app php occ maintenance:mode --off
Maintenance mode makes the snapshot consistent. Our restic and Borg comparison covers the backup tool, and the point about testing restores applies here more than anywhere: restore into a scratch instance and confirm you can log in and see your files.
occ, the command that does everything
docker compose exec -u www-data app php occ status
docker compose exec -u www-data app php occ files:scan --all
docker compose exec -u www-data app php occ user:resetpassword admin
docker compose exec -u www-data app php occ db:add-missing-indices
docker compose exec -u www-data app php occ maintenance:repair
files:scan is the fix when files added outside Nextcloud do not appear. db:add-missing-indices is worth running after upgrades, because new versions add indices and the upgrade does not always apply them.
Run as www-data, or the files created will have the wrong owner.
Upgrading
One major version at a time. 29 to 31 is not supported; 29 to 30 to 31 is.
Back up first, genuinely. A failed Nextcloud upgrade with no backup is a long evening.
Disable third-party apps before upgrading and re-enable them after, since an incompatible app can block the upgrade or break the interface afterwards.
Is it right for you
Worth saying honestly, because Nextcloud is heavy.
It is a PHP application with a database, a cache, a cron worker, and a large app ecosystem. It does file sync, calendars, contacts, notes, collaborative editing, and video calls. If you want that suite, it is the best self-hosted option there is.
If you only want file sync, Syncthing is simpler, faster, and has no server at all. If you only want calendar and contacts, Radicale is a fraction of the weight.
Nextcloud earns its complexity when you use several of its parts. Running the whole thing to sync one folder is a lot of machinery for the job, and our self-hosting introduction covers matching the tool to what you actually need.
Frequently Asked Questions
Why is my Nextcloud slow?
Usually three things: SQLite instead of PostgreSQL or MariaDB, no memory cache configured, and cron running through the web interface rather than as a real system job. Fixing those three accounts for most complaints about Nextcloud performance.
Should I use SQLite for Nextcloud?
Only for a single-user test instance. SQLite locks the whole database on writes, so concurrent activity from several clients syncing at once serialises badly. PostgreSQL is the recommended production choice and the migration later is more work than starting correctly.
What does the background jobs setting actually change?
AJAX mode runs maintenance tasks only when someone loads a page, so on a quiet instance they rarely run at all and work piles up. Setting it to Cron and running the job from systemd or crontab every five minutes means maintenance happens on schedule regardless of usage.
Where should user files be stored?
On a dedicated path outside the application directory, set as the datadirectory in config.php. That keeps data separate from the code so upgrades never touch it, makes backup targets obvious, and lets you put the data on different storage from the application.
Do I need Redis for Nextcloud?
You need a memory cache, and Redis is the usual choice because it also provides file locking. Without one, Nextcloud repeats expensive lookups constantly and the admin overview warns about it. APCu alone works for a single-server instance but does not handle locking as well.
How do I back up Nextcloud correctly?
Three things together: the data directory, a database dump, and config.php. Backing up files without the database gives you data Nextcloud cannot index, and the reverse gives you a catalogue pointing at nothing. Put the instance in maintenance mode while you take them so they are consistent.