SSD Nodes Learn
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-07-24

Nextcloud installeren op VPS met Docker

Leer Nextcloud draaien op een VPS met Docker Compose, Postgres en Redis. Inclusief TLS met Let's Encrypt en een betrouwbaar back-up plan voor uw data.

Wat u daadwerkelijk bouwt

Deze handleiding installeert Nextcloud op een VPS met Docker Compose. Er wordt Let's Encrypt TLS voor geplaatst en er wordt een back-up ingesteld die daadwerkelijk hersteld kan worden. Vier containers en een proxy: de officiële nextcloud image die luistert op loopback, Postgres voor alle bestandmetadata, Redis voor de file locks, een tweede kopie van de Nextcloud image die uitsluitend de cron loop uitvoert, en nginx op de host voor de TLS-terminatie. De installatie duurt twintig minuten, maar dat is niet het belangrijkste deel. Twee beslissingen in het eerste uur bepalen of u over een jaar uw bestanden nog heeft: het gebruik van een echte database in plaats van SQLite, en een back-up die de data directory, de database en config.php als één consistent geheel vastlegt.

Dit vereist Ubuntu 24.04 LTS of Debian 13, Docker Engine met de Compose v2 plugin geïnstalleerd vanuit het officiële Docker-repository, en een DNS A record (plus AAAA bij gebruik van IPv6) dat al naar cloud.example.com op de VPS wijst. U heeft volledige controle over de server nodig — het is niet mogelijk om TLS-terminatie en een database dump uit te voeren op een externe SaaS.

Sizing: wat het geheugen daadwerkelijk verbruikt

Het geheugengebruik van Nextcloud wordt gedomineerd door drie factoren. Geen van deze factoren is "Nextcloud" zelf.

PHP workers. De -apache image verwerkt elke gelijktijdige aanvraag via een worker process dat een PHP interpreter bevat. Elke worker kan groeien tot PHP_MEMORY_LIMIT voordat PHP de aanvraag beëindigt. Het maximale resident geheugen is ongeveer gelijktijdige aanvragen × het geheugenlimiet. Een desktop sync client opent meerdere parallelle verbindingen per gebruiker. Het aantal gelijktijdige aanvragen bepaalt de limiet, niet het aantal gebruikers.

De database. Postgres start een backend per verbinding en houdt shared buffers in het geheugen. De werklast schaalt met het aantal bestanden, niet met het aantal bytes: oc_filecache bevat één rij per bestand per gebruiker. Honderdduizend kleine bestanden belast de database zwaarder dan honderd grote bestanden.

Preview generatie. Het genereren van een thumbnail decodeert de bronafbeelding naar het geheugen op volledige resolutie. Video previews maken gebruik van ffmpeg. Het uitvoeren van occ preview:generate-all veroorzaakt deze piek herhaaldelijk achter elkaar. Dit is de meest voorkomende oorzaak waardoor een kleine VPS door de OOM killer wordt afgesloten.

Redis is relatief goedkoop. Alles wat u later toevoegt — Collabora, full-text search, een antivirus scanner — is een aparte resident service met een eigen geheugenvoetafdruk. Neem dit op in uw sizing plan voordat u de functie activeert.

De instellingen, indien u beperkt RAM heeft: verlaag PHP_MEMORY_LIMIT, stel een limiet in voor preview_max_x / preview_max_y / preview_max_filesize_image, beperk enabledPreviewProviders tot de formaten die u daadwerkelijk bekijkt, en stel trashbin_retention_obligation en versions_retention_obligation zo in dat de data directory niet ongemerkt vele malen groter wordt dan de bestanden zelf. Voeg een swap file toe. Swap is traag, maar een OOM kill tijdens een upgrade is erger.

Waarom SQLite problemen veroorzaakt

Nextcloud bevat ondersteuning voor SQLite en de officiële image gebruikt dit standaard. Doe dit niet. SQLite blokkeert het hele bestand tijdens het schrijven: er kan slechts één schrijfactie tegelijkertijd plaatsvinden voor het volledige bestand. Nextcloud voert continu schrijfacties uit — zoals file locks, activity rows, cache entries en job states — terwijl een enkele desktop client die een mappenstructuur synchroniseert veel parallelle verzoeken verstuurt. Door dit patroon ontstaan SQLSTATE[HY000]: General error: 5 database is locked en HTTP 500-fouten. Deze fouten treden op zodra de instantie intensief wordt gebruikt.

Conversie is later mogelijk met behulp van occ db:convert-type, maar dit is een langdurige migratie waarbij de volledige dataset moet worden overgezet. Begin direct met Postgres of MariaDB.

Het Compose-bestand

Plaats dit in /srv/nextcloud/compose.yaml, met secrets in een naastgelegen .env-bestand in mode 600.

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}

  app:
    image: nextcloud:31-apache
    restart: unless-stopped
    depends_on: [db, redis]
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data
    environment:
      POSTGRES_HOST: db
      POSTGRES_DB: nextcloud
      POSTGRES_USER: nextcloud
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_HOST_PASSWORD: ${REDIS_PASSWORD}
      NEXTCLOUD_ADMIN_USER: admin
      NEXTCLOUD_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      NEXTCLOUD_TRUSTED_DOMAINS: cloud.example.com
      TRUSTED_PROXIES: 172.16.0.0/12
      OVERWRITEPROTOCOL: https
      OVERWRITECLIURL: https://cloud.example.com
      APACHE_DISABLE_REWRITE_IP: "1"
      PHP_MEMORY_LIMIT: 512M
      PHP_UPLOAD_LIMIT: 10G

  cron:
    image: nextcloud:31-apache
    restart: unless-stopped
    entrypoint: /cron.sh
    depends_on: [db, redis]
    volumes:
      - html:/var/www/html
      - /srv/nextcloud/data:/var/www/html/data

volumes:
  db:
  html:

Gebruik een specifieke major tag en controleer de huidige versie op Docker Hub voordat u 31 letterlijk kopieert. latest kan u naar een nieuwe major versie leiden bij een toekomstige docker compose pull, en Nextcloud ondersteunt dit niet.

De datadirectory is opzettelijk een bind mount en geen named volume: een pad dat u direct aan een backup-tool kunt koppelen is belangrijker dan een nette structuur. Maak de directory aan met de www-data UID van de image en de permissies die Nextcloud vereist:

sudo mkdir -p /srv/nextcloud/data
sudo chown -R 33:33 /srv/nextcloud/data
sudo chmod 0770 /srv/nextcloud/data

Let op de port publish: 127.0.0.1:8080:80. Docker publiceert poorten door DNAT-regels te schrijven die worden geëvalueerd voordat de INPUT-chain van ufw het pakket ziet. Een enkele 8080:80 plaatst een onversleutelde Nextcloud op het publieke internet, ongeacht de instellingen van ufw. Door te binden aan loopback blijft de applicatie buiten de publieke interface. De firewall hoeft dan alleen de proxy toe te laten — en als u SSH liever niet openzet voor het hele internet, dan kunt u met verbinding maken met de VPS via een zelfgehoste WireGuard VPN poort 22 volledig verwijderen uit de publieke regels:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Start de container met docker compose up -d en controleer vervolgens docker compose logs -f app. Tijdens de eerste start wordt de volledige applicatiestructuur naar het volume gekopieerd en wordt de installer uitgevoerd; de container reageert pas zodra dit proces is voltooid.

TLS en de reverse proxy

Installeer nginx en certbot via de distro. Maak een standaard port-80 server block aan met de juiste server_name. Laat certbot dit blok vervolgens herschrijven. De werking van de HTTP-01 challenge, de renewal timer en de foutmodi worden volledig beschreven in het uitgeven van Let's Encrypt certificaten met certbot en nginx op Ubuntu 24.04:

sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d cloud.example.com

Certbot voegt de ssl_certificate regels en de :80:443 redirect toe. Ook wordt een systemd timer geïnstalleerd die het 90-dagen certificaat vernieuwt. Controleer de aanwezigheid met systemctl list-timers | grep certbot. Een renewal timer die niet is ingeschakeld, werkt als een 90-dagen tijdbom.

De proxy block zelf:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name cloud.example.com;

    # certbot manages ssl_certificate / ssl_certificate_key here

    add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

    client_max_body_size 10G;
    client_body_timeout 300s;

    location = /.well-known/carddav { return 301 /remote.php/dav; }
    location = /.well-known/caldav  { return 301 /remote.php/dav; }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_request_buffering off;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

Voeg http2 on; toe op nginx 1.25 en nieuwer. Ubuntu 24.04 gebruikt een oudere build waarbij het equivalent listen 443 ssl http2; is. nginx -t geeft aan welke optie uw build ondersteunt.

client_max_body_size en de lange read timeouts voorkomen dat grote uploads halverwege mislukken. proxy_request_buffering off streamt de upload direct in plaats van het volledige bestand eerst naar de disk van de proxy te schrijven.

nginx op de host is de eenvoudigste oplossing voor één applicatie. Als Nextcloud de VPS moet delen met andere containers, dan verplaatst het draaien van Traefik als Docker Compose reverse proxy voor meerdere apps de routing en certificaatuitgifte naar container labels. Dezelfde client_max_body_size en timeout-aspecten zijn daar aanwezig als middleware en transport instellingen.

trusted_proxies en overwriteprotocol

Dit is de meest voorkomende fout bij zelfgehoste Nextcloud-instanties. De symptomen lijken vaak niets met de oorzaak te maken te hebben.

X-Forwarded-Proto: https wordt alleen geaccepteerd als de aanvraag afkomstig is van een adres dat in trusted_proxies staat vermeld. Als dit niet gebeurt, ziet Nextcloud de aanvraag als gewone HTTP en genereert het http:// URL's. De proxy leidt deze door naar HTTPS. De browser volgt deze doorleiding, waardoor Nextcloud opnieuw http:// genereert. Dit veroorzaakt een redirect loop. OVERWRITEPROTOCOL: https dwingt het protocol af, ongeacht de instellingen.

De fout bij TRUSTED_PROXIES is dat het adres dat Nextcloud ziet niet 127.0.0.1 is. nginx draait op de host en maakt verbinding via een gepubliceerde port. De container ziet hierdoor het Docker bridge gateway-adres — iets uit 172.x. Zoek het juiste subnet:

docker network inspect nextcloud_default \
  -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

Voeg dit CIDR (of de dekking van 172.16.0.0/12) toe aan TRUSTED_PROXIES. Als de instelling te breed is, kan elke client X-Forwarded-For vervalsen. Als de instelling onjuist is, lijkt elke login afkomstig van het gateway-adres. De brute-force bescherming blokkeert dan de gehele instantie en de admin-overzicht toont: "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy."

OVERWRITECLIURL is noodzakelijk voor de cron-container. Deze container ontvangt geen inkomende aanvragen om een hostname uit af te leiden. Zonder deze instelling genereren achtergrondtaken links naar localhost en bevatten e-mailmeldingen onbruikbare URL's.

Achtergrondtaken: cron, niet AJAX

De standaard job runner van Nextcloud is AJAX: taken worden uitgevoerd als een bijproduct van het laden van een pagina. Omdat er om 04:00 uur niemand surft, stagneren het verwijderen van prullenbakken, het opschonen van versies, het genereren van previews en federated retries. Het eerste symptoom is een datadirectory die constant blijft groeien. De cron service hierboven voert de officiële /cron.sh loop uit op dezelfde volumes. Laat Nextcloud hiervan weten:

docker compose exec -u www-data app php occ background:cron

Elk occ commando heeft deze structuur: docker compose exec -u www-data app php occ <command>. Het is raadzaam om hiervoor een alias aan te maken.

Backups: three things, or none

A filesystem-only backup restores to a broken instance. The data directory holds the bytes; Postgres holds the file cache, shares, users, and app state; config.php holds the database credentials, the instance ID and the password salt. Restore the files without the database and Nextcloud cannot see them. Restore the database without config.php and it cannot open the database. Restore an old database against a newer data directory and you get shares pointing at files that moved.

Back up all three, from a quiesced instance:

#!/usr/bin/env bash
set -euo pipefail
cd /srv/nextcloud
DEST="/var/backups/nextcloud/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DEST"

occ() { docker compose exec -T -u www-data app php occ "$@"; }

occ maintenance:mode --on
trap 'occ maintenance:mode --off' EXIT

docker compose exec -T db \
  pg_dump -U nextcloud --clean --if-exists nextcloud | gzip > "$DEST/db.sql.gz"

docker compose exec -T app \
  tar -C /var/www/html -cf - config custom_apps themes > "$DEST/app.tar"

rsync -a --delete /srv/nextcloud/data/ /var/backups/nextcloud/data/

Maintenance mode is what makes the dump and the file copy agree with each other. Skip it and you will eventually capture a database that references a file the rsync had not reached yet. Note that the script keeps timestamped database dumps but only one rolling mirror of the data directory — rsync --delete overwrites it each run — so only the newest dump pairs with the file copy.

Then get it off the box. A backup that lives on the same VPS as the thing it backs up is a copy, not a backup. restic against object storage or a second host is the usual answer, and its deduplication handles the data directory far better than a nightly tarball. The full setup, from repository init to the nightly timer and the restore drill, is in off-box VPS backups with restic.

Restore is not simply the reverse. A freshly-started stack runs the installer and writes a brand-new config.php — a new instance ID and password salt — and importing the dump on top of that new identity leaves broken sessions and share tokens. Put the old identity back first, in this order:

docker compose up -d && docker compose stop app cron    # create the volumes, then halt the app
sudo rsync -a --delete /var/backups/nextcloud/data/ /srv/nextcloud/data/
docker compose run --rm -T --entrypoint "" app \
  tar -C /var/www/html -xf - < app.tar                  # the original config.php returns
gunzip -c db.sql.gz | docker compose exec -T db psql -U nextcloud -d nextcloud
docker compose start app cron
docker compose exec -T -u www-data app php occ maintenance:mode --off
docker compose exec -T -u www-data app php occ files:scan --all

files:scan reconciles the file cache with what is actually on disk. Rehearse this once, on a spare VPS, before you need it.

Upgrades: één grote versie tegelijk

Nextcloud ondersteunt het upgraden van slechts één major version tegelijk. Een sprong van 29 naar 31 mislukt; dit veroorzaakt Exception: Updates between multiple major versions and downgrades are unsupported. en plaatst het systeem in maintenance mode.

De Docker upgrade verloopt als volgt: maak een backup, wijzig de tag van 31 naar 32 in zowel de app als de cron services, voer vervolgens docker compose pull && docker compose up -d uit, en daarna docker compose logs -f app. De image entrypoint controleert de nieuwe code tegen de bestaande data en voert occ upgrade zelfstandig uit. Onderbreek dit proces niet. Zodra de logs stoppen met schrijven, voert u docker compose exec -u www-data app php occ status uit en controleert u versionstring en of de apps weer ingeschakeld zijn.

Twee regels die fouten voorkomen: upgrade eerst één major version, verifieer dit, en upgrade dan de volgende. Pas nooit de tag aan in de app service zonder ook cron aan te passen; het gebruik van twee verschillende Nextcloud versies op één database leidt tot corruptie.

De fouten die u daadwerkelijk zult zien

"Your data directory is readable by other users. Please change the permissions to 0770." De bind-mounted directory heeft leesrechten voor de groep of andere gebruikers. sudo chmod 0770 /srv/nextcloud/data en sudo chown -R 33:33 /srv/nextcloud/data.

"Your data directory is invalid. Ensure there is a file called .ocdata in the root." De bind mount wijst naar een locatie die Nextcloud nooit heeft geïnitialiseerd — een typefout in het pad, of een nieuwe lege directory die een werkende instantie heeft vervangen. Controleer of het host-pad overeenkomt met de volume-regel.

"Access through untrusted domain." De hostname in het verzoek staat niet in trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS is alleen van toepassing bij de eerste installatie; stel het daarna live in: occ config:system:set trusted_domains 1 --value=cloud.example.com.

502 Bad Gateway, met connect() failed (111: Connection refused) while connecting to upstream in /var/log/nginx/error.log. nginx bereikte niets op 127.0.0.1:8080. De container is nog aan het initialiseren (controleer docker compose logs app), de container is gestopt (docker compose ps), of de publish-regel komt niet overeen met de proxy_pass port. Controleer dit met ss -ltnp | grep 8080.

Een redirect loop, of "insecure" waarschuwingen in de admin overview. OVERWRITEPROTOCOL: https ontbreekt, of TRUSTED_PROXIES bevat niet het Docker gateway subnet. Zie de sectie over de proxy hierboven.

LockedException: "files/..." is locked. Wanneer REDIS_HOST is ingesteld, configureert de image Redis als de locking backend en komen verouderde locks zelden voor. Zonder deze instelling staan locks in de database tabel oc_file_locks en laat een afgebroken verzoek tijdens het schrijven rijen achter. Controleer of Redis daadwerkelijk in gebruik is — occ config:system:get memcache.locking moet de Redis class retourneren — voordat u handmatig lock-rijen verwijdert.

"The PHP memory limit is below the recommended value of 512MB." Verhoog PHP_MEMORY_LIMIT en maak de container opnieuw aan. Houd rekening met het effect op uw maximale limiet.

Wat problemen veroorzaakt bij schaalvergroting

De eerste barrière is een datadirectory die de omvang van het volume overschrijdt. Het vergroten van een volume op een VPS vereist een resize en een filesystem grow. Dit is veel moeilijker te plannen wanneer de schijf voor 100% vol staat. Stel nu een alert in voor schijfgebruik, niet pas later.

De tweede barrière is oc_filecache. Het opstellen van bestandslijsten en sync scans worden trager naarmate het aantal rijen toeneemt. De oplossing ligt bij de database: houd Postgres op snelle opslag, zorg voor voldoende shared memory, en verwijder overbodige data en versies met retention settings in plaats van deze onbeperkt te laten ophopen.

De derde barrière is de generatie van previews die concurreert met andere processen. Gebruik op een kleine server beperkte preview providers en voer occ preview:generate-all nooit uit tijdens kantooruren.

Daarbuiten is het eerlijke antwoord dat extra componenten een eigen machine vereisen. Collabora en full-text search zijn aparte resident services met eigen geheugenprofielen. Het draaien van deze services op dezelfde machine als de enige kopie van uw bestanden vergroot de failure domain zonder voordeel. Verplaats de bestandsopslag naar S3-compatibele primaire opslag wanneer het volume niet langer de juiste vorm heeft. Houd er rekening mee dat dit back-ups bemoeilijkt in plaats van vergemakkelijkt: de database bevat nog steeds de metadata en moet gelijktijdig met de bucket worden gedumpt.

Zodra de instance echte gebruikers bedient, dient u Uptime Kuma ervoor te plaatsen. Zo krijgt u melding van downtime voordat de sync clients dit doen. Een private cloud werkt goed samen met uw eigen mailserver. Als u services niet handmatig wilt koppelen, bieden Cloudron, CasaOS en Coolify platforms die dit proces automatiseren.

FAQ

Kan ik Nextcloud op SQLite draaien in plaats van Postgres?

Dat kan, en de officiële image staat dit toe, maar een enkele desktop sync client met parallelle verzoeken veroorzaakt SQLSTATE[HY000]: General error: 5 database is locked en HTTP 500-fouten. SQLite gebruikt een database-brede write lock, terwijl Nextcloud constant schrijft — voor file locks, activity rows en job state. Begin met Postgres of MariaDB; occ db:convert-type bestaat, maar dit is een langdurige migratie waarbij alle data tegelijk moet worden omgezet.

Hoeveel RAM heeft een Nextcloud VPS daadwerkelijk nodig?

Kies de grootte op basis van gelijktijdige verzoeken, niet op basis van het aantal gebruikers. Het maximale geheugengebruik is ongeveer het aantal gelijktijdige verzoeken vermenigvuldigd met PHP_MEMORY_LIMIT, plus Postgres shared buffers en één backend per verbinding, plus de pieken tijdens het genereren van previews. Een server met 2 GB is geschikt voor een kleine huishoudelijke instantie als u previews beperkt en swap toevoegt; voeg Collabora of full-text search toe en u moet rekening houden met een tweede set actieve services.

Waarom mislukken grote uploads achter de nginx reverse proxy?

Dit wordt meestal veroorzaakt door twee instellingen op de proxy: client_max_body_size staat standaard op 1 MB, wat de request afbreekt, en korte proxy_read_timeout / proxy_send_timeout waarden verbreken langdurige transfers. Stel beide ruim voldoende in, zet proxy_request_buffering off op stream in plaats van spool, en verhoog PHP_UPLOAD_LIMIT in de app container naar een vergelijkbare waarde.

Waarom ontstaat er een redirect-loop of een waarschuwing over de reverse proxy in Nextcloud?

De container ziet nginx niet op 127.0.0.1 — de container ziet de Docker bridge gateway in 172.x. Wanneer dit adres ontbreekt in TRUSTED_PROXIES, wordt de X-Forwarded-Proto: https header genegeerd. Nextcloud genereert dan http:// URL's, die de proxy vervolgens weer terugstuurt. Stel TRUSTED_PROXIES in op het juiste bridge subnet en stel OVERWRITEPROTOCOL: https vast.

Kan ik Nextcloud direct upgraden van 29 naar 31?

Nee. Nextcloud ondersteunt slechts één hoofdversie per upgrade. Het overslaan van versies stopt bij Updates between multiple major versions and downgrades are unsupported., waardoor de instantie in maintenance mode komt te staan. Maak een backup, verhoog de tag met één hoofdversie voor zowel de app als de cron services en docker compose pull && docker compose up -d, verifieer met occ status, en herhaal het proces.