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:
- 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). - 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
Timezonedel 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
--useren la WSL,02,08,14,20hora de Nueva York =06,12,18,00UTC. 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/.enven 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
.dmpson 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/.