Clone
1
Backups
rafa edited this page 2026-08-01 11:59:10 +00:00

Backups del servidor Hetzner

Montado el 2026-08-01. Cubre feadulta, summaraise, los dos CRM (Relaticle), gitea y el propio Coolify.

Diseño en dos capas

Capa 1 — bases de datos: feature estándar de Coolify. Scheduled Database Backups, solo a disco local del servidor, retención 7 copias. Sin S3.

Capa 2 — traslado a disco local: pull-hetzner.sh en la WSL. Un rsync sobre Tailscale que se trae los dumps y, además, los volúmenes de ficheros que Coolify no sabe backupear (es una carencia conocida del producto: solo hace backup de bases de datos, no de volúmenes persistentes).

Por qué PULL y no push

Lo lanza siempre la máquina local, nunca el servidor. Dos motivos:

  1. El PC no está siempre encendido. Con push, cada ventana con el PC apagado sería un backup fallido con alerta. Con pull, la siguiente pasada recupera sola (y el timer usa Persistent=true, así que se ejecuta al arrancar si se saltó la hora).
  2. El servidor no tiene ninguna credencial de escritura sobre el destino. feadulta ya fue comprometida una vez (incidente #183). Si vuelve a pasar, un atacante con root en Hetzner no tiene camino hacia estas copias.

Sin --delete, a propósito

El destino es aditivo. Un borrado en el servidor —accidental o malicioso— no se propaga al backup. Es lo que separa una copia de una réplica. Consecuencia esperada: el destino tiene a veces más ficheros que el origen, y eso NO es un fallo. La verificación solo salta si faltan ficheros.

Qué se copia

Origen (srv1) Destino en N:\Backup\hetzner\
/data/coolify/backups/ (dumps de capa 1, incluido el de la BD de Coolify) dumps-coolify/
volumen r2ssjifwj0r0ghyoqd528uaa_wordpress-files feadulta-wp/wordpress-files/
volumen pgjls5t2h3t1jmda6ht8tu6e_wordpress-files summaraise/wordpress-files/
volumen fr9v89w8g1nsyrc4718du5r7_wordpress-files aqtalent-web/wordpress-files/
volumen krd5enbr8iafux0rx3clc65w_gitea-data gitea/gitea-data/
volumen hqt9vm56ijhfcsxhais61yge_storage triptyk-crm/storage/
/data/coolify/ssh/ y /data/coolify/proxy/ coolify-config/

Fuera del backup a propósito: /data/feadulta-antiguo (4,9 GB del mirror estático) es regenerable desde la WSL con 80-sync-hetzner.sh. El redis del CRM es caché y Coolify no lo soporta.

Tamaño total: 7,0 GB. Primera pasada 20 min; las incrementales, 4 min.

Programación

  • Dumps (Coolify): los crons se interpretan en la zona del servidor, que está en UTC (el campo Timezone del formulario está disabled, es informativo). summaraise 02:15, feadulta 04:00, aqtalent 04:30, CRM 04:45, gitea 05:00, BD de Coolify 05:15 — todo UTC.
  • Pull: timer de systemd --user en la WSL, 02,08,14,20 hora de Nueva York = 06,12,18,00 UTC. La pasada de las 06:00 UTC recoge todos los dumps de la madrugada.
  • Alerta: OnFailure=avisa-telegram.sh, que lee las credenciales de ~/.hermes/.env en el momento de usarlas.

Verificado el 2026-08-01

Restauración real del dump de feadulta en un MySQL 8 desechable, comparada contra producción: 43.947 posts / 29.486 publicados / 986 options / 1.228 usuarios / 135.928 postmeta / 54 tablas, idéntico en ambos lados. Import en 94 s. Acentos correctos, 0 mojibake, y wordfence_core_options (el payload del atacante del #183) ausente del dump.

Trampas encontradas — no volver a descubrirlas

NTFS no distingue mayúsculas y se come ficheros en silencio. En uploads/Musica/ conviven aleluya.mp3 y Aleluya.mp3. En el primer intento faltaban 18 ficheros en el destino y rsync salió con código 0 sin una sola queja. De esos 18 pares, 4 eran ficheros realmente distintos. Arreglado con:

fsutil file setCaseSensitiveInfo "N:\Backup\hetzner" enable

(requiere PowerShell como administrador). El atributo lo heredan los subdirectorios que se creen después, así que hay que ponerlo con la carpeta vacía y luego copiar. Verificar que funciona creando Foo.txt y foo.txt en un subdirectorio anidado nuevo, no fiarse del mensaje del comando.

Los flags de rsync importan sobre DrvFs. Hay que usar -rlt --modify-window=1 y no -a: NTFS no guarda permisos POSIX, así que con -a rsync cree que todo cambió y recopia los 7 GB en cada pasada. Con estos flags, la segunda pasada transfiere 0 ficheros.

Coolify no ofrece "Backup All Databases" para bases de datos dentro de un servicio. El bloque de la vista solo se renderiza para StandaloneMysql / StandalonePostgresql / StandaloneMariadb. Consecuencia: los dumps de MySQL/MariaDB van sin comprimir y mysqldump corre sin --single-transaction, o sea bloqueando tablas mientras vuelca (segundos, de madrugada). Postgres no está afectado: usa pg_dump --format=custom, ya comprimido.

Retención 0/0/0 significa "no borrar nunca". Verificado en bootstrap/helpers/databases.php: si los tres criterios están a cero, la función devuelve una colección vacía y no se limpia nada.

La API de Coolify no sirve para esto. /api/v1/databases devuelve [] y GET /databases/{uuid}/backups con el uuid de una BD de servicio responde Database not found: solo maneja bases de datos standalone. Los backups hay que crearlos en el panel, igual que pasa con el FQDN de un servicio.

El aviso rojo "No validated S3 Storages found." no bloquea. Es informativo; saveToS3 es false por defecto y la validación solo exige frequency.

mysqladmin ping miente durante la inicialización de MySQL. Responde "alive" incluso con Access denied mientras el entrypoint todavía está arrancando. Para esperar a que un MySQL esté listo de verdad hay que hacer una consulta real (select 1).

Pendiente

  • No es 3-2-1: hay copia en el servidor y en casa, pero nada fuera de casa. Los dumps de BD pesan poco (~240 MB); un tercer destino barato solo para ellos sería el siguiente paso.
  • Rotación local: el destino crece sin límite (los .dmp son ficheros nuevos cada día). Con 132 GB libres en N: hay margen de sobra para un buen tiempo, pero conviene una poda.
  • Los dumps de MySQL en N: se podrían comprimir tras la copia (~80% de ahorro).

Ficheros

Todo en ~/backup-hetzner/ de la WSL: pull-hetzner.sh, avisa-telegram.sh, lib.sh y las unidades de systemd en systemd/.