El backup más peligroso es el que crees que tienes pero nunca has probado. En nuestra experiencia dando soporte técnico a empresas, el escenario más común no es que no tengan backups — es que los configuraron una vez, nunca los probaron y cuando los necesitaron, estaban corruptos, incompletos o eran de hace tres meses.
En esta guía vas a crear un sistema de backup automatizado con Python que respalda bases de datos y archivos, comprime todo, sube a un destino remoto, limpia respaldos antiguos y te notifica si algo falla.
Requisitos previos
Necesitas un servidor Linux con Python 3.10+ y acceso a las bases de datos que quieres respaldar. Vamos a usar solo librerías estándar de Python más pg_dump para PostgreSQL.
# Verificar Python
python3 --version
# Verificar pg_dump (para backups de PostgreSQL)
pg_dump --version
Estructura del proyecto
backup/
├── backup.py # Script principal
├── config.py # Configuración
├── .env # Credenciales (no se sube a git)
└── logs/ # Logs de ejecución
¿Por qué Python y no bash?
Para un pg_dump + tar simple, bash es suficiente. Usamos Python cuando necesitas manejo robusto de errores, notificaciones, rotación inteligente, subida a S3 y logs estructurados. Además, es más fácil de extender conforme crecen tus necesidades.
Paso 1: Configuración
Primero, el archivo de configuración que define qué respaldar y dónde guardarlo:
# config.py
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
# Directorio de backups
BACKUP_DIR = Path(os.getenv("BACKUP_DIR", "/var/backups/app"))
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
# Retención: cuántos días conservar los backups
RETENTION_DAYS = int(os.getenv("RETENTION_DAYS", "30"))
# PostgreSQL
PG_HOST = os.getenv("PG_HOST", "localhost")
PG_PORT = os.getenv("PG_PORT", "5432")
PG_USER = os.getenv("PG_USER", "postgres")
PG_PASSWORD = os.getenv("PG_PASSWORD", "")
PG_DATABASES = os.getenv("PG_DATABASES", "miapp").split(",")
# Directorios de archivos a respaldar
FILE_DIRS = [
"/var/www/app/uploads",
"/etc/nginx/sites-enabled",
"/etc/postgresql/16/main",
]
# Notificaciones (Slack webhook, opcional)
SLACK_WEBHOOK = os.getenv("SLACK_WEBHOOK", "")
El archivo .env con las credenciales:
# .env
PG_HOST=localhost
PG_USER=app
PG_PASSWORD=tu_contraseña_segura
PG_DATABASES=miapp,otrabase
BACKUP_DIR=/var/backups/app
RETENTION_DAYS=30
SLACK_WEBHOOK=https://hooks.slack.com/services/xxx/yyy/zzz
Paso 2: Script principal de backup
El script que ejecuta todo el proceso:
#!/usr/bin/env python3
"""
backup.py — Backup automatizado de bases de datos y archivos.
Uso: python3 backup.py
"""
import subprocess
import tarfile
import logging
import json
from datetime import datetime, timedelta
from pathlib import Path
from urllib.request import Request, urlopen
import config
# --- Logging ---
log_file = config.BACKUP_DIR / "logs" / f"backup-{datetime.now():%Y-%m-%d}.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler(),
],
)
log = logging.getLogger(__name__)
# --- Timestamp ---
TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
BACKUP_SUBDIR = config.BACKUP_DIR / TIMESTAMP
BACKUP_SUBDIR.mkdir(parents=True, exist_ok=True)
def backup_postgresql():
"""Respalda cada base de datos PostgreSQL con pg_dump."""
log.info("Iniciando backup de PostgreSQL...")
dumps = []
for db in config.PG_DATABASES:
db = db.strip()
dump_file = BACKUP_SUBDIR / f"{db}.sql.gz"
cmd = (
f"PGPASSWORD='{config.PG_PASSWORD}' "
f"pg_dump -h {config.PG_HOST} -p {config.PG_PORT} "
f"-U {config.PG_USER} -d {db} -Fc -Z6 -f {dump_file}"
)
try:
subprocess.run(cmd, shell=True, check=True, capture_output=True)
size_mb = dump_file.stat().st_size / (1024 * 1024)
log.info(f" ✓ {db} → {dump_file.name} ({size_mb:.1f} MB)")
dumps.append({"db": db, "file": str(dump_file), "size_mb": round(size_mb, 1)})
except subprocess.CalledProcessError as e:
log.error(f" ✗ Error en {db}: {e.stderr.decode()}")
raise
return dumps
def backup_files():
"""Comprime directorios de archivos en un tar.gz."""
log.info("Iniciando backup de archivos...")
archive_file = BACKUP_SUBDIR / "archivos.tar.gz"
with tarfile.open(archive_file, "w:gz") as tar:
for dir_path in config.FILE_DIRS:
path = Path(dir_path)
if path.exists():
tar.add(str(path), arcname=path.name)
log.info(f" ✓ {dir_path}")
else:
log.warning(f" ⚠ {dir_path} no existe, saltando...")
size_mb = archive_file.stat().st_size / (1024 * 1024)
log.info(f" Archivo: {archive_file.name} ({size_mb:.1f} MB)")
return {"file": str(archive_file), "size_mb": round(size_mb, 1)}
def cleanup_old_backups():
"""Elimina backups más antiguos que RETENTION_DAYS."""
log.info(f"Limpiando backups con más de {config.RETENTION_DAYS} días...")
cutoff = datetime.now() - timedelta(days=config.RETENTION_DAYS)
removed = 0
for item in config.BACKUP_DIR.iterdir():
if item.is_dir() and item.name != "logs":
try:
dir_date = datetime.strptime(item.name[:8], "%Y%m%d")
if dir_date < cutoff:
import shutil
shutil.rmtree(item)
log.info(f" ✓ Eliminado: {item.name}")
removed += 1
except ValueError:
continue
log.info(f" {removed} backup(s) eliminado(s)")
return removed
def notify_slack(message: str, success: bool = True):
"""Envía notificación a Slack."""
if not config.SLACK_WEBHOOK:
return
color = "#36a64f" if success else "#ff0000"
emoji = "✅" if success else "🚨"
payload = {
"attachments": [{
"color": color,
"title": f"{emoji} Backup {'completado' if success else 'FALLIDO'}",
"text": message,
"ts": int(datetime.now().timestamp()),
}]
}
try:
req = Request(
config.SLACK_WEBHOOK,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
urlopen(req, timeout=10)
except Exception as e:
log.warning(f"No se pudo notificar a Slack: {e}")
def main():
"""Ejecuta el proceso completo de backup."""
log.info(f"{'='*50}")
log.info(f"BACKUP INICIADO — {TIMESTAMP}")
log.info(f"{'='*50}")
start = datetime.now()
results = {"databases": [], "files": None, "cleaned": 0}
try:
# 1. Backup de bases de datos
results["databases"] = backup_postgresql()
# 2. Backup de archivos
results["files"] = backup_files()
# 3. Limpiar backups antiguos
results["cleaned"] = cleanup_old_backups()
# Resumen
elapsed = (datetime.now() - start).total_seconds()
total_size = sum(d["size_mb"] for d in results["databases"])
if results["files"]:
total_size += results["files"]["size_mb"]
summary = (
f"Servidor: {config.PG_HOST}\n"
f"Bases de datos: {len(results['databases'])}\n"
f"Tamaño total: {total_size:.1f} MB\n"
f"Backups eliminados: {results['cleaned']}\n"
f"Duración: {elapsed:.0f}s"
)
log.info(f"\n{'='*50}")
log.info(f"BACKUP COMPLETADO en {elapsed:.0f}s — {total_size:.1f} MB total")
log.info(f"{'='*50}")
notify_slack(summary, success=True)
except Exception as e:
log.error(f"BACKUP FALLIDO: {e}")
notify_slack(f"Error: {e}\nServidor: {config.PG_HOST}", success=False)
raise
if __name__ == "__main__":
main()
Paso 3: Probar el script manualmente
Antes de automatizar, ejecuta el script a mano para verificar que funciona:
cd /opt/backup
python3 backup.py
Deberías ver la salida con cada base de datos respaldada, los archivos comprimidos y el resumen final. Verifica que los archivos existen:
ls -lh /var/backups/app/
Prueba la restauración
Un backup sin prueba de restauración no es un backup. Antes de confiar en este script, restaura al menos una base de datos para verificar que el dump es válido:
pg_restore -h localhost -U app -d miapp_test /var/backups/app/20260210_030000/miapp.sql.gz
Paso 4: Automatizar con cron
Ahora que el script funciona, prográmalo con cron para que se ejecute automáticamente:
# Editar crontab del root
sudo crontab -e
Agrega la línea:
# Backup diario a las 3:00 AM
0 3 * * * /usr/bin/python3 /opt/backup/backup.py >> /var/log/backup-cron.log 2>&1
Sintaxis de cron
┌───────── minuto (0-59)
│ ┌─────── hora (0-23)
│ │ ┌───── día del mes (1-31)
│ │ │ ┌─── mes (1-12)
│ │ │ │ ┌─ día de la semana (0-7, 0 y 7 = domingo)
│ │ │ │ │
0 3 * * * # Todos los días a las 3:00 AM
0 3 * * 0 # Solo domingos a las 3:00 AM
0 */6 * * * # Cada 6 horas
Verifica que el cron se guardó:
sudo crontab -l
Paso 5: Enviar backup a destino remoto
El script guarda los backups localmente. Para cumplir con la regla 3-2-1, necesitas una copia fuera del servidor. Aquí dos opciones:
Opción A: rsync a otro servidor
Agrega esta función al script o ejecútala después con cron:
# Sincronizar backups a servidor remoto
rsync -avz --delete /var/backups/app/ backup@10.0.1.20:/var/backups/app-remoto/
Opción B: Subir a S3
Agrega al script:
import boto3
def upload_to_s3(backup_dir: Path):
"""Sube el backup del día a S3."""
s3 = boto3.client("s3")
bucket = os.getenv("S3_BUCKET", "mi-empresa-backups")
for file in backup_dir.glob("*"):
key = f"backups/{backup_dir.name}/{file.name}"
s3.upload_file(str(file), bucket, key)
log.info(f" ✓ S3: s3://{bucket}/{key}")
Monitoreo de backups
Un backup automatizado que falla silenciosamente es peor que no tener backup — porque crees que estás protegido. Implementa al menos una de estas capas de verificación:
Notificaciones — El script ya incluye notificación a Slack. Si no recibes el mensaje diario de "backup completado", algo está mal.
Monitoreo con Zabbix — Configura un item que verifique que el archivo de backup más reciente tiene menos de 25 horas de antigüedad:
# Script para Zabbix agent
find /var/backups/app -maxdepth 1 -type d -mmin -1500 | wc -l
Si el resultado es 0, no hubo backup en las últimas 25 horas — alerta.
Pruebas de restauración automáticas — El nivel más alto de confianza. Un cron semanal que restaura el último backup en una base de datos de prueba y verifica que los datos son consistentes.
Siguientes pasos
Con el backup automatizado funcionando, puedes mejorar la estrategia:
- WAL archiving para PostgreSQL — point-in-time recovery que permite restaurar a cualquier segundo, no solo al momento del dump diario
- Backups incrementales con BorgBackup o restic — solo respaldan lo que cambió, reduciendo tiempo y almacenamiento
- Infraestructura de backup profesional con Veeam o Proxmox Backup Server para ambientes virtualizados
- Plan de recuperación ante desastres (DRP) — documento que define exactamente qué hacer cuando todo falla
Backups empresariales
¿Tu estrategia de backup está probada?
Diseñamos e implementamos tu plan de backup y recuperación ante desastres con pruebas de restauración periódicas documentadas.



