Copia de seguridad y restauración
La copia de seguridad de Notarium es un comando integrado de la imagen, no una copia de archivos hecha desde fuera. notarium backup arma un ZIP lógico mientras el servicio sigue atendiendo peticiones: la lectura permanece disponible en todo momento y la escritura solo se retiene durante dos puntos de control muy breves. Basta con Docker y un contenedor en marcha: no hay que parar nada.
meta.db en usocp /data/meta.db no es una copia de seguridad. La base de datos de metadatos funciona en modo WAL: puede haber filas ya confirmadas que sigan en meta.db-wal, y unos archivos copiados uno a uno no son una instantánea de un mismo instante. Una instancia restaurada a partir de esa copia pierde datos en silencio.
Hacer una copia de seguridad
docker compose exec -T notarium backup > notarium-20260731.zip
Esto es una copia de seguridad de verdad, no una prueba de «a ver si funciona»: el comando construye el archivo, lo pasa por su propio verificador y solo entonces transmite los bytes. El flag -T es obligatorio: sin él, Compose asigna un pseudoterminal, un flujo binario no lo atraviesa intacto y acabas con un archivo corrupto. docker exec a secas no asigna pseudoterminal por defecto, así que ahí el flag no hace falta. La salida estándar está reservada estrictamente a los bytes del ZIP; el progreso y el resumen final van a stderr y nunca ensucian el archivo.
Para un trabajo programado
La redirección con > tiene una trampa: el archivo con su nombre definitivo se crea antes de que el comando haga nada. Si Docker o la copia se caen a medio camino, te queda un archivo con el nombre correcto y contenido incompleto. Abajo tienes una secuencia de publicación segura ante fallos: escribir en un archivo temporal, volcarlo a disco y publicarlo con un enlace duro atómico.
backup="notarium-$(date -u +%Y%m%dT%H%M%SZ).zip"
partial="${backup}.partial.$$"
set -eu
umask 077
committed=0
cleanup() { test "$committed" -eq 1 || rm -f "$partial"; }
trap cleanup EXIT
docker compose exec -T notarium backup > "$partial"
sync -f "$partial"
sync -f "$(dirname "$backup")"
committed=1
ln "$partial" "$backup"
if sync -f "$(dirname "$backup")"; then
if rm "$partial"; then
sync -f "$(dirname "$backup")" ||
echo "backup warning: final is durable; partial cleanup fsync failed" >&2
else
echo "backup warning: final is durable; retaining recovery partial $partial" >&2
fi
else
echo "backup warning: final is visible; retaining durable recovery partial $partial" >&2
fi
trap - EXIT
set -e impide que el ZIP se publique si Docker o la copia devolvieron un error. El nombre temporal lleva el PID, así que dos trabajos simultáneos no se pelearán por el mismo archivo. El archivo temporal y su directorio se vuelcan a disco antes del punto de publicación, y la publicación en sí es un enlace duro atómico que nunca sobrescribe: dos trabajos que apunten al mismo nombre de destino no pueden pisarse.
Los fallos posteriores al punto de publicación son advertencias, no falsas alarmas: el archivo final ya está en su sitio y, en los casos dudosos, el temporal se conserva como copia de repuesto. Incluso un código de salida ambiguo y distinto de cero de ln la conserva: el enlace pudo crearse justo antes de que se interrumpiera el proceso. umask 077 hace que el archivo solo lo pueda leer su propietario. Mantén el temporal y el final en el mismo sistema de archivos.
Para un contenedor levantado con docker run a secas y con el nombre notarium, todo lo anterior sirve igual; solo cambia una línea:
docker exec notarium backup > "$partial"
La envoltura de arriba existe porque el transporte por stdout no puede publicar un archivo por sí mismo. Cuando el directorio de copias ya está montado —un recurso compartido de copias, NFS, un volumen temporal junto a una raíz de solo lectura—, backup --output /path/archive.zip hace el mismo trabajo: escribe en un archivo temporal junto al de destino, lo vuelca a disco, lo verifica y lo publica con un enlace duro atómico que nunca sobrescribe; si algo falla, no deja nada bajo el nombre de destino. Entonces no hace falta ninguna envoltura de shell, y por stdout sale un único resumen JSON.
El comando se lanza con docker exec dentro del contenedor que ya está sirviendo, no como un contenedor aparte: para tomar una instantánea consistente hay que coordinarse con la aplicación en ejecución.
Cuándo puede fallar una copia de seguridad
La copia está construida de forma que nunca acabes en silencio con un archivo inconsistente: si no se puede tomar la instantánea, el comando termina con error y no publica nada. Ocurre en dos casos:
- Un flujo continuo de ediciones. Mientras se arma el archivo, los datos tienen que quedarse quietos; una escritura que se solape provoca un reintento y, ante un flujo interminable de ediciones, el comando se rinde con un error. En la práctica lo verás en una instancia con mucha actividad: basta con reintentar más tarde.
- Hay una importación o exportación larga en curso. La copia necesita un par de pausas muy breves en la escritura, y un trabajo largo no cabe en ellas. No programes una copia en la misma ventana que una importación masiva.
Ninguno de los dos casos perjudica al servicio: la cola de escritura se libera de inmediato y el comando del operador nunca deja la aplicación en espera. En cualquiera de los casos, la lectura normal sigue disponible durante toda la copia.
Qué hay dentro del archivo
| Ruta en el archivo | Qué es |
|---|---|
data/meta.db | Cuentas, sesiones, membresía, identificadores estables, historial de versiones y estado de los trabajos. |
data/spaces/ | La fuente de verdad en Markdown, incluida la memoria del agente y los archivos marcadores de proyecto. |
data/jobs/ | Artefactos terminados y subidas durables de importación. |
manifest.json | Versión del formato, marca de tiempo, el conjunto exacto de directorios, más el tamaño, el mtime y el SHA-256 de cada archivo. |
El directorio derivado data/engine/ no se incluye: los índices se reconstruyen a partir de los archivos tras la restauración. De los archivos incompletos solo se omiten los internos: los temporales de la escritura atómica de notas, las importaciones subidas a medias y los fragmentos de artefactos de exportación. Los archivos de usuario normales cuyo nombre acaba en .part se quedan en el archivo de copia de seguridad: son legítimos.
El ZIP guarda cuentas y estado de sesión de la base de datos de metadatos. Trátalo como un secreto: el umask 077 del fragmento de arriba hace que el archivo nuevo solo lo pueda leer su propietario.
Verificación
La verificación no cambia nada y debe formar parte de todo trabajo de copia de seguridad:
docker compose exec -T notarium backup verify < notarium-20260722.zip
# para docker run a secas:
docker exec -i notarium backup verify < notarium-20260722.zip
Si todo va bien, el comando imprime un único resumen JSON y termina con código cero. Se rechazan las rutas inseguras y duplicadas, los archivos que no figuran en el manifiesto, un conjunto de directorios que no coincide exactamente, las discrepancias de tamaño y de hash, los metadatos de tiempo inválidos, los límites superados y una comprobación de integridad de SQLite no superada. El directorio de datos en uso ni se lee ni se modifica en el proceso.
Los hashes detectan la corrupción accidental, pero no protegen frente a la manipulación: quien pueda sustituir a la vez el contenido y el manifiesto pasará la verificación. Trata el almacén de copias como estado de confianza con acceso restringido, o añade firma o cifrado en la capa que se encargue de mover el archivo.
Restauración
La restauración es una operación de emergencia y fuera de línea. Solo acepta una raíz de datos limpia y vacía, y nunca se fusiona con una instancia existente ni la sobrescribe.
Prepara un volumen nuevo y apunta el servicio hacia él antes de empezar:
set -eu
docker compose stop notarium
# aparta el volumen antiguo; monta un /data vacío en compose
docker compose run --rm --no-deps -T notarium restore \
< notarium-20260722.zip
docker compose up -d --force-recreate --no-deps notarium
Después hay que recrear el contenedor, no solo arrancarlo: docker compose start levantaría el contenedor antiguo con su antigua configuración de montajes. Conserva el volumen viejo hasta que hayas comprobado la instancia restaurada.
La restauración verifica el archivo entero antes de instalar nada. Si el proceso se interrumpe a mitad de la instalación, queda un marcador explícito: considera ese destino de un solo uso y restaura en uno nuevo y vacío, en lugar de insistir o fusionar.
Qué comprobar después de restaurar:
- Inicia sesión con una cuenta de la copia.
- Abre varios espacios y confirma que las direcciones y los identificadores siguen en su sitio.
- Abre una nota que hubieras editado y mira su historial.
- Revisa los trabajos de importación y exportación cuyas subidas o artefactos te importen.
La base de datos de metadatos que restauras debe llevar un registro de migraciones que la compilación de destino acepte. Una base de datos no vacía y sin ese registro falla en modo fail-closed: la restauración no adivina su versión ni le pone una marca por su cuenta. Consulta Base de datos.
Límites
El comando integrado solo admite la disposición canónica: una única raíz de datos y una base de datos de metadatos en un archivo SQLite. Si META_DB_URL apunta a Postgres, o las notas viven fuera de DATA_DIR, el comando falla en modo fail-closed en lugar de entregarte un archivo parcial: en ese caso, usa las herramientas propias de tu base de datos junto con instantáneas de los directorios montados.
Los archivos intermedios de la copia y de la verificación viven por defecto en /tmp. Una copia de seguridad en streaming se verifica a sí misma antes de publicar, así que puede necesitar temporalmente espacio para el archivo comprimido más dos etapas expandidas; una verificación por separado necesita el archivo comprimido más una. La restauración almacena en búfer el flujo entrante en el espacio de trabajo temporal, pero expande el archivo comprimido directamente en la propia raíz de datos nueva. Si el sistema de archivos raíz del contenedor está montado en solo lectura, o si tienes muchos datos, apunta NOTARIUM_BACKUP_TMPDIR a un directorio montado con permiso de escritura.
La entrada, comprimida y expandida, está limitada a 64 GiB y un millón de entradas; los nombres, las estructuras internas del ZIP y manifest.json tienen un límite de memoria aparte de 32 MiB. Las instalaciones grandes y de confianza pueden subir NOTARIUM_BACKUP_MAX_BYTES, NOTARIUM_BACKUP_MAX_ENTRIES y NOTARIUM_BACKUP_MAX_METADATA_BYTES; consulta Variables de entorno.
Siguiente
- CLI de la imagen — el contrato completo de comandos, flujos y códigos de salida.
- Base de datos — qué guarda exactamente la base de datos de metadatos y por qué nunca puede quedarse fuera de una copia de seguridad.
- Producción — proxy inverso, el invariante de instancia única, la operación del día a día.