SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-24

Nextcloud en un VPS con Docker, TLS y copias de seguridad

Configura Nextcloud en un VPS con Docker Compose, Postgres, Redis y nginx, y aprende a respaldar y restaurar datos, base de datos y configuración de forma coherente.

Lo que realmente está configurando

Esta guía ejecuta Nextcloud en un VPS con Docker Compose, coloca TLS de Let's Encrypt delante y configura una copia de seguridad que se puede restaurar de verdad. Hay cuatro contenedores y un proxy: la imagen oficial nextcloud, que escucha en loopback; Postgres, que almacena todos los metadatos de los archivos; Redis, que almacena los bloqueos de archivos; una segunda copia de la imagen de Nextcloud, que ejecuta únicamente el bucle de cron; y nginx en el host, que termina TLS delante de todos ellos. La instalación tarda veinte minutos, pero no es la parte importante. Dos decisiones tomadas durante la primera hora determinan si sus archivos seguirán disponibles dentro de un año: usar una base de datos real en lugar de SQLite y realizar una copia de seguridad que capture el directorio de datos, la base de datos y config.php como un conjunto coherente.

Esto presupone Ubuntu 24.04 LTS o Debian 13, Docker Engine con el complemento Compose v2 instalado desde el repositorio oficial de Docker y un registro DNS A (además de AAAA si tiene IPv6) que ya apunte cloud.example.com al VPS. Todo requiere un servidor bajo su control. No hay forma de terminar TLS y volcar una base de datos en el SaaS de otra persona.

Dimensionamiento: qué consume realmente la memoria

El uso de memoria de Nextcloud está dominado por tres elementos, y ninguno de ellos es «Nextcloud» como tal.

Procesos de PHP. La imagen -apache atiende cada petición simultánea mediante un proceso de trabajo que contiene un intérprete de PHP. Cada proceso puede crecer hasta PHP_MEMORY_LIMIT antes de que PHP finalice la petición. La memoria residente máxima aproximada es peticiones simultáneas × límite de memoria, y un cliente de sincronización de escritorio abre varias conexiones paralelas por usuario. El límite lo establece la concurrencia, no el número de usuarios.

La base de datos. Postgres crea un proceso backend por conexión y mantiene los búferes compartidos en memoria residente. Su conjunto de trabajo aumenta según el número de archivos, no según el número de bytes: oc_filecache contiene una fila por archivo y por usuario. Cien mil archivos pequeños suponen una base de datos más exigente que cien archivos grandes.

Generación de vistas previas. La generación de una miniatura decodifica la imagen de origen en memoria con su resolución completa. Las vistas previas de vídeo ejecutan ffmpeg mediante un proceso externo. Ejecutar occ preview:generate-all repite ese pico varias veces seguidas y es la forma más habitual de provocar que un VPS pequeño active el OOM killer.

Redis consume relativamente pocos recursos. Cualquier componente que añada después, como Collabora, la búsqueda de texto completo o un antivirus, es un servicio residente independiente con su propia huella de memoria. Debe incluirlo en el plan de dimensionamiento antes de activarlo.

Si falta RAM, ajuste estos parámetros: reduzca PHP_MEMORY_LIMIT, limite preview_max_x / preview_max_y / preview_max_filesize_image, reduzca enabledPreviewProviders a los formatos que realmente consulta y configure trashbin_retention_obligation y versions_retention_obligation para que el directorio de datos no crezca silenciosamente hasta varias veces el tamaño de sus archivos. Añada un archivo de swap. El swap es lento, pero un OOM kill durante una actualización es peor.

Por qué SQLite falla

Nextcloud incluye compatibilidad con SQLite y la imagen oficial lo utilizará sin problemas. No lo haga. SQLite serializa las escrituras mediante un bloqueo de toda la base de datos: sólo puede escribir un proceso cada vez y el bloqueo se aplica a todo el archivo. Nextcloud escribe constantemente: bloqueos de archivos, registros de actividad, entradas de caché y estado de las tareas. Además, un solo cliente de escritorio que sincroniza un árbol de directorios genera muchas solicitudes en paralelo. Con ese patrón aparecen SQLSTATE[HY000]: General error: 5 database is locked y errores HTTP 500. El fallo se produce justo cuando la instancia empieza a ser útil.

Más adelante es posible convertir la base de datos mediante occ db:convert-type, pero se trata de una migración larga y atómica sobre un conjunto de datos activo. Empiece con Postgres o MariaDB.

El archivo Compose

Coloque esto en /srv/nextcloud/compose.yaml, con los secretos en un archivo .env contiguo con el modo 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:

Fije la etiqueta principal y compruebe la actual en Docker Hub antes de copiar 31 literalmente. latest le hará pasar a otra versión principal en algún docker compose pull futuro, y Nextcloud no admite ese cambio.

El directorio de datos es un bind mount, no un volumen con nombre, de forma intencionada: una ruta que pueda proporcionar directamente a una herramienta de copias de seguridad es más útil que una estructura ordenada. Créelo con el UID www-data de la imagen y los permisos que exige Nextcloud:

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

Observe la publicación del puerto: 127.0.0.1:8080:80. Docker publica puertos mediante reglas DNAT que se evalúan antes de que el paquete llegue a la cadena INPUT de ufw; un 8080:80 sin más expone un Nextcloud sin cifrado en Internet, independientemente de lo que indique ufw. El enlace a loopback lo mantiene fuera de la interfaz pública. Así, el firewall sólo tiene que permitir el proxy. Si prefiere no dejar SSH abierto a todo Internet, acceder al VPS mediante una VPN WireGuard autohospedada permite eliminar el puerto 22 de las reglas públicas por completo:

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

Inícielo con docker compose up -d y, después, supervise docker compose logs -f app. En el primer arranque se copia todo el árbol de la aplicación en el volumen y se ejecuta el instalador; el contenedor no responde hasta que termina.

TLS y el proxy inverso

Instale nginx y certbot desde los repositorios de la distribución, cree un bloque de servidor básico en el puerto 80 con el server_name correcto y deje que certbot lo reescriba. El funcionamiento del desafío HTTP-01, el temporizador de renovación y los modos de fallo se explican en detalle en emitir certificados de Let's Encrypt con certbot y nginx en Ubuntu 24.04:

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

Certbot añade las líneas ssl_certificate y la redirección :80:443. También instala un temporizador de systemd que renueva el certificado de 90 días. Confirme que existe con systemctl list-timers | grep certbot. Un temporizador de renovación que nunca se habilitó es una cuenta atrás de 90 días.

El bloque del proxy es el siguiente:

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

En nginx 1.25 y posteriores, añada http2 on;. Ubuntu 24.04 incluye una compilación anterior, en la que el equivalente es listen 443 ssl http2;. nginx -t le indicará cuál acepta su compilación.

client_max_body_size y los tiempos de espera de lectura prolongados evitan que las cargas grandes fallen a mitad de camino. proxy_request_buffering off transmite la carga directamente en lugar de guardar primero todo el archivo en el disco del proxy.

nginx en el host es la opción más sencilla que funciona para una sola aplicación. Si Nextcloud va a compartir el VPS con otros contenedores, ejecutar Traefik como proxy inverso de Docker Compose para varias aplicaciones traslada el enrutamiento y la emisión de certificados a las etiquetas de los contenedores. Allí vuelven a aparecer las mismas consideraciones sobre client_max_body_size y los tiempos de espera, como ajustes de middleware y transporte.

proxies_trusted y overwriteprotocol

Aquí es donde fallan la mayoría de las instancias de Nextcloud autohospedadas. Los síntomas parecen no estar relacionados con la causa.

X-Forwarded-Proto: https sólo se aplica cuando la solicitud llega desde una dirección incluida en trusted_proxies. Si no se aplica, Nextcloud interpreta que la solicitud usa HTTP simple y genera URL con http://. El proxy las redirige a HTTPS. El navegador sigue la redirección y Nextcloud vuelve a generar http://. Ese es el bucle de redirección. OVERWRITEPROTOCOL: https fija el esquema en cualquier caso.

El problema de TRUSTED_PROXIES es que la dirección que ve Nextcloud no es 127.0.0.1. nginx se ejecuta en el host y se conecta a un puerto publicado. Por tanto, el contenedor ve la puerta de enlace del puente de Docker, algo incluido en 172.x. Determine la subred real:

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

Incluya esa CIDR (o la 172.16.0.0/12 que la contenga) en TRUSTED_PROXIES. Si la define con demasiada amplitud, cualquier cliente podría falsificar X-Forwarded-For. Si la define de forma incorrecta, todos los inicios de sesión parecerán proceder de la dirección de la puerta de enlace, la protección contra fuerza bruta bloqueará toda la instancia de una vez y la vista general de administración mostrará "The reverse proxy header configuration is incorrect, or you are accessing Nextcloud from a trusted proxy."

OVERWRITECLIURL es importante para el contenedor de cron, que no recibe ninguna solicitud entrante de la que pueda inferir un nombre de host. Sin este valor, las tareas en segundo plano generan enlaces a localhost y las notificaciones por correo contienen URL inutilizables.

Trabajos en segundo plano: cron, no AJAX

El ejecutor de trabajos predeterminado de Nextcloud es AJAX: los trabajos se ejecutan como efecto secundario de que alguien cargue una página. Nadie navega a las 04:00, por lo que la caducidad de la papelera, la limpieza de versiones, las vistas previas y los reintentos federados se detienen. El primer síntoma es que el directorio de datos no deja de crecer. El servicio cron anterior ejecuta el bucle oficial de /cron.sh sobre los mismos volúmenes. Indique a Nextcloud que debe esperarlo:

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

Todos los comandos de occ siguen esta estructura: docker compose exec -u www-data app php occ <command>. Conviene crear un alias.

Copias de seguridad: tres elementos o ninguno

Una copia de seguridad que sólo incluye el sistema de archivos restaura una instancia dañada. El directorio de datos contiene los bytes; Postgres contiene la caché de archivos, los recursos compartidos, los usuarios y el estado de la aplicación; config.php contiene las credenciales de la base de datos, el ID de la instancia y la sal de la contraseña. Si restaura los archivos sin la base de datos, Nextcloud no puede verlos. Si restaura la base de datos sin config.php, no puede abrir la base de datos. Si restaura una base de datos antigua sobre un directorio de datos más reciente, obtendrá recursos compartidos que apuntan a archivos que han cambiado de ubicación.

Haga una copia de seguridad de los tres elementos con la instancia detenida:

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

El modo de mantenimiento hace que el volcado y la copia de archivos sean coherentes entre sí. Si lo omite, tarde o temprano capturará una base de datos que hace referencia a un archivo al que rsync todavía no había llegado. Tenga en cuenta que el script conserva los volcados de la base de datos con marcas de tiempo, pero sólo un espejo rotatorio del directorio de datos; rsync --delete lo sobrescribe en cada ejecución. Por tanto, sólo el volcado más reciente se corresponde con la copia de archivos.

Después, saque la copia del servidor. Una copia de seguridad que reside en el mismo VPS que el sistema que protege es una copia, no una copia de seguridad. restic con almacenamiento de objetos o con un segundo host es la opción habitual, y su deduplicación gestiona el directorio de datos mucho mejor que un archivo tar nocturno. La configuración completa, desde la inicialización del repositorio hasta el temporizador nocturno y la prueba de restauración, está en copias de seguridad de VPS fuera del servidor con restic.

La restauración no consiste simplemente en invertir el proceso. Una pila iniciada desde cero ejecuta el instalador y escribe un config.php completamente nuevo, un ID de instancia y una sal de contraseña nuevos. Importar el volcado sobre esa identidad nueva deja sesiones y tokens de recursos compartidos dañados. Restaure primero la identidad anterior, en este orden:

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 reconcilia la caché de archivos con lo que realmente existe en el disco. Practique este procedimiento una vez en un VPS de reserva, antes de necesitarlo. La misma separación entre los bytes del disco y los metadatos de Postgres se aplica a cualquier otra aplicación de este tipo. Por eso una copia de seguridad de Immich que captura la biblioteca pero no la base de datos restaura una línea temporal vacía.

Actualizaciones: una versión principal cada vez

Nextcloud permite actualizar exactamente una versión principal cada vez. Saltar de 29 a 31 no falla de forma controlada: falla con Exception: Updates between multiple major versions and downgrades are unsupported. y deja el sistema en modo de mantenimiento.

El procedimiento de actualización de Docker es el siguiente: haga una copia de seguridad, cambie la etiqueta de 31 a 32 en los servicios app y cron, luego ejecute docker compose pull && docker compose up -d y después docker compose logs -f app. El punto de entrada de la imagen detecta que el código es más reciente que los datos existentes y ejecuta occ upgrade automáticamente. No lo interrumpa. Cuando los registros dejen de mostrar actividad, ejecute docker compose exec -u www-data app php occ status y compruebe versionstring y que las aplicaciones vuelvan a estar habilitadas.

Dos reglas evitan problemas: actualice una versión principal, verifique el resultado y después actualice la siguiente. Nunca cambie la etiqueta del servicio app sin cambiar también cron para que coincidan. Dos versiones diferentes de Nextcloud contra una misma base de datos pueden corromper los datos.

Los errores que realmente verá

«El directorio de datos puede ser leído por otros usuarios. Cambie los permisos a 0770.» El directorio montado mediante bind tiene activados los bits de lectura para el grupo o para todos los usuarios. sudo chmod 0770 /srv/nextcloud/data y sudo chown -R 33:33 /srv/nextcloud/data.

«El directorio de datos no es válido. Compruebe que haya un archivo llamado .ocdata en la raíz.» El bind mount apunta a una ubicación que Nextcloud nunca inicializó, hay un error tipográfico en la ruta o se sustituyó el directorio vacío por una instancia operativa. Compruebe que la ruta del host coincida con la línea del volumen.

«Acceso mediante un dominio no confiable.» El nombre de host de la petición no está incluido en trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS sólo se aplica durante la instalación inicial; después, establézcalo en caliente: occ config:system:set trusted_domains 1 --value=cloud.example.com.

502 Bad Gateway, con connect() failed (111: Connection refused) while connecting to upstream en /var/log/nginx/error.log. nginx no encontró ningún servicio escuchando en 127.0.0.1:8080. El contenedor todavía se está inicializando (compruebe docker compose logs app), se detuvo (docker compose ps) o la línea de publicación no coincide con el puerto proxy_pass. Confírmelo con ss -ltnp | grep 8080.

Un bucle de redirección o advertencias de conexión «insegura» en la vista general de administración. Falta OVERWRITEPROTOCOL: https o TRUSTED_PROXIES no contiene la subred de la puerta de enlace de Docker. Consulte la sección anterior sobre el proxy.

LockedException: "files/..." is locked. Con REDIS_HOST establecido, la imagen configura Redis como backend de bloqueo y los bloqueos obsoletos son poco frecuentes. Sin esta opción, los bloqueos se almacenan en la tabla de base de datos oc_file_locks y una petición terminada durante una escritura deja filas pendientes. Confirme que Redis se esté usando realmente: occ config:system:get memcache.locking debe devolver la clase de Redis, antes de eliminar manualmente las filas de bloqueo.

«El límite de memoria de PHP está por debajo del valor recomendado de 512MB.» Aumente PHP_MEMORY_LIMIT y vuelva a crear el contenedor. Tenga en cuenta cómo afecta esto al límite máximo en el peor caso.

Qué falla al escalar

El primer límite aparece cuando el directorio de datos supera el tamaño del volumen. Ampliar un volumen en un VPS requiere redimensionarlo y ampliar el sistema de archivos. Es mucho menos problemático programar esta tarea antes de alcanzar el 100 %. Configure ahora las alertas de uso de disco, no después.

El segundo límite es oc_filecache. Los listados de archivos y los análisis de sincronización se ralentizan a medida que aumenta el número de filas. La solución requiere trabajo en la base de datos: mantenga Postgres en almacenamiento rápido, permítale usar suficiente memoria compartida y elimine la papelera y las versiones mediante ajustes de retención, en lugar de dejar que se acumulen indefinidamente.

El tercer problema es que la generación de vistas previas compite con el resto de las tareas. En un servidor pequeño, limite los proveedores de vistas previas y no ejecute nunca occ preview:generate-all durante el horario de trabajo. Si la mayor parte de los datos almacenados procede del carrete de la cámara de un teléfono, esa generación de miniaturas debería ejecutarse en un servidor de fotos específico. Comparativa de PhotoPrism e Immich en memoria RAM, aplicaciones para teléfonos y comandos de copia de seguridad explica el coste de cada opción junto a un servidor Nextcloud.

A partir de ahí, la respuesta práctica es que los servicios adicionales necesitan su propio equipo. Collabora y la búsqueda de texto completo son servicios residentes independientes, con sus propios perfiles de memoria. Instalarlos en el mismo equipo que contiene la única copia de los archivos amplía el dominio de fallo sin aportar ninguna ventaja. Si quiere añadir la edición de documentos desde el navegador, los requisitos mínimos de RAM del proveedor y los límites de conexiones que diferencian OnlyOffice de Collabora determinan cuál de los dos puede funcionar en un VPS de 2 a 4 GB. Traslade el almacenamiento de archivos a un almacenamiento principal compatible con S3 cuando el volumen deje de tener el tamaño adecuado. Tenga en cuenta que esto dificulta las copias de seguridad, no que las simplifica: la base de datos sigue conteniendo los metadatos y debe volcarse al mismo tiempo que el bucket.

Cuando la instancia ya atienda a usuarios reales, coloque Uptime Kuma delante para recibir avisos de las interrupciones antes que los clientes de sincronización. Una nube privada combina bien con su propio servidor de correo. Si prefiere no conectar los servicios manualmente, Cloudron, CasaOS y Coolify comparan las plataformas que lo hacen por usted. Si un motor de búsqueda autoalojado es el siguiente servicio de la lista, espere un problema de otra categoría: los errores 429 de SearXNG pueden deberse a su propio limitador de tasa o a que los motores ascendentes bloquean la IP del VPS. Sólo el registro permite determinar cuál de las dos causas se aplica.

FAQ

¿Puedo ejecutar Nextcloud con SQLite en lugar de Postgres?

Sí, la imagen oficial lo permite, pero un único cliente de sincronización de escritorio que emita solicitudes en paralelo alcanzará SQLSTATE[HY000]: General error: 5 database is locked y generará errores HTTP 500. SQLite bloquea las escrituras en toda la base de datos, y Nextcloud escribe constantemente datos de bloqueo de archivos, filas de actividad y estado de tareas. Empiece con Postgres o MariaDB; occ db:convert-type existe, pero es una migración larga y completa sobre datos activos.

¿Cuánta RAM necesita realmente un VPS de Nextcloud?

Dimensione el sistema según la concurrencia, no según el número de usuarios. La memoria residente máxima aproximada es el número de solicitudes simultáneas multiplicado por PHP_MEMORY_LIMIT, más los búferes compartidos de Postgres y un backend por conexión, además del consumo máximo que alcance la generación de vistas previas. Un equipo con 2 GB puede ejecutar una instancia pequeña para un hogar si limita las vistas previas y añade swap; si añade Collabora o búsqueda de texto completo, deberá dimensionar otro conjunto de servicios residentes.

¿Por qué fallan las cargas grandes detrás del proxy inverso de nginx?

Dos opciones del proxy suelen explicarlo: client_max_body_size, que mantiene el valor predeterminado de 1 MB, trunca la solicitud, y los valores bajos de proxy_read_timeout / proxy_send_timeout interrumpen las transferencias largas a mitad del proceso. Establezca ambos valores con un margen amplio, configure proxy_request_buffering off para transmitir en lugar de almacenar temporalmente y aumente PHP_UPLOAD_LIMIT en el contenedor de la aplicación para que coincida.

¿Por qué Nextcloud redirige en un bucle o muestra una advertencia sobre el proxy inverso?

El contenedor no ve nginx en 127.0.0.1, sino la puerta de enlace de la red bridge de Docker, en algún punto de 172.x. Cuando esa dirección no aparece en TRUSTED_PROXIES, se ignora la cabecera X-Forwarded-Proto: https, Nextcloud genera URL http:// y el proxy las redirige de nuevo. Establezca TRUSTED_PROXIES en la subred bridge real y fije OVERWRITEPROTOCOL: https.

¿Puedo actualizar Nextcloud directamente de 29 a 31?

No. Nextcloud admite una versión principal por actualización, y omitir una versión detiene el proceso con Updates between multiple major versions and downgrades are unsupported., dejando la instancia en modo de mantenimiento. Haga una copia de seguridad, aumente una versión principal la etiqueta en los servicios app y cron, docker compose pull && docker compose up -d, verifique el resultado con occ status y repita el proceso.