SSD Nodes Learn
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-07-24

Installare Nextcloud su VPS con Docker

Guida pratica per configurare Nextcloud con Docker Compose, Postgres e Redis. Impara a gestire TLS e backup sicuri per evitare la perdita dei tuoi dati.

Cosa stai costruendo effettivamente

Questa guida installa Nextcloud su un VPS utilizzando Docker Compose, configura TLS tramite Let's Encrypt e imposta un backup che sia effettivamente ripristinabile. Il sistema utilizza quattro container e un proxy: l'immagine ufficiale nextcloud in ascolto su loopback, Postgres per i metadati dei file, Redis per i file lock, una seconda copia dell'immagine Nextcloud per l'esecuzione del cron e nginx sull'host per la terminazione TLS. L'installazione richiede venti minuti, ma non è la parte fondamentale. Due decisioni prese nella prima ora determinano la sicurezza dei tuoi file tra un anno: l'uso di un database reale invece di SQLite e un backup che includa la directory dei dati, il database e config.php come set consistente.

Il procedimento richiede Ubuntu 24.04 LTS o Debian 13, Docker Engine con il plugin Compose v2 installato dal repository ufficiale di Docker e un record DNS A (più AAAA se utilizzi IPv6) già puntato verso cloud.example.com sul VPS. È necessario un server sotto il proprio controllo: non è possibile eseguire la terminazione TLS e il dump del database su un servizio SaaS esterno.

Dimensionamento: cosa consuma effettivamente la memoria

L'utilizzo di memoria di Nextcloud è dominato da tre fattori, e nessuno di essi è "Nextcloud" in quanto tale.

PHP workers. L'immagine -apache gestisce ogni richiesta concorrente tramite un processo worker che contiene un interprete PHP. Ogni worker può occupare fino a PHP_MEMORY_LIMIT prima che PHP interrompa la richiesta. La memoria residente nel caso peggiore è approssimativamente numero di richieste concorrenti × limite di memoria; il client di sincronizzazione desktop apre diverse connessioni parallele per ogni utente. Il limite massimo è determinato dalla concorrenza, non dal numero di utenti.

Il database. Postgres crea un backend per ogni connessione e mantiene residenti i buffer condivisi. Il set di dati di lavoro scala in base al numero di file, non al numero di byte: oc_filecache contiene una riga per ogni file per ogni utente. Centomila file piccoli generano un database più pesante rispetto a cento file grandi.

Generazione delle anteprime. La generazione di una miniatura decodifica l'immagine originale in memoria alla risoluzione completa. Le anteprime video utilizzano ffmpeg tramite chiamate di sistema. L'esecuzione di occ preview:generate-all provoca questo picco ripetutamente e in sequenza; è il motivo principale per cui un piccolo VPS viene terminato dall'OOM killer.

Redis è relativamente economico. Qualsiasi componente aggiuntivo successivo — Collabora, ricerca full-text, antivirus — è un servizio residente separato con il proprio footprint; deve essere incluso nel piano di dimensionamento prima di essere abilitato.

Le opzioni di configurazione, se la RAM è limitata: ridurre PHP_MEMORY_LIMIT, limitare preview_max_x / preview_max_y / preview_max_filesize_image, ridurre enabledPreviewProviders solo ai formati effettivamente visualizzati, e configurare trashbin_retention_obligation e versions_retention_obligation affinché la directory dei dati non cresca silenziosamente fino a superare di diverse volte la dimensione dei file. Aggiungere un file di swap. Lo swap è lento, ma un OOM kill durante un upgrade è peggio.

Perché SQLite fallisce

Nextcloud include il supporto per SQLite e l'immagine ufficiale lo utilizzerà di default. Non farlo. SQLite serializza le operazioni di scrittura tramite un lock sull'intero database: un solo scrittore alla volta per l'intero file. Nextcloud esegue scritture costanti — lock dei file, righe di attività, voci della cache, stato dei job — e un singolo client desktop che sincronizza una struttura di directory invia molteplici richieste parallele. Con questo schema si ottengono SQLSTATE[HY000]: General error: 5 database is locked ed errori HTTP 500; il fallimento si verifica non appena l'istanza diventa operativa.

La conversione successiva è possibile tramite occ db:convert-type, ma si tratta di una migrazione lunga e totale su un dataset attivo. Inizia con Postgres o MariaDB.

The Compose file

Inserisci questo file in /srv/nextcloud/compose.yaml, con i segreti in un file .env adiacente in modalità 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:

Fissa il tag principale e verifica quello attuale su Docker Hub prima di copiare 31 così com'è. latest potrebbe causare un aggiornamento a una versione maggiore durante un futuro docker compose pull, e Nextcloud non supporta questo passaggio.

La directory dei dati è un bind mount e non un volume nominato per scelta: un percorso puntabile direttamente da un tool di backup è più importante dell'ordine dei file. Creala con l'UID www-data dell'immagine e i permessi richiesti da Nextcloud:

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

Nota la pubblicazione della porta: 127.0.0.1:8080:80. Docker pubblica le porte scrivendo regole DNAT che vengono valutate prima che la catena INPUT di ufw veda il pacchetto — una porta 8080:80 aperta espone un Nextcloud non criptato su internet, indipendentemente dalle impostazioni di ufw. Il binding su loopback lo mantiene isolato dall'interfaccia pubblica. In questo modo il firewall deve solo autorizzare il proxy — e se preferisci non lasciare SSH aperto su internet, connetterti al VPS tramite una VPN WireGuard self-hosted ti permette di rimuovere completamente la porta 22 dalle regole pubbliche:

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

Avvia il servizio con docker compose up -d, quindi monitora docker compose logs -f app. Al primo avvio, l'applicazione copia l'intera struttura delle cartelle nel volume ed esegue l'installer; il container non risponde a nessuna richiesta finché l'operazione non è completata.

TLS e il reverse proxy

Installa nginx e certbot tramite il distributore, crea un server block sulla porta 80 con il corretto server_name, quindi lascia che certbot riscriva la configurazione. Il funzionamento dell'HTTP-01 challenge, il timer di rinnovo e le modalità di errore sono descritti dettagliatamente in emissione di certificati Let's Encrypt con certbot e nginx su Ubuntu 24.04:

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

Certbot aggiunge le righe ssl_certificate e il redirect :80:443, e installa un timer systemd per il rinnovo del certificato di 90 giorni. Verifica la sua presenza con systemctl list-timers | grep certbot; un timer di rinnovo non abilitato è un rischio per la scadenza a 90 giorni.

Il blocco proxy:

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;
    }
}

Su nginx 1.25 e versioni successive, aggiungi http2 on;. Ubuntu 24.04 utilizza una versione precedente dove l'equivalente è listen 443 ssl http2;. nginx -t indica quale versione è accettata dalla tua build.

client_max_body_size e i timeout di lettura prolungati evitano che i caricamenti di grandi dimensioni falliscano a metà operazione. proxy_request_buffering off gestisce il caricamento tramite streaming invece di scrivere l'intero file sul disco del proxy.

L'uso di nginx sull'host è la soluzione più semplice per una singola applicazione. Se Nextcloud deve condividere il VPS con altri container, esecuzione di Traefik come reverse proxy Docker Compose per più app sposta il routing e l'emissione dei certificati all'interno delle label dei container; in quel caso, le stesse preoccupazioni relative a client_max_body_size e ai timeout si ripresentano come impostazioni di middleware e trasporto.

trusted_proxies e overwriteprotocol

Questo è il punto in cui la maggior parte delle istanze Nextcloud self-hosted presenta errori, con sintomi che sembrano non correlati alla causa.

X-Forwarded-Proto: https viene rispettato solo se la richiesta proviene da un indirizzo elencato in trusted_proxies. Se non viene rispettato, Nextcloud ritiene che la richiesta sia in HTTP semplice e genera URL http://; il proxy reindirizza tali URL verso HTTPS; il browser segue il reindirizzamento; Nextcloud genera nuovamente http://. Questo causa un loop di reindirizzamento. OVERWRITEPROTOCOL: https impone lo schema indipendentemente da tutto.

L'errore in TRUSTED_PROXIES consiste nel fatto che l'indirizzo visto da Nextcloud non è 127.0.0.1. nginx è in esecuzione sull'host e si connette a una porta pubblicata, quindi il container vede il gateway del bridge Docker — ovvero un indirizzo appartenente a 172.x. Trova la subnet reale:

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

Inserisci quel CIDR (o il relativo 172.16.0.0/12) in TRUSTED_PROXIES. Se l'intervallo è troppo ampio, qualsiasi client potrebbe falsificare X-Forwarded-For; se è errato, ogni login sembrerà provenire dall'indirizzo del gateway, la protezione brute-force bloccherà l'intera istanza e la panoramica admin mostrerà "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy."

OVERWRITECLIURL è fondamentale per il container cron, che non riceve richieste in entrata per inferire un hostname. Senza questa impostazione, i job in background generano link verso localhost e le notifiche email inviano URL inutilizzabili.

Background jobs: cron, non AJAX

Il runner di job predefinito di Nextcloud è AJAX: i job vengono eseguiti come effetto collaterale del caricamento di una pagina da parte di un utente. Poiché nessuno naviga alle ore 04:00, l'espiazione dei cestini, la pulizia delle versioni, la generazione delle anteprime e i tentativi di riconnessione federata si bloccano. Il primo sintomo è una directory dei dati che cresce continuamente. Il servizio cron sopra indicato esegue il loop ufficiale /cron.sh sugli stessi volumi. Configura Nextcloud per utilizzarlo:

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

Ogni comando occ segue questo schema: docker compose exec -u www-data app php occ <command>. È consigliabile creare un alias.

Backups: tre elementi, o nessuno

Un backup del solo filesystem non ripristina un'istanza corrotta. La directory dei dati contiene i byte; Postgres gestisce la cache dei file, le condivisioni, gli utenti e lo stato dell'app; config.php contiene le credenziali del database, l'ID dell'istanza e il password salt. Se si ripristinano i file senza il database, Nextcloud non può vederli. Se si ripristina il database senza config.php, non è possibile aprire il database. Se si ripristina un database vecchio su una directory dati più recente, le condivisioni punteranno a file che sono stati spostati.

Eseguire il backup di tutti e tre gli elementi da un'istanza in stato di quiescenza:

#!/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/

La modalità di manutenzione garantisce la coerenza tra il dump e la copia dei file. Se viene saltata, si rischia di catturare un database che fa riferimento a un file che rsync non ha ancora elaborato. Nota: lo script conserva i dump del database con timestamp, ma mantiene solo un mirror rotante della directory dei dati — rsync --delete lo sovrascrive a ogni esecuzione — pertanto solo l'ultimo dump è compatibile con la copia dei file.

Successivamente, spostare i dati fuori dal server. Un backup salvato sullo stesso VPS dell'istanza da proteggere è una copia, non un backup. La soluzione standard è l'uso di restic verso uno storage a oggetti o un secondo host; la sua deduplicazione gestisce la directory dei dati molto meglio di un archivio tar notturno. La configurazione completa, dall'inizializzazione del repository al timer notturno e alla procedura di ripristino, è disponibile in backups su VPS esterni con restic.

Il ripristino non è una semplice operazione inversa. Un sistema appena avviato esegue l'installer e genera un nuovo config.php — un nuovo instance ID e un nuovo password salt — e importare il dump su questa nuova identità causa errori nelle sessioni e nei token di condivisione. Ripristinare prima la vecchia identità, seguendo questo ordine:

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 sincronizza la cache dei file con il contenuto effettivo su disco. Eseguire una prova su un VPS di test prima di averne effettivamente bisogno.

Upgrades: una versione major alla volta

Nextcloud supporta l'aggiornamento di una sola versione major alla volta. Il passaggio diretto dalla versione 29 alla 31 non avviene correttamente: si verifica l'errore Exception: Updates between multiple major versions and downgrades are unsupported. e il sistema entra in maintenance mode.

L'aggiornamento tramite Docker segue questo procedimento: eseguire un backup, modificare il tag da 31 a 32 sia nel servizio app che nel servizio cron, quindi eseguire docker compose pull && docker compose up -d e successivamente docker compose logs -f app. L'entrypoint dell'immagine rileva il nuovo codice rispetto ai dati esistenti ed esegue automaticamente occ upgrade. Non interrompere l'operazione. Quando i log smettono di scorrere, eseguire docker compose exec -u www-data app php occ status e verificare versionstring e che le app siano tornate abilitate.

Due regole per evitare errori: aggiornare una versione major, verificare il funzionamento, quindi aggiornare la successiva. Non modificare mai il tag nel servizio app senza aggiornare anche cron in modo corrispondente: l'uso di due versioni diverse di Nextcloud su un unico database causa la corruzione dei dati.

Gli errori che visualizzerai effettivamente

"Your data directory is readable by other users. Please change the permissions to 0770." La directory montata tramite bind-mount ha permessi di lettura per il gruppo o per altri utenti. sudo chmod 0770 /srv/nextcloud/data e sudo chown -R 33:33 /srv/nextcloud/data.

"Your data directory is invalid. Ensure there is a file called .ocdata in the root." Il bind mount punta a una directory non inizializzata da Nextcloud — un errore di battitura nel percorso, o una directory vuota sovrascritta sotto un'istanza funzionante. Verifica che il percorso sull'host corrisponda alla riga del volume.

"Access through untrusted domain." L'hostname nella richiesta non è presente in trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS si applica solo durante la prima installazione; successivamente, imposta il dominio live: occ config:system:set trusted_domains 1 --value=cloud.example.com.

502 Bad Gateway, con connect() failed (111: Connection refused) while connecting to upstream in /var/log/nginx/error.log. nginx non riesce a connettersi a 127.0.0.1:8080. Il container è ancora in fase di inizializzazione (controlla docker compose logs app), è uscito (docker compose ps), oppure la riga di pubblicazione non corrisponde alla porta proxy_pass. Verifica con ss -ltnp | grep 8080.

Un loop di reindirizzamento, o avvisi "insecure" nella panoramica admin. OVERWRITEPROTOCOL: https è assente, oppure TRUSTED_PROXIES non contiene la subnet del gateway Docker. Consulta la sezione dedicata al proxy sopra.

LockedException: "files/..." is locked. Con REDIS_HOST impostato, l'immagine configura Redis come backend per il locking e i lock obsoleti sono rari. Senza questa impostazione, i lock risiedono nella tabella del database oc_file_locks e una richiesta interrotta durante la scrittura lascia righe residue. Verifica che Redis sia effettivamente in uso — occ config:system:get memcache.locking deve restituire la classe Redis — prima di procedere alla cancellazione manuale delle righe di lock.

"The PHP memory limit is below the recommended value of 512MB." Aumenta PHP_MEMORY_LIMIT e ricrea il container. Considera l'impatto sul limite massimo di memoria allocata.

Cosa si rompe su larga scala

Il primo limite è la directory dei dati che supera la dimensione del volume. Espandere un volume su un VPS richiede il ridimensionamento del volume e l'espansione del filesystem; è un'operazione meno problematica se pianificata rispetto a una gestione con disco al 100% — imposta un alert sull'utilizzo del disco subito, non in seguito.

Il secondo limite è oc_filecache. L'elenco dei file e le scansioni di sincronizzazione rallentano all'aumentare del numero di righe. La soluzione richiede interventi sul database: mantieni Postgres su storage veloce, assegna una quantità sufficiente di shared memory e rimuovi i dati obsoleti e le versioni tramite impostazioni di retention, invece di lasciarli accumulare indefinitamente.

Il terzo limite è la generazione delle anteprime che compete con tutti gli altri processi. Su una macchina piccola, limita i provider di anteprima e non eseguire mai occ preview:generate-all durante l'orario di lavoro.

Oltre questo punto, la risposta onesta è che i servizi aggiuntivi richiedono una macchina dedicata. Collabora e la ricerca full-text sono servizi residenti separati con profili di memoria propri; installarli sulla stessa macchina che ospita l'unica copia dei tuoi file aumenta il dominio di errore senza alcun beneficio. Sposta lo storage dei file su uno storage primario compatibile con S3 quando il volume non è più adatto — nota che questo rende i backup più complessi, non più semplici: il database contiene ancora i metadati e deve essere esportato in sincronia con il bucket.

Quando l'istanza serve utenti reali, installa Uptime Kuma davanti ad essa, così da ricevere notifiche di downtime prima che i client di sincronizzazione rilevino il problema. Un cloud privato si integra bene con il proprio mail server; se preferisci non configurare i servizi manualmente, Cloudron, CasaOS e Coolify sono piattaforme che automatizzano questo processo.

FAQ

Posso eseguire Nextcloud su SQLite invece di Postgres?

È possibile e l'immagine ufficiale lo consente, ma un singolo client desktop che esegue richieste parallele causerà errori SQLSTATE[HY000]: General error: 5 database is locked e HTTP 500. SQLite applica un lock di scrittura sull'intero database, mentre Nextcloud esegue scritture costanti per lock dei file, righe di attività e stato dei job. Inizia con Postgres o MariaDB; occ db:convert-type è disponibile, ma è una migrazione lunga e definitiva sui dati live.

Di quanta RAM ha realmente bisogno un VPS per Nextcloud?

Dimensiona in base alla concorrenza, non al numero di utenti. La memoria residente nel caso peggiore è approssimativamente il numero di richieste simultanee moltiplicato per PHP_MEMORY_LIMIT, più i shared buffers di Postgres e un backend per ogni connessione, più i picchi per la generazione delle anteprime. Una macchina da 2 GB gestisce un'istanza domestica di piccole dimensioni se si limita la generazione delle anteprime e si aggiunge lo swap; con Collabora o la ricerca full-text, è necessario dimensionare un secondo set di servizi residenti.

Perché i caricamenti di grandi dimensioni falliscono dietro il reverse proxy nginx?

Due impostazioni sul proxy spiegano solitamente il problema: client_max_body_size impostato al default di 1 MB tronca la richiesta, e valori brevi per proxy_read_timeout / proxy_send_timeout interrompono i trasferimenti lunghi a metà. Imposta entrambi con valori generosi, imposta proxy_request_buffering off su stream invece di spool e aumenta PHP_UPLOAD_LIMIT sul container dell'app per corrispondere ai valori del proxy.

Perché Nextcloud va in redirect loop o segnala errori relativi al reverse proxy?

Il container non vede nginx su 127.0.0.1, ma vede il gateway del bridge Docker, all'interno di 172.x. Quando quell'indirizzo manca in TRUSTED_PROXIES, l'header X-Forwarded-Proto: https viene ignorato, Nextcloud genera URL con http:// e il proxy li rimanda indietro in un ciclo infinito. Imposta TRUSTED_PROXIES sulla subnet reale del bridge e fissa OVERWRITEPROTOCOL: https.

Posso aggiornare Nextcloud dalla versione 29 direttamente alla 31?

No. Nextcloud supporta un'unica versione maggiore per ogni aggiornamento; saltare i passaggi si ferma con Updates between multiple major versions and downgrades are unsupported., lasciando l'istanza in maintenance mode. Esegui il backup, aggiorna il tag di una versione maggiore sia per i servizi app che cron, esegui docker compose pull && docker compose up -d, verifica con occ status e ripeti l'operazione.