Self-Hosting Nextcloud the Right Way: Docker Compose

2026-11-05 · Unix3r

A production-grade Nextcloud deployment in Docker Compose — with the database, cache, background jobs, reverse-proxy headers, PHP tuning and backup routine that turn a sluggish default install into something you actually want to use every day

Nextcloud is the first thing most people self-host and the first thing most people give up on. Not because it's bad — because the default install is slow, nags about a dozen warnings on the admin page, and loses its mind the moment you put it behind a reverse proxy.

Every one of those problems has a known fix. This post is the full deployment I run: Compose file, database, Redis, a dedicated cron container, the config.php additions that stop the warnings, Collabora for in-browser documents, and the backup routine. Copy it, change the secrets, and you'll have a Nextcloud that passes its own security check and opens a folder in under a second.

What we're building

Everything lives in one Compose project on an internal network. Only the app container publishes a port, and only to localhost or a Tailscale IP — never 0.0.0.0.

Directory layout

app/ — Nextcloud's own files (/var/www/html): apps, config, themes
data/ — user files. Put this on your biggest/cheapest disk; it's the only thing that grows
db/ — MariaDB
redis/ — Redis persistence (optional, it's a cache)

.env

docker-compose.yml

A few things worth noticing:

Pin the major version (nextcloud:30-apache), not latest. Nextcloud only supports upgrading one major at a time; an unpinned docker compose pull after a long gap will brick the install.
The cron container is the same image with a different entrypoint. The official image ships /cron.sh, which runs cron.php every five minutes as www-data. This is the only correct way to run background jobs — AJAX cron is unreliable and system cron on the host can't see the container's PHP.
TRUSTEDPROXIES includes Docker's default range and the Tailscale CGNAT range so the real client IP survives whatever proxy you use.
OVERWRITE variables tell Nextcloud it's behind HTTPS even though the container only speaks HTTP. Without these you get redirect loops and http:// links everywhere.

First boot

Give it two or three minutes the first time — it's copying the app tree into ./app/.

Then open https://cloud.example.com through your reverse proxy (see below) and log in with the admin credentials from .env.

Reverse proxy

The container listens on plain HTTP on ${NCBIND}:${NCPORT}. Whatever fronts it needs to pass the host header, forward the real IP, and allow large uploads.

Caddy:

Apache (Virtualmin-style vhost):

The .well-known redirects are what clear the "Your web server is not properly set up to resolve /.well-known/caldav" warning. The HSTS header clears another one.

config.php: the settings the installer doesn't set

Run these once after first login. occ is Nextcloud's CLI; everything below is persisted into app/config/config.php.

Now Administration → Overview should show "All checks passed." If anything's left, it tells you the exact occ command.

Why Redis matters

Without memcache.locking, Nextcloud uses the database for file locks. Every file operation becomes several INSERT/DELETE queries on ocfilelocks, and a sync client with 10,000 files turns that table into a hot spot. Redis locking is the single biggest performance change on this page.

Collabora: documents in the browser (optional)

Add to docker-compose.yml:

Give it its own hostname through the proxy (office.example.com → 127.0.0.1:9980, WebSocket support required — Caddy does this by default, Apache needs modproxywstunnel), then in Nextcloud:

Collabora is RAM-hungry (plan for 1–2 GB when documents are open). If the box is small, skip this or use the "built-in CODE server" app for occasional use.

Backups

Three things need backing up, in this order of importance: data/, the database, app/config/. Everything else is reinstallable.

Maintenance mode is on for the few seconds the DB dump takes, so the dump and the files are consistent with each other. Run it from a systemd timer at 03:00 and ship $DEST off-box with Restic.

Restore is the reverse: bring up db alone, pipe the dump in, restore data/ and app/config/, start app, run occ maintenance:data-fingerprint so clients know the server was restored, then occ files:scan --all.

Upgrades

Then check Overview for new warnings — every major release adds one or two.

Day-two checklist

Install the desktop and mobile clients, point them at https://cloud.example.com, confirm two-way sync.
Enable Two-Factor TOTP (occ app:enable twofactortotp) and enforce it for the admin group.
Turn on server-side encryption only if you understand the recovery-key implications — it's not the same as end-to-end and it makes backups harder to verify.
Add External storage (SMB, S3, another server over WebDAV) if you want Nextcloud to be the single front door to everything.
Put the admin login behind Tailscale or Authelia; the public hostname only needs to serve WebDAV and the normal user UI.

The short version

Nextcloud is slow when it runs without Redis, without a real cron, and with its database left untuned. Fix those three, behind a proxy that forwards the right headers, and it becomes the dependable "my files, my calendar, my contacts, my documents" server it's supposed to be — on hardware that costs less per month than the subscription it replaces.