instalar nextcloud en vps con docker
Guía para desplegar Nextcloud con Docker Compose, Postgres y Redis. Incluye configuración de TLS con nginx y estrategias de backup para evitar pérdida de datos.
Lo que realmente vas a construir
Esta guía ejecuta Nextcloud en un VPS con Docker Compose, implementa TLS de Let's Encrypt como proxy inverso y configura un respaldo que permite la restauración real. Se utilizan cuatro contenedores y un proxy: la imagen oficial de nextcloud escuchando en loopback, Postgres para los metadatos de los archivos, Redis para los bloqueos de archivos, una segunda copia de la imagen de Nextcloud ejecutando únicamente el bucle cron, y nginx en el host para la terminación TLS. La instalación tarda veinte minutos, pero eso no es lo importante. Dos decisiones tomadas en la primera hora determinarán si conservas tus archivos en un año: usar una base de datos real en lugar de SQLite, y un respaldo que capture el directorio de datos, la base de datos y config.php como un conjunto consistente.
Se asume el uso de Ubuntu 24.04 LTS o Debian 13, Docker Engine con el plugin Compose v2 instalado desde el repositorio oficial de Docker, y un registro DNS A (más AAAA si usas IPv6) apuntando ya cloud.example.com al VPS. Todo el proceso requiere un servidor bajo tu control; no es posible realizar la terminación TLS y un volcado de base de datos en un servicio SaaS externo.
Dimensionamiento: qué consume realmente la memoria
El uso de memoria de Nextcloud está dominado por tres factores, y ninguno es "Nextcloud" como tal.
PHP workers. La imagen -apache atiende cada solicitud concurrente mediante un proceso worker que contiene un intérprete PHP. Cada worker puede crecer hasta PHP_MEMORY_LIMIT antes de que PHP finalice la solicitud. El peor escenario de memoria residente es aproximadamente solicitudes concurrentes × el límite de memoria; un cliente de sincronización de escritorio abre varias conexiones paralelas por usuario. La concurrencia, y no el número de usuarios, establece el límite máximo.
La base de datos. Postgres crea un proceso backend por cada conexión y mantiene los shared buffers en memoria residente. Su conjunto de trabajo escala según el número de archivos, no según el número de bytes: oc_filecache contiene una fila por cada archivo por usuario. Cien mil archivos pequeños generan una base de datos más pesada que cien archivos grandes.
Generación de previews. Generar una miniatura decodifica la imagen original en memoria a resolución completa. Las previews de video ejecutan comandos de ffmpeg. Ejecutar occ preview:generate-all provoca este pico de consumo repetidamente y de forma consecutiva; es la causa más común de errores OOM killer en un VPS pequeño.
Redis es comparativamente ligero. Cualquier componente adicional que instale más tarde —Collabora, búsqueda de texto completo, un antivirus— es un servicio residente independiente con su propio consumo de memoria, y debe incluirse en su plan de dimensionamiento antes de activarlo.
Si tiene poca RAM, estas son las opciones: reduzca PHP_MEMORY_LIMIT, limite preview_max_x / preview_max_y / preview_max_filesize_image, reduzca enabledPreviewProviders a los formatos que realmente visualiza, y configure trashbin_retention_obligation y versions_retention_obligation para que el directorio de datos no crezca silenciosamente hasta alcanzar varias veces el tamaño de sus archivos. Añada un archivo swap. El swap es lento, pero un error OOM durante una actualización es peor.
Por qué falla SQLite
Nextcloud incluye soporte para SQLite y la imagen oficial lo utilizará por defecto. No lo haga. SQLite serializa las escrituras mediante un bloqueo de toda la base de datos: un solo escritor a la vez para todo el archivo. Nextcloud realiza escrituras constantes —bloqueos de archivos, filas de actividad, entradas de caché, estado de tareas— y un único cliente de escritorio sincronizando un árbol de directorios genera múltiples peticiones paralelas. Bajo ese patrón se producen errores SQLSTATE[HY000]: General error: 5 database is locked y errores HTTP 500; el fallo ocurre precisamente cuando la instancia comienza a ser útil.
Es posible realizar la conversión posteriormente con occ db:convert-type, pero es una migración larga y absoluta sobre un conjunto de datos en vivo. Comience con Postgres o MariaDB.
El archivo Compose
Guarde esto en /srv/nextcloud/compose.yaml, con los secrets en un archivo .env hermano en 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 verifique la etiqueta actual en Docker Hub antes de copiar 31 textualmente. latest le cambiará a una versión mayor 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, por diseño: una ruta a la que pueda apuntar directamente una herramienta de backup es más útil que el orden. Créelo con el UID www-data de la imagen y los permisos que Nextcloud requiere:
sudo mkdir -p /srv/nextcloud/data
sudo chown -R 33:33 /srv/nextcloud/data
sudo chmod 0770 /srv/nextcloud/dataObserve la publicación del puerto: 127.0.0.1:8080:80. Docker publica puertos escribiendo reglas DNAT que se evalúan antes de que la cadena INPUT de ufw vea el paquete; un 8080:80 simple deja un Nextcloud sin cifrar en internet público sin importar la configuración de ufw. Vincularlo al loopback lo mantiene fuera de la interfaz pública. Así, el firewall solo debe permitir el proxy —y si prefiere no dejar SSH abierto a todo internet, acceder al VPS mediante una VPN WireGuard propia le 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 enableInícielo con docker compose up -d y luego observe docker compose logs -f app. El primer arranque copia todo el árbol de la aplicación en el volumen y ejecuta el instalador; el contenedor no responderá nada hasta que esto termine.
TLS y el proxy inverso
Instale nginx y certbot desde su distribución, cree un server block estándar en el puerto 80 con el server_name correcto y luego permita que certbot lo reescriba. El funcionamiento del desafío HTTP-01, el temporizador de renovación y los modos de error se explican detalladamente en emisión de certificados 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.comCertbot añade las líneas ssl_certificate y la redirección :80 → :443, e instala un timer de systemd para renovar el certificado cada 90 días. Confirme su existencia con systemctl list-timers | grep certbot; un timer de renovación que no se haya habilitado es una bomba de tiempo de 90 días.
El bloque del 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;
}
}En nginx 1.25 o versiones superiores, añada http2 on;. Ubuntu 24.04 incluye una versión anterior donde el equivalente es listen 443 ssl http2;. nginx -t le indicará cuál acepta su versión.
client_max_body_size y los timeouts de lectura largos evitan que las subidas de archivos grandes fallen a la mitad. proxy_request_buffering off transmite la subida directamente en lugar de guardar todo el archivo en el disco del proxy primero.
Usar nginx en el host es la opción más sencilla para una sola aplicación. Si Nextcloud va a compartir el VPS con otros contenedores, ejecución de Traefik como proxy inverso de Docker Compose para múltiples aplicaciones traslada el enrutamiento y la emisión de certificados a etiquetas de contenedor; allí reaparecen las mismas preocupaciones de client_max_body_size y timeouts mediante configuraciones de middleware y transporte.
trusted_proxies y overwriteprotocol
Aquí es donde fallan la mayoría de las instancias de Nextcloud auto-alojadas, y los síntomas parecen no tener relación con la causa.
X-Forwarded-Proto: https solo se aplica cuando la solicitud proviene de una dirección listada en trusted_proxies. Cuando no se aplica, Nextcloud cree que la solicitud es HTTP simple y genera URLs con http://; el proxy las redirige a HTTPS; el navegador sigue la redirección; Nextcloud genera http:// nuevamente. Ese es el bucle de redirección. OVERWRITEPROTOCOL: https fija el protocolo sin importar lo anterior.
El problema en TRUSTED_PROXIES es que la dirección que Nextcloud ve no es 127.0.0.1. nginx se ejecuta en el host y se conecta a un puerto publicado, por lo que el contenedor ve la puerta de enlace (gateway) del bridge de Docker — algo en 172.x. Encuentra la subred real:
docker network inspect nextcloud_default \
-f '{{range .IPAM.Config}}{{.Subnet}}{{end}}'Coloca ese CIDR (o el 172.16.0.0/12 correspondiente) en TRUSTED_PROXIES. Si lo configuras de forma demasiado amplia, cualquier cliente podría suplantar a X-Forwarded-For; si lo configuras incorrectamente, cada inicio de sesión parecerá provenir de la dirección del gateway, la protección contra fuerza bruta bloqueará toda la instancia a la vez, y el resumen 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 cron, que no recibe solicitudes entrantes para inferir un hostname. Sin esto, los trabajos en segundo plano generan enlaces a localhost y las notificaciones por correo electrónico envían URLs inutilizables.
Trabajos en segundo plano: cron, no AJAX
El ejecutor de tareas predeterminado de Nextcloud es AJAX: las tareas se ejecutan como un efecto secundario de la carga de una página por parte de un usuario. Nadie navega a las 04:00, por lo que la expiración de la papelera, la limpieza de versiones, las vistas previas y los reintentos federados se detienen. El primer síntoma es un directorio de datos que crece sin control. El servicio cron mencionado anteriormente ejecuta el bucle oficial de /cron.sh sobre los mismos volúmenes. Configure Nextcloud para que lo utilice:
docker compose exec -u www-data app php occ background:cronCada comando occ sigue este formato: docker compose exec -u www-data app php occ <command>. Se recomienda crear un alias.
Backups: tres elementos, o ninguno
Un backup solo del filesystem no sirve para restaurar una instancia dañada. El directorio de datos contiene los bytes; Postgres contiene el cache de archivos, los shares, los usuarios y el estado de la aplicación; config.php contiene las credenciales de la base de datos, el instance ID y el password salt. Si restauras los archivos sin la base de datos, Nextcloud no podrá verlos. Si restauras la base de datos sin config.php, no podrá abrir la base de datos. Si restauras una base de datos antigua en un directorio de datos más reciente, los shares apuntarán a archivos que han cambiado de ubicación.
Realiza el backup de los tres elementos desde una instancia en estado quiesced:
#!/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 maintenance mode garantiza la consistencia entre el dump y la copia de archivos. Si se omite, eventualmente se capturará una base de datos que referencia un archivo que rsync aún no ha procesado. El script conserva los database dumps con timestamp, pero solo mantiene un mirror rotativo del directorio de datos — rsync --delete lo sobrescribe en cada ejecución — por lo que solo el dump más reciente es compatible con la copia de archivos.
Luego, extrae el backup del servidor. Un backup almacenado en el mismo VPS que el sistema original es una copia, no un backup. La solución habitual es usar restic contra object storage o un segundo host; su deduplicación gestiona el directorio de datos mucho mejor que un tarball nocturno. La configuración completa, desde la inicialización del repositorio hasta el timer nocturno y el simulacro de restauración, está en backups de VPS fuera del host con restic.
La restauración no es simplemente el proceso inverso. Una pila recién iniciada ejecuta el installer y genera un config.php nuevo — un nuevo instance ID y password salt — e importar el dump sobre esa nueva identidad genera sesiones y tokens de share rotos. Restaura primero la identidad antigua, 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 --allfiles:scan sincroniza el file cache con lo que realmente existe en el disco. Practica este proceso una vez en un VPS de prueba antes de que lo necesites.
Actualizaciones: una versión mayor a la vez
Nextcloud solo admite la actualización de una versión mayor a la vez. Saltar de la 29 a la 31 no es un proceso seguro; el sistema falla con Exception: Updates between multiple major versions and downgrades are unsupported. y queda en modo de mantenimiento.
La actualización en Docker consiste en: realizar un backup, cambiar la etiqueta de 31 a 32 en los servicios app y cron, luego ejecutar docker compose pull && docker compose up -d y después docker compose logs -f app. El entrypoint de la imagen detecta el código nuevo frente a los datos existentes y ejecuta occ upgrade automáticamente. No interrumpas el proceso. Cuando los logs dejen de mostrar actividad, ejecuta docker compose exec -u www-data app php occ status y verifica versionstring y que las apps estén habilitadas nuevamente.
Dos reglas para evitar errores: actualiza una versión mayor, verifica el estado, y luego actualiza la siguiente. Nunca cambies la etiqueta en el servicio app sin cambiar también cron para que coincidan; usar dos versiones distintas de Nextcloud con una misma base de datos causará corrupción de datos.
Los errores que realmente verás
"Your data directory is readable by other users. Please change the permissions to 0770." El directorio montado mediante bind-mount tiene permisos de lectura para el grupo o para otros usuarios. sudo chmod 0770 /srv/nextcloud/data y sudo chown -R 33:33 /srv/nextcloud/data.
"Your data directory is invalid. Ensure there is a file called .ocdata in the root." El bind mount apunta a una ubicación que Nextcloud nunca inicializó; puede ser un error tipográfico en la ruta o un directorio vacío reemplazando a una instancia funcional. Verifique que la ruta del host coincida con la línea del volume.
"Access through untrusted domain." El hostname de la solicitud no está en trusted_domains. NEXTCLOUD_TRUSTED_DOMAINS solo aplica durante la instalación inicial; después, configúrelo en vivo: 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 pudo conectar con nada en 127.0.0.1:8080. El contenedor aún se está inicializando (verifique docker compose logs app), el contenedor finalizó (docker compose ps), o la línea de publish no coincide con el puerto proxy_pass. Confirme con ss -ltnp | grep 8080.
Un bucle de redireccionamiento, o advertencias de "insecure" en el resumen de administración. Falta OVERWRITEPROTOCOL: https, o TRUSTED_PROXIES no contiene la subred del gateway de Docker. Consulte la sección de proxy anterior.
LockedException: "files/..." is locked. Con REDIS_HOST configurado, la imagen utiliza Redis como backend de locking y los bloqueos obsoletos son poco comunes. Sin esto, los bloqueos se guardan en la tabla de la base de datos oc_file_locks y una solicitud interrumpida durante la escritura deja filas residuales. Confirme que Redis esté en uso — occ config:system:get memcache.locking debe devolver la clase Redis — antes de intentar borrar filas de bloqueo manualmente.
"The PHP memory limit is below the recommended value of 512MB." Aumente PHP_MEMORY_LIMIT y recree el contenedor. Tenga en cuenta el impacto en su límite máximo de consumo.
Qué falla al escalar
El primer límite es que el directorio de datos supere el tamaño del volumen. Redimensionar un volumen en un VPS requiere un resize y un filesystem grow; es mucho más difícil de programar cuando el disco está al 100% de capacidad. Configure alertas de uso de disco ahora, no después.
El segundo límite es oc_filecache. El listado de archivos y los escaneos de sincronización se ralentizan al aumentar el número de filas. La solución requiere mantenimiento de la base de datos: mantenga Postgres en almacenamiento rápido, asigne suficiente shared memory y elimine archivos basura y versiones mediante configuraciones de retención en lugar de permitir que se acumulen indefinidamente.
El tercero es la generación de vistas previas compitiendo con otros procesos. En un servidor pequeño, limite los proveedores de vistas previas y nunca ejecute occ preview:generate-all durante el horario laboral.
Más allá de eso, la respuesta honesta es que los servicios adicionales requieren su propia máquina. Collabora y la búsqueda de texto completo son servicios residentes independientes con perfiles de memoria propios. Instalarlos en el mismo servidor que contiene su única copia de archivos aumenta el radio de fallo sin ofrecer beneficios. Mueva el almacenamiento de archivos a un almacenamiento primario compatible con S3 cuando el volumen deje de ser adecuado; tenga en cuenta que esto complica los backups: la base de datos sigue conteniendo los metadatos y debe realizarse un dump en sincronía con el bucket.
Cuando la instancia sirva a usuarios reales, coloque Uptime Kuma delante de ella para detectar el tiempo de inactividad antes que los clientes de sincronización. Una nube privada funciona bien con su propio servidor de correo y, si prefiere no configurar los servicios manualmente, Cloudron, CasaOS y Coolify son plataformas que lo hacen por usted.
FAQ
¿Puedo ejecutar Nextcloud con SQLite en lugar de Postgres?
Es posible y la imagen oficial lo permite, pero un cliente de sincronización de escritorio con peticiones paralelas causará errores SQLSTATE[HY000]: General error: 5 database is locked y HTTP 500. SQLite aplica un bloqueo de escritura en toda la base de datos y Nextcloud realiza escrituras constantes (bloqueos de archivos, filas de actividad, estado de tareas). Comience con Postgres o MariaDB; occ db:convert-type existe, pero es una migración de datos en vivo larga y de todo o nada.
¿Cuánta RAM necesita realmente un VPS para Nextcloud?
Calcule el tamaño según la concurrencia, no según el número de usuarios. El consumo de memoria residente en el peor de los casos es aproximadamente el número de peticiones concurrentes multiplicado por PHP_MEMORY_LIMIT, más los shared buffers de Postgres y un proceso backend por conexión, más los picos de generación de previsualizaciones. Un servidor de 2 GB funciona para una instancia doméstica pequeña si limita las previsualizaciones y añade swap; si añade Collabora o búsqueda de texto completo, debe dimensionar un segundo conjunto de servicios residentes.
¿Por qué fallan las subidas grandes detrás del proxy inverso nginx?
Dos configuraciones en el proxy suelen ser la causa: client_max_body_size en su valor predeterminado de 1 MB trunca la petición, y valores cortos de proxy_read_timeout / proxy_send_timeout interrumpen las transferencias largas. Configure ambos con valores generosos, establezca proxy_request_buffering off en stream en lugar de spool, y aumente PHP_UPLOAD_LIMIT en el contenedor de la aplicación para que coincida.
¿Por qué Nextcloud entra en un bucle de redireccionamiento o advierte sobre el proxy inverso?
El contenedor no ve a nginx en 127.0.0.1, sino que ve la puerta de enlace del bridge de Docker en el rango 172.x. Si esa dirección no está en TRUSTED_PROXIES, se ignora el encabezado X-Forwarded-Proto: https, Nextcloud genera URLs con http:// y el proxy las devuelve en bucle. Configure TRUSTED_PROXIES con la subred real del bridge y fije OVERWRITEPROTOCOL: https.
¿Puedo actualizar Nextcloud de la versión 29 directamente a la 31?
No. Nextcloud solo admite una versión principal por actualización; saltar versiones se detiene en Updates between multiple major versions and downgrades are unsupported. y deja la instancia en modo mantenimiento. Realice una copia de seguridad, actualice la etiqueta una versión principal tanto en los servicios app como cron, ejecute docker compose pull && docker compose up -d, verifique con occ status y repita el proceso.