¿Por qué automatizar las tareas en un VPS?
Un VPS es un servidor que funciona de forma continua, a menudo sin supervisión humana directa. Precisamente por eso la automatización de las tareas recurrentes es fundamental: nadie estará ahí a las 3 de la madrugada para lanzar la copia de seguridad o renovar el certificado Let's Encrypt. Una tarea olvidada puede traducirse en una pérdida de datos, en un certificado caducado que deja el sitio fuera de línea o en un disco saturado por falta de rotación de logs.
En la práctica, cinco categorías de tareas aparecen en todo VPS Linux serio: rotación de logs de Nginx para evitar que /var/log/nginx/ llegue a varios gigabytes en pocas semanas; limpieza de imágenes Docker huérfanas (docker image prune -af) para recuperar espacio en disco sin intervención manual; copias de seguridad de PostgreSQL hacia almacenamiento remoto; verificación del certificado SSL antes de su expiración para lanzar una renovación de certbot; y finalmente scripts de scraping ligero o indexación planificados que deben ejecutarse a hora fija. Las dos grandes herramientas disponibles en Linux — cron y los systemd timers — permiten planificar estas operaciones de forma fiable.
Casos de uso típicos en un VPS
- Copias de seguridad — dump de MySQL o PostgreSQL a almacenamiento remoto (
pg_dump -Fc mydb | gzip > /backup/mydb_$(date +%F).sql.gz), archivado de ficheros conrsyncorclonehacia un bucket S3 - Renovación de certificados TLS —
certbot renew --quietoacme.sh --cronplanificados dos veces al día, con recarga automática de Nginx tras el éxito - Rotación y purga de logs — borrado de los ficheros de más de 30 días (
find /var/log -mtime +30 -delete), compresión semanal, vaciado de logs de aplicación en producción - Limpieza de Docker —
docker image prune -af, eliminación de volúmenes huérfanos, rotación de logs de contenedores para liberar/var/lib/docker - Sondas de salud — comprobación de que un servicio escucha (
curl -sf http://localhost:3000/health), reinicio automático si hace falta, alerta por webhook - Sincronización de datos —
rsynchacia un segundo nodo, actualización de cachés, sincronización de ficheros estáticos hacia un CDN - Tareas de aplicación planificadas — envío de correos recordatorios, cálculo de informes periódicos, indexación de contenido, scripts de scraping ligero
cron: recordatorio rápido
Cron está presente en casi todos los sistemas Linux. Para planificar una tarea se edita la crontab del usuario actual con crontab -e. La sintaxis se basa en cinco campos separados por espacios, seguidos del comando:
# minuto (0-59), hora (0-23), día del mes (1-31), mes (1-12), día de la semana (0-7)
0 3 * * * /usr/local/bin/backup.shLos atajos @daily, @weekly, @monthly y @reboot simplifican los casos habituales. Los ficheros en /etc/cron.d/ pertenecen al sistema e indican el usuario de ejecución directamente. Para revisar las ejecuciones pasadas: /var/log/syslog con grep CRON /var/log/syslog, o journalctl -t CRON. Un detalle importante: el PATH por defecto de cron es mínimo — /usr/bin:/bin — lo que obliga a usar rutas absolutas en los scripts, o a redefinir PATH= al inicio de la crontab. La variable MAILTO controla el envío de salidas por correo; ponerla vacía (MAILTO="") lo desactiva silenciosamente. Cron no conserva ningún historial nativo de las ejecuciones pasadas y no gestiona las tareas perdidas si la máquina estaba apagada.
systemd timers: introducción
Un timer de systemd es una pareja de dos ficheros de unidad: un fichero .service que describe qué ejecutar y un fichero .timer que describe cuándo lanzarlo. El timer se activa con systemctl enable --now monservice.timer y lo gobierna el gestor de servicios.
La directiva OnCalendar= acepta una sintaxis rica y legible:
- daily — todos los días a medianoche
- Mon *-*-* 03:00:00 — cada lunes a las 3 AM
- *-*-* 02:30:00 — todos los días a las 2:30
- *:0/15 — cada 15 minutos
La directiva AccuracySec= (por defecto: 1 minuto) permite a systemd agrupar los timers próximos para ahorrar despertares del procesador. La directiva Persistent=true en el bloque [Timer] es la funcionalidad diferencial respecto a cron: si la máquina estaba apagada a la hora prevista, el timer recupera la ejecución perdida en el siguiente arranque. Todas las salidas pasan por journald y se consultan con journalctl -u monservice.service. El comando systemctl list-timers muestra todos los timers activos, su próximo vencimiento y la fecha de su última activación.
cron vs systemd timers: tabla comparativa
Desplace la tabla
| Criterio | cron | systemd timer |
|---|---|---|
| Sintaxis de planificación | 5 campos `* * * * *` | `OnCalendar=` legible (`daily`, `Mon 03:00`) |
| Logs | Salida por correo o redirección manual, `/var/log/syslog` | journald nativo, `journalctl -u` |
| Tareas perdidas (máquina apagada) | Perdidas (salvo `anacron`) | `Persistent=true` las reproduce tras reiniciar |
| Dependencias entre servicios | Ninguna | `After=`, `Requires=`, `Wants=` |
| Aislamiento y recursos | Hereda el entorno del shell, PATH mínimo | Cgroups, `MemoryMax=`, `CPUQuota=` |
| Notificación de fallo | Ninguna de forma nativa | `OnFailure=notify.service` dispara una alerta |
| Ejecución como un usuario concreto | Campo user en `/etc/cron.d/` | `User=` y `Group=` en el `.service` |
| Depuración | Difícil sin logs | `systemctl status`, `journalctl -xe` |
| Disponibilidad | Todos los sistemas Unix (contenedores, BSD, Alpine) | Sistemas con systemd (Debian, Ubuntu, RHEL…) |
¿Cuándo conservar cron?
Cron sigue siendo la buena elección en varias situaciones concretas. En sistemas antiguos o minimalistas sin systemd — algunos contenedores Docker ligeros, imágenes Alpine, sistemas BSD — cron suele ser la única herramienta disponible. En un entorno multiusuario donde cada desarrollador gestiona sus propias tareas con crontab -e sin permisos de root, cron es más sencillo de delegar.
Para scripts de una sola línea muy simples — un certbot renew semanal, un rsync nocturno sin dependencias — la crontab sigue siendo legible y mantenible sin multiplicar los ficheros. Cron es también la buena elección para incorporar a un equipo no especializado en Linux: la sintaxis de los cinco campos, documentada en todas partes, es más accesible que una unidad systemd. Por último, si sus tareas cron llevan años funcionando sin problemas, la migración no aporta valor inmediato. El criterio decisivo es la necesidad de logs estructurados, dependencias entre servicios o restricciones de recursos.
¿Cuándo pasar a los systemd timers?
Los timers de systemd se imponen en cuanto la tarea supera el marco de un simple comando aislado. ¿Necesita journald para trazar cada ejecución, sus salidas y su código de retorno sin fontanería manual? Systemd timers. ¿Su script de copia de seguridad debe ejecutarse solo después de que PostgreSQL esté listo, declarando After=postgresql.service? Systemd timers. ¿Quiere limitar la memoria de un trabajo de sincronización para que no ahogue a los demás procesos (MemoryMax=512M)? Systemd timers.
Las dependencias entre servicios son el argumento más fuerte: un backup lanzado cuando la base de datos no está disponible tras un reinicio produce un fichero vacío sin error visible bajo cron. Systemd garantiza el orden. El logging integrado via journald elimina todo el ruido de las redirecciones manuales. La gestión de errores con OnFailure= permite disparar una notificación (correo, webhook) sin contaminar el script. Y si el servidor se reinicia a las 2:58 cuando la copia de las 3 tendría que haberse ejecutado, Persistent=true garantiza que se ejecutará en el siguiente arranque.
Crear un timer de systemd desde cero (ejemplo: copia de seguridad diaria)
Crear el fichero service
Abra
/etc/systemd/system/backup.servicee introduzca:[Unit] Description=Copia de seguridad diaria de PostgreSQL After=postgresql.service [Service] Type=oneshot User=postgres ExecStart=/usr/local/bin/backup.shEl tipo
oneshotindica que el servicio termina una vez ejecutado el script — el tipo estándar para las tareas planificadas.Crear el fichero timer
Cree
/etc/systemd/system/backup.timercon:[Unit] Description=Disparador diario de la copia de seguridad [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true [Install] WantedBy=timers.targetPersistent=truegarantiza que si la máquina estaba apagada a las 3, la copia se ejecutará en el siguiente arranque.Recargar systemd y activar el timer
Ejecute
systemctl daemon-reloadpara que systemd tenga en cuenta los nuevos ficheros, y luego active e inicie el timer de inmediato:systemctl enable --now backup.timerComprobar con systemctl status
Compruebe el estado del timer con
systemctl status backup.timer— la salida debe indicaractive (waiting)y mostrar la próxima fecha de disparo. Consulte también el estado del servicio tras la primera ejecución consystemctl status backup.service.Listar todos los timers activos
El comando
systemctl list-timers --allmuestra una tabla de todos los timers del sistema con tres columnas clave:NEXT(próxima ejecución),LEFT(tiempo restante) yLAST(última ejecución). Es su panel de control para verificar que sus timers se disparan como se espera.Consultar los logs
Acceda a todas las salidas del servicio con
journalctl -u backup.servicepara el historial completo, ojournalctl -u backup.service -n 50 --since todaypara las 50 últimas líneas del día.Añadir notificación de error (opcional)
Para recibir alertas en caso de fallo, añada en el bloque
[Unit]del fichero.service:OnFailure=notify-failure@%n.serviceEste servicio puede enviar un correo, un webhook de Slack o un ping ntfy según su infraestructura. Es lo que cron no puede hacer de forma nativa sin fontanería en el propio script.
Probar el servicio manualmente
Antes de esperar el disparo planificado, pruebe la ejecución inmediata con
systemctl start backup.service(sin.timer). Consulte el resultado de inmediato conjournalctl -u backup.service -n 20. Si el script tiene éxito, la configuración está validada.
Migrar una tarea cron existente a systemd
Identificar la tarea cron que migrar
Liste sus crontabs con
crontab -l(usuario actual) ycat /etc/cron.d/*(sistema). Anote el comando exacto, el usuario de ejecución y la planificación en cinco campos. Por ejemplo:30 2 * * 1 root /usr/bin/certbot renew --quietsignifica cada lunes a las 2:30 como root.Traducir la planificación a OnCalendar
El formato
OnCalendar=de systemd se escribeDayOfWeek Year-Month-Day Hour:Minute:Second.30 2 * * 1se convierte enMon *-*-* 02:30:00.Para validar la traducción antes de aplicarla, use
systemd-analyze calendar:systemd-analyze calendar 'Mon *-*-* 02:30:00'El comando muestra las 10 próximas ocurrencias calculadas, eliminando cualquier ambigüedad sobre el comportamiento real.
Crear los ficheros .service y .timer
Cree
/etc/systemd/system/certbot-renew.serviceconType=oneshot,ExecStart=/usr/bin/certbot renew --quietyUser=root. Cree después/etc/systemd/system/certbot-renew.timerconOnCalendar=Mon *-*-* 02:30:00yPersistent=true. Ejecutesystemctl daemon-reload && systemctl enable --now certbot-renew.timer.Probar el servicio antes de desactivar cron
Pruebe de inmediato con
systemctl start certbot-renew.servicey consulte el resultado conjournalctl -u certbot-renew.service -n 20. Compruebe que el timer aparece ensystemctl list-timerscon una fecha próxima coherente. Solo desactive la línea cron tras la validación.Desactivar la línea cron
Una vez validado el timer, comente o borre la línea en la crontab original. Para un fichero en
/etc/cron.d/, renómbrelo con extensión.disabledo bórrelo. Deje coexistir ambos durante unos días si la tarea es crítica — cron y los systemd timers no interfieren.
Validar y depurar sus expresiones OnCalendar
Antes de activar un timer en producción, valide siempre su expresión OnCalendar= con systemd-analyze calendar:
systemd-analyze calendar "daily"
systemd-analyze calendar "Mon *-*-* 02:30:00"
systemd-analyze calendar "*:0/15"Cada comando muestra la próxima ocurrencia y las diez siguientes — lo que hace inmediatamente visible una expresión mal formada o un desfase horario inesperado. Para una visión de todos sus timers activos con su próxima fecha de disparo y el tiempo restante, systemctl list-timers --all es su panel operacional diario.
Problemas frecuentes con los timers de systemd
Cinco problemas se repiten al poner en marcha los timers.
1. "Unit not found" al hacer systemctl enable — el fichero .timer o .service no está en /etc/systemd/system/ o tiene un nombre incorrecto. Compruebe la ortografía exacta con ls /etc/systemd/system/*.timer. Un daemon-reload es obligatorio antes de cualquier enable o start sobre un fichero nuevo.
2. Timer activo pero que nunca se dispara — ¿muestra systemctl list-timers una fecha coherente en la columna NEXT? Si NEXT está vacío o en el pasado, un Persistent=true olvidado combinado con una máquina que no ha reiniciado desde la creación del timer es una causa habitual. Compruebe systemctl status backup.timer: active (waiting) es el estado normal.
3. ExecStart no encontrado o permiso denegado — use siempre la ruta absoluta en ExecStart= (/usr/bin/certbot y no certbot). Verifique que el script es ejecutable (chmod +x /usr/local/bin/backup.sh) y que el usuario declarado en User= tiene acceso a él.
4. Permisos en el script — si User=postgres pero el script pertenece a root sin permiso de ejecución para postgres, el servicio fallará con el código 203/EXEC. Consulte journalctl -u backup.service -n 20 para ver el mensaje exacto.
5. Conflicto entre cron y systemd en la misma tarea — si está migrando de forma progresiva, asegúrese de no tener ambos activos a la vez para una tarea destructiva (purga de logs, rotación de ficheros). Los dos se ejecutarán independientemente y duplicarán las operaciones. La comprobación es sencilla: crontab -l | grep backup y systemctl is-active backup.timer.
Conclusión
Cron y los systemd timers conviven sin problema en el mismo servidor: no tiene que migrarlo todo de golpe. Conserve cron para sus tareas sencillas y sus scripts de usuario existentes, y adopte los timers de systemd para las nuevas automatizaciones que se benefician de los logs de journald, de las dependencias entre servicios o del aislamiento por cgroups. Los comandos systemd-analyze calendar y systemctl list-timers son sus mejores aliados para validar y supervisar sus planificaciones.