SSD Nodes Learn
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-07-19

Nextcloud sur VPS : Docker, TLS, sauvegardes

Faites tourner Nextcloud sur votre VPS avec Docker Compose, Postgres, Redis et un proxy inverse TLS, avec sauvegardes et mises à jour fiables.

Ce que vous construisez réellement

Ce guide fait tourner Nextcloud sur un VPS avec Docker Compose, place Let's Encrypt TLS devant lui et met en place une sauvegarde qui se restaure vraiment. Quatre conteneurs et un proxy : l'image officielle nextcloud qui écoute sur la boucle locale, Postgres qui contient chaque élément des métadonnées de fichiers, Redis qui gère les verrous de fichiers, une seconde copie de l'image Nextcloud qui n'exécute rien d'autre que la boucle cron, et nginx sur l'hôte qui termine le TLS devant tout cela. L'installation elle-même prend vingt minutes, et ce n'est pas la partie qui compte. Deux décisions prises dans la première heure déterminent si vous avez encore vos fichiers dans un an : une vraie base de données au lieu de SQLite, et une sauvegarde qui capture le répertoire de données, la base de données et config.php comme un ensemble cohérent.

Cela suppose Ubuntu 24.04 LTS ou Debian 13, Docker Engine avec le plugin Compose v2 installé depuis le dépôt de Docker lui-même, et un enregistrement DNS A (plus AAAA si vous avez IPv6) qui pointe déjà cloud.example.com vers le VPS. Tout cela nécessite un serveur que vous contrôlez : il n'y a aucun moyen de faire la terminaison TLS et un dump de base de données sur le SaaS de quelqu'un d'autre.

Dimensionnement : ce qui consomme réellement la mémoire

L'utilisation de la mémoire par Nextcloud est dominée par trois choses, et aucune d'elles n'est « Nextcloud » à proprement parler.

Les workers PHP. L'image -apache sert chaque requête concurrente depuis un processus worker qui contient un interpréteur PHP. Chaque worker peut grandir jusqu'à PHP_MEMORY_LIMIT avant que PHP ne tue la requête. Votre mémoire résidente dans le pire des cas est à peu près le nombre de requêtes concurrentes × la limite de mémoire, et un client de synchronisation de bureau ouvre plusieurs connexions parallèles par utilisateur. C'est la concurrence, pas le nombre d'utilisateurs, qui fixe le plafond.

La base de données. Postgres crée un backend par connexion (fork) et garde les buffers partagés en mémoire résidente. Son ensemble de travail évolue avec le nombre de fichiers, pas le nombre d'octets : oc_filecache porte une ligne par fichier et par utilisateur. Cent mille petits fichiers forment une base de données plus lourde que cent gros fichiers.

La génération des aperçus. Générer une miniature décode l'image source en mémoire à pleine résolution. Les aperçus vidéo font appel à ffmpeg. Exécuter occ preview:generate-all répète ce pic encore et encore, coup sur coup, et c'est de loin la façon la plus courante de pousser un petit VPS dans les bras de l'OOM killer.

Redis est comparativement peu coûteux. Tout ce que vous ajoutez ensuite (Collabora, la recherche plein texte, un antivirus) est un service résident distinct avec sa propre empreinte, et doit figurer dans votre plan de dimensionnement avant de l'activer.

Les leviers, si vous êtes juste en RAM : baissez PHP_MEMORY_LIMIT, plafonnez preview_max_x / preview_max_y / preview_max_filesize_image, réduisez enabledPreviewProviders aux formats que vous consultez vraiment, et définissez trashbin_retention_obligation et versions_retention_obligation pour que le répertoire de données ne gonfle pas silencieusement jusqu'à plusieurs fois la taille de vos fichiers. Ajoutez un fichier d'échange (swap). Le swap est lent, mais un OOM kill au milieu d'une mise à jour est pire.

Pourquoi SQLite casse

Nextcloud est livré avec le support de SQLite et l'image officielle l'utilisera volontiers. Ne le faites pas. SQLite sérialise les écritures avec un verrou à l'échelle de toute la base : un seul écrivain à la fois, pour le fichier entier. Nextcloud écrit en permanence (verrous de fichiers, lignes d'activité, entrées de cache, état des tâches) et un seul client de bureau qui synchronise une arborescence de répertoires émet de nombreuses requêtes parallèles. Avec ce schéma, vous obtenez SQLSTATE[HY000]: General error: 5 database is locked et des erreurs HTTP 500, et la panne apparaît exactement au moment où l'instance commence à être utile.

Convertir plus tard est possible avec occ db:convert-type, mais c'est une migration longue et tout-ou-rien sur un jeu de données en production. Commencez avec Postgres ou MariaDB.

Le fichier Compose

Placez ceci dans /srv/nextcloud/compose.yaml, avec les secrets dans un fichier .env voisin en 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:

Épinglez le tag majeur et vérifiez celui en cours sur Docker Hub avant de recopier 31 tel quel. latest vous fera franchir une frontière majeure lors d'un futur docker compose pull, et Nextcloud ne le supporte pas.

Le répertoire de données est un bind mount, pas un volume nommé, et c'est volontaire : un chemin que vous pouvez cibler directement avec un outil de sauvegarde vaut mieux que la propreté. Créez-le avec l'UID www-data de l'image et les permissions qu'exige Nextcloud :

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

Notez la publication de port : 127.0.0.1:8080:80. Docker publie les ports en écrivant des règles DNAT qui sont évaluées avant même que la chaîne INPUT d'ufw ne voie le paquet : un simple 8080:80 place un Nextcloud non chiffré sur l'internet public quoi que dise ufw. Se lier à la boucle locale le garde hors de l'interface publique. Le pare-feu n'a alors qu'à autoriser le proxy, et si vous préférez ne pas laisser SSH ouvert à tout l'internet, joindre le VPS via un VPN WireGuard auto-hébergé vous permet de retirer entièrement le port 22 des règles publiques :

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

Démarrez le tout avec docker compose up -d, puis surveillez docker compose logs -f app. Le premier démarrage copie toute l'arborescence de l'application dans le volume et lance l'installateur ; le conteneur ne répond à rien tant que ce n'est pas terminé.

Le TLS et le proxy inverse

Installez nginx et certbot depuis la distribution, créez un bloc serveur simple sur le port 80 avec le bon server_name, puis laissez certbot le réécrire. Les mécanismes du défi HTTP-01, du timer de renouvellement et des modes de panne sont détaillés en entier dans émettre des certificats Let's Encrypt avec certbot et nginx sur Ubuntu 24.04 :

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

Certbot ajoute les lignes ssl_certificate et la redirection :80:443, et installe un timer systemd qui renouvelle le certificat valable 90 jours. Confirmez son existence avec systemctl list-timers | grep certbot : un timer de renouvellement qui n'a jamais été activé est une mèche de 90 jours.

Le bloc du proxy lui-même :

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

Sur nginx 1.25 et plus récent, ajoutez http2 on;. Ubuntu 24.04 fournit une version plus ancienne où l'équivalent est listen 443 ssl http2;. nginx -t vous dira laquelle votre version accepte.

client_max_body_size et les longs délais de lecture sont ce qui empêche les gros envois de mourir à mi-chemin. proxy_request_buffering off fait transiter l'envoi en flux plutôt que d'écrire d'abord tout le fichier sur le disque du proxy.

nginx sur l'hôte est la chose la plus simple qui fonctionne pour une seule application. Si Nextcloud doit partager le VPS avec d'autres conteneurs, utiliser Traefik comme proxy inverse Docker Compose pour plusieurs applications déplace le routage et l'émission des certificats dans des labels de conteneur, et les mêmes préoccupations de client_max_body_size et de délais réapparaissent là sous forme de réglages de middleware et de transport.

trusted_proxies et overwriteprotocol

C'est là que la plupart des instances Nextcloud auto-hébergées se trompent, et les symptômes semblent sans rapport avec la cause.

X-Forwarded-Proto: https n'est honoré que lorsque la requête arrive d'une adresse listée dans trusted_proxies. Quand il ne l'est pas, Nextcloud croit que la requête est du HTTP simple et émet des URL http:// ; le proxy les redirige vers HTTPS ; le navigateur suit ; Nextcloud émet à nouveau http://. C'est la boucle de redirection. OVERWRITEPROTOCOL: https fixe le schéma quoi qu'il arrive.

Le piège de TRUSTED_PROXIES est que l'adresse que Nextcloud voit n'est pas 127.0.0.1. nginx tourne sur l'hôte et se connecte à un port publié, donc le conteneur voit la passerelle du pont Docker, quelque chose en 172.x. Trouvez le vrai sous-réseau :

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

Mettez ce CIDR (ou le plus large 172.16.0.0/12) dans TRUSTED_PROXIES. Réglez-le trop large et n'importe quel client pourrait falsifier X-Forwarded-For ; réglez-le mal et chaque connexion semble venir de l'adresse de la passerelle, la protection anti-force-brute bloque toute votre instance d'un coup, et la vue d'administration affiche « The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. »

OVERWRITECLIURL compte pour le conteneur cron, qui n'a aucune requête entrante d'où déduire un nom d'hôte. Sans lui, les tâches en arrière-plan génèrent des liens vers localhost et les notifications par e-mail expédient des URL inutilisables.

Tâches en arrière-plan : cron, pas AJAX

Le lanceur de tâches par défaut de Nextcloud est AJAX : les tâches s'exécutent comme effet secondaire du chargement d'une page par quelqu'un. Personne ne navigue à 04h00, donc l'expiration de la corbeille, le nettoyage des versions, les aperçus et les nouvelles tentatives fédérées s'arrêtent, et le premier symptôme est un répertoire de données qui n'arrête jamais de grandir. Le service cron ci-dessus exécute la boucle officielle /cron.sh sur les mêmes volumes. Dites à Nextcloud de s'y attendre :

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

Chaque commande occ suit cette forme : docker compose exec -u www-data app php occ <command>. Cela vaut la peine d'en faire un alias.

Sauvegardes : trois choses, ou aucune

Une sauvegarde limitée au système de fichiers se restaure en une instance cassée. Le répertoire de données contient les octets ; Postgres contient le cache de fichiers, les partages, les utilisateurs et l'état des applications ; config.php contient les identifiants de la base de données, l'ID d'instance et le sel de mot de passe. Restaurez les fichiers sans la base de données et Nextcloud ne peut pas les voir. Restaurez la base de données sans config.php et il ne peut pas ouvrir la base. Restaurez une ancienne base contre un répertoire de données plus récent et vous obtenez des partages qui pointent vers des fichiers qui ont bougé.

Sauvegardez les trois, depuis une instance mise au repos :

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

Le mode maintenance est ce qui fait concorder le dump et la copie de fichiers l'un avec l'autre. Sautez-le et vous finirez par capturer une base de données qui référence un fichier que le rsync n'avait pas encore atteint. Notez que le script conserve des dumps de base de données horodatés mais un seul miroir tournant du répertoire de données (rsync --delete l'écrase à chaque exécution), donc seul le dump le plus récent va de pair avec la copie de fichiers.

Ensuite, sortez-la de la machine. Une sauvegarde qui vit sur le même VPS que la chose qu'elle sauvegarde est une copie, pas une sauvegarde. restic vers un stockage objet ou un second hôte est la réponse habituelle, et sa déduplication gère le répertoire de données bien mieux qu'une archive tar nocturne. La configuration complète, de l'initialisation du dépôt au timer nocturne et à l'exercice de restauration, se trouve dans sauvegardes VPS hors machine avec restic.

La restauration n'est pas simplement l'inverse. Une pile fraîchement démarrée lance l'installateur et écrit un config.php tout neuf (un nouvel ID d'instance et un nouveau sel de mot de passe), et importer le dump par-dessus cette nouvelle identité laisse des sessions et des jetons de partage cassés. Remettez d'abord l'ancienne identité, dans cet ordre :

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 réconcilie le cache de fichiers avec ce qui se trouve réellement sur le disque. Répétez cette manœuvre une fois, sur un VPS de rechange, avant d'en avoir besoin.

Mises à jour : une majeure à la fois

Nextcloud ne supporte la mise à jour que d'exactement une version majeure à la fois. Sauter de 29 à 31 n'échoue pas en douceur : cela échoue avec Exception: Updates between multiple major versions and downgrades are unsupported. et vous laisse en mode maintenance.

La mise à jour Docker consiste à : prendre une sauvegarde, changer le tag de 31 à 32 dans les deux services app et cron, puis docker compose pull && docker compose up -d, puis docker compose logs -f app. L'entrypoint de l'image détecte le code plus récent face aux données existantes et exécute occ upgrade lui-même. Ne l'interrompez pas. Quand les logs se taisent, exécutez docker compose exec -u www-data app php occ status et vérifiez versionstring et que les applications sont revenues activées.

Deux règles qui vous sauvent : montez d'une majeure, vérifiez, puis montez la suivante. Et ne changez jamais le tag du service app sans changer cron en conséquence : deux versions de Nextcloud différentes contre une seule base de données est un chemin vers la corruption.

Les erreurs que vous verrez vraiment

« Your data directory is readable by other users. Please change the permissions to 0770. » Le répertoire monté en bind a des bits de lecture pour le groupe ou pour tous. sudo chmod 0770 /srv/nextcloud/data et sudo chown -R 33:33 /srv/nextcloud/data.

« Your data directory is invalid. Ensure there is a file called .ocdata in the root. » Le bind mount pointe vers un endroit que Nextcloud n'a jamais initialisé : une faute de frappe dans le chemin, ou un répertoire vide et neuf substitué sous une instance qui marchait. Vérifiez que le chemin de l'hôte correspond à la ligne du volume.

« Access through untrusted domain. » Le nom d'hôte dans la requête n'est pas dans trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS ne s'applique qu'à la première installation ; ensuite, définissez-le à chaud : occ config:system:set trusted_domains 1 --value=cloud.example.com.

502 Bad Gateway, avec connect() failed (111: Connection refused) while connecting to upstream dans /var/log/nginx/error.log. nginx n'a rien atteint sur 127.0.0.1:8080. Soit le conteneur est encore en cours d'initialisation (vérifiez docker compose logs app), soit il s'est arrêté (docker compose ps), soit la ligne de publication ne correspond pas au port de proxy_pass. Confirmez avec ss -ltnp | grep 8080.

Une boucle de redirection, ou des avertissements « insecure » dans la vue d'administration. OVERWRITEPROTOCOL: https est absent, ou TRUSTED_PROXIES ne contient pas le sous-réseau de la passerelle Docker. Voir la section sur le proxy ci-dessus.

LockedException: "files/..." is locked. Avec REDIS_HOST défini, l'image configure Redis comme backend de verrouillage et les verrous obsolètes sont rares. Sans lui, les verrous vivent dans la table de base de données oc_file_locks et une requête tuée en pleine écriture laisse des lignes derrière elle. Confirmez que Redis est réellement utilisé (occ config:system:get memcache.locking doit renvoyer la classe Redis) avant d'aller effacer des lignes de verrous à la main.

« The PHP memory limit is below the recommended value of 512MB. » Augmentez PHP_MEMORY_LIMIT et recréez le conteneur. Souvenez-vous de ce que cela fait à votre plafond dans le pire des cas.

Ce qui casse à grande échelle

Le premier mur est le répertoire de données qui dépasse la taille du volume. Agrandir un volume sur un VPS est un redimensionnement plus un agrandissement du système de fichiers, et c'est bien moins pénible planifié qu'à 100 % de remplissage : alertez sur l'utilisation du disque maintenant, pas plus tard.

Le deuxième mur est oc_filecache. Les listages de fichiers et les analyses de synchronisation ralentissent avec le nombre de lignes, et la solution relève de la base de données : gardez Postgres sur un stockage rapide, laissez-le utiliser assez de mémoire partagée, et élaguez la corbeille et les versions avec des réglages de rétention plutôt que de les laisser s'accumuler indéfiniment.

Le troisième est la génération d'aperçus qui entre en concurrence avec tout le reste. Sur une petite machine, gardez les fournisseurs d'aperçus restreints et n'exécutez jamais occ preview:generate-all pendant les heures de travail.

Au-delà, la réponse honnête est que les extras veulent leur propre machine. Collabora et la recherche plein texte sont des services résidents distincts avec leurs propres profils mémoire, et les mettre sur la machine qui contient aussi votre unique copie de vos fichiers agrandit le domaine de panne sans aucun bénéfice. Déplacez le stockage des fichiers vers un stockage primaire compatible S3 quand le volume cesse d'avoir la bonne forme, et notez que cela rend les sauvegardes plus difficiles, pas plus faciles : la base de données contient toujours les métadonnées, et elle doit être dumpée en phase avec le bucket.

Une fois que l'instance sert de vrais utilisateurs, placez Uptime Kuma devant elle pour que vous entendiez parler d'une panne avant les clients de synchronisation. Un cloud privé se marie bien avec votre propre serveur de messagerie, et si vous préférez ne pas câbler les services à la main, Cloudron, CasaOS et Coolify comparent les plateformes qui le font pour vous.

FAQ

Puis-je faire tourner Nextcloud sur SQLite au lieu de Postgres ?

Vous le pouvez, et l'image officielle vous le permettra, mais un seul client de synchronisation de bureau qui émet des requêtes parallèles tombera sur SQLSTATE[HY000]: General error: 5 database is locked et des erreurs HTTP 500. SQLite prend un verrou d'écriture à l'échelle de toute la base, et Nextcloud écrit en permanence : verrous de fichiers, lignes d'activité, état des tâches. Commencez sur Postgres ou MariaDB ; occ db:convert-type existe mais c'est une migration longue et tout-ou-rien sur des données en production.

De combien de RAM un VPS Nextcloud a-t-il réellement besoin ?

Dimensionnez pour la concurrence, pas pour le nombre d'utilisateurs. La mémoire résidente dans le pire des cas est à peu près le nombre de requêtes concurrentes multiplié par PHP_MEMORY_LIMIT, plus les buffers partagés de Postgres et un backend par connexion, plus le pic éventuel de la génération d'aperçus. Une machine de 2 Go fait tourner une petite instance domestique si vous plafonnez les aperçus et ajoutez du swap ; ajoutez Collabora ou la recherche plein texte et vous dimensionnez un second ensemble de services résidents.

Pourquoi les gros envois échouent-ils derrière le proxy inverse nginx ?

Deux réglages du proxy l'expliquent généralement : client_max_body_size laissé à sa valeur par défaut de 1 Mo tronque la requête, et de courtes valeurs de proxy_read_timeout / proxy_send_timeout tuent les longs transferts à mi-chemin. Réglez les deux généreusement, mettez proxy_request_buffering off pour diffuser en flux plutôt que d'écrire sur disque, et augmentez PHP_UPLOAD_LIMIT sur le conteneur app pour correspondre.

Pourquoi Nextcloud boucle-t-il en redirection ou avertit-il à propos du proxy inverse ?

Le conteneur ne voit pas nginx sur 127.0.0.1, il voit la passerelle du pont Docker, quelque part en 172.x. Quand cette adresse manque dans TRUSTED_PROXIES, l'en-tête X-Forwarded-Proto: https est ignoré, Nextcloud émet des URL http://, et le proxy les renvoie. Réglez TRUSTED_PROXIES sur le vrai sous-réseau du pont et fixez OVERWRITEPROTOCOL: https.

Puis-je mettre à jour Nextcloud de 29 directement à 31 ?

Non. Nextcloud ne supporte qu'une seule version majeure par mise à jour, et sauter s'arrête sur Updates between multiple major versions and downgrades are unsupported., laissant l'instance en mode maintenance. Sauvegardez, montez le tag d'une majeure sur les deux services app et cron, docker compose pull && docker compose up -d, vérifiez avec occ status, puis répétez.