2024-07-24
Montando Nextcloud en una Raspberry Pi: Docker Compose, PostgreSQL, Traefik y notificaciones push
Montando Nextcloud en una Raspberry Pi: Docker Compose, PostgreSQL, Traefik y notificaciones push
Actualización (julio 2026): dos años después este stack es bastante distinto. El borde con Traefik y puertos redirigidos que se describe aquí acabó sustituido por un túnel de Cloudflare con cero puertos abiertos. Este post se queda como registro de la construcción original. La continuación es De abrir puertos a túneles de Cloudflare.
Tras escribir sobre por qué montar un servidor doméstico, aquí va la primera pieza concreta de mi montaje: Nextcloud en una Raspberry Pi, todo en contenedores, con base de datos en serio, caché, TLS que se renueva solo y algo que casi nadie documenta bien en Docker: notificaciones push que de verdad funcionan en los clientes.
Dos guías de la comunidad marcaron el camino: esta del foro de Nextcloud para Raspberry Pi y el muy útil docker compose con notify_push de wwe. Lo de abajo es mi adaptación para Pi ARM64, más los detalles que me costaron varias tardes descubrir.
El stack de un vistazo
Un proyecto Compose, seis contenedores, cada uno con una función clara:
| Contenedor | Función |
|---|---|
app |
Nextcloud en la imagen Apache: PHP, WebDAV y el resto |
cron |
La misma imagen, otro entrypoint: tareas de fondo cada 5 minutos |
notify_push |
Demonio Client Push: sincronización en tiempo real sin polling |
db |
PostgreSQL 15, en mis pruebas más rápido que MariaDB y con actualizaciones más limpias |
redis |
Bloqueo de ficheros, caché y sesiones PHP |
imaginary |
Previsualizaciones, para que PHP no se ahogue con las fotos |
Delante va Traefik v3, terminando TLS con certificados Let's Encrypt. El router redirige 80 y 443 hacia la Pi. Un registro A en Cloudflare apunta el dominio a mi IP doméstica (dinámica, por desgracia).
Las partes interesantes del Compose
El fichero completo es largo. Aquí van las piezas que sostienen el diseño. Postgres y la cuenta de administrador reciben credenciales vía secrets de Docker: ficheros chmod 600 en disco, nunca variables de entorno.
db:
image: postgres:15
restart: unless-stopped
volumes:
- ./db:/var/lib/postgresql/data
environment:
- POSTGRES_DB_FILE=/run/secrets/postgres_db
- POSTGRES_USER_FILE=/run/secrets/postgres_user
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $(cat /run/secrets/postgres_db) -U $(cat /run/secrets/postgres_user)"]
start_period: 15s
interval: 30s
secrets:
- postgres_db
- postgres_password
- postgres_user
Los healthchecks no son adorno: app declara depends_on con condition: service_healthy, así Nextcloud no arranca contra una base que aún no acepta conexiones. Esa línea evita el clásico "Internal Server Error en el primer arranque" de los hilos del foro.
Los contenedores app y cron comparten los mismos bind mounts (./nextcloud, ./config, ./apps, ./data), de modo que una actualización de imagen mueve a los dos a la vez.
notify_push, la parte que vale la pena copiar
Sin Client Push, cada cliente consulta el servidor con un temporizador: lento y derrochador en una Pi. La app notify_push trae un demonio en Rust que mantiene un websocket con cada cliente y avisa en cuanto algo cambia. Hacerlo funcionar en Compose es lo que casi todas las guías omiten, y es justo lo que resuelve el hilo de wwe:
notify_push:
image: nextcloud:${NEXTCLOUD_VERSION}
restart: unless-stopped
depends_on:
- app
environment:
- PORT=7070
- NEXTCLOUD_URL=http://app # hablar con Nextcloud directamente, sin pasar por el proxy
entrypoint: /var/www/html/custom_apps/notify_push/bin/aarch64/notify_push /var/www/html/config/config.php
volumes:
- ./apps:/var/www/html/custom_apps
- ./config:/var/www/html/config
Tres detalles importan aquí:
- La ruta del binario depende de la arquitectura. La app trae binarios precompilados. En Pi es
bin/aarch64/notify_push. La rutax86_64de la mayoría de ejemplos falla con un críptico "no such file" dentro de un contenedor que claramente tiene ficheros. NEXTCLOUD_URL=http://appapunta el demonio al contenedor de la app por la red Docker. Mandarlo por la URL pública añadiría TLS y saltos de proxy a una conversación de loopback.- Se reiniciará en bucle al principio, y no pasa nada. El binario no existe hasta instalar la app dentro de Nextcloud. La secuencia: levantar el stack, instalar la app, reiniciar el servicio y conectarlo:
docker compose exec app php occ app:install notify_push
docker compose up -d notify_push
docker compose exec app php occ notify_push:setup https://nextcloud.example.com/push
El enrutado es lo sutil: los clientes llegan al demonio por el mismo dominio que Nextcloud, bajo /push. Traefik casa PathPrefix(/push) con prioridad de router más alta que el comodín principal, recorta el prefijo con un middleware y reenvía al puerto 7070. Si la prioridad falla, el router principal se traga /push. No hay error visible. El push simplemente no funciona.
# configuracion dinamica de traefik
http:
middlewares:
nextcloud-redirect:
redirectScheme:
scheme: https
permanent: true
secHeaders:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
nextcloud_striprefix_push:
stripPrefix:
prefixes: ["/push"]
occ notify_push:self-test confirma la cadena completa. Todos los checks en verde.
Ficheros pequeños, horas grandes de depuración
- Las sesiones PHP van en Redis. La imagen oficial guarda sesiones en disco, lo que choca con varios contenedores compartiendo el volumen. Un arreglo de un fichero montado en el directorio de configuración de PHP (contexto) apunta el almacenamiento de sesiones al servicio
redis. Los logins dejan de caducar misteriosamente. - A Apache hay que contarle lo del proxy con
remoteip. Detrás de Traefik, todas las peticiones parecen venir del bridge de Docker. Un pequeñoremoteip.confmontado en el contenedor recupera las IP reales de los clientes. Sin él, la protección anti fuerza bruta estrangula al proxy en lugar de al atacante. - HSTS y la redirección a https viven en Traefik, no en Nextcloud. Los middlewares
secHeadersyredirectSchemede arriba lo cubren. El escáner de seguridad del propio Nextcloud se pone en verde con eso. - El primer arranque tarda minutos. El contenedor dice "started" mucho antes de terminar la inicialización. La paciencia gana a recrear el stack porque la página de login no aparece a los treinta segundos.
El problema de la IP dinámica (un adelanto)
La verruga de este montaje: mi operadora rota la IP pública, y el registro A de Cloudflare tiene que seguirla. De momento, un script en un timer de systemd comprueba la IP pública a diario, actualiza el registro DNS por la API de Cloudflare cuando cambia, y resetea el almacén ACME de Traefik para emitir un certificado nuevo. Funciona, pero es la parte de la arquitectura en la que menos confío. La guía original insinúa una integración con Cloudflare Zero Trust, y sospecho que ahí acabará este servidor.
Mientras tanto: los ficheros se sincronizan, los calendarios se comparten, las fotos suben desde el móvil, las previsualizaciones vuelan gracias a imaginary, y el cliente de escritorio se actualiza en cuanto un fichero cambia en cualquier sitio. Para un ordenador del tamaño de una tarjeta de crédito metido en un cajón, es una nube personal muy completa.