Un servidor sin automatización es como un reloj sin cuerda: funciona, pero solo mientras alguien esté allí para darle cuerda. Los scripts que has escrito en los capítulos anteriores — respaldos, monitorización, limpieza de logs, generación de reportes — necesitan ejecutarse sin intervención humana, en horarios precisos y con garantías de ejecución.
Aquí entran cron y systemd-timer:
- Cron existe desde 1975. Es el programador de tareas más conocido, más simple y más ubicuo. Está en cada servidor Linux, en cada contenedor, en cada instalación mínima.
- Systemd-Timer es el reemplazo moderno integrado en systemd. Ofrece más control, mejor logging, dependencias entre unidades, y capacidades que cron no tiene: despertar el sistema, ejecutar tras un evento, calendar syntax más expresiva.
En este capítulo aprenderás a:
- Programar tareas con cron, desde lo básico hasta patrones profesionales
- Crear timers de systemd con unidades
.servicey.timer - Manejar logging, errores y notificaciones en ambos sistemas
- Decidir cuándo usar cada uno según el contexto
- Construir herramientas que gestionen tareas programadas automáticamente
Filosofía del capítulo: no se trata de elegir un ganador. Cron y systemd-timer coexisten y se complementan. Cron es simple, directo y funciona en cualquier sitio. Systemd-timer es potente, expresivo y se integra con el ecosistema systemd. Sabrás cuándo usar cada uno.
Cron — El clásico de la automatización
Sintaxis de cron
Cada línea en un crontab describe una tarea periódica con cinco campos de tiempo seguidos del comando:
┌───────── minuto (0-59)
│ ┌──────── hora (0-23)
│ │ ┌────── día del mes (1-31)
│ │ │ ┌──── mes (1-12, o nombres: jan, feb, mar...)
│ │ │ │ ┌── día de la semana (0-7, 0=domingo, 7=domingo; o nombres: sun, mon...)
│ │ │ │ │
* * * * * comando a ejecutar
Ejemplos básicos:
# Cada minuto
* * * * * /home/user/script.sh
# Cada día a las 2:30 AM
30 2 * * * /home/user/backup.sh
# Cada lunes a las 9:00 AM
0 9 * * 1 /home/user/reporte.sh
# Primer día de cada mes a medianoche
0 0 1 * * /home/user/facturacion.sh
# Cada 15 minutos
*/15 * * * * /home/user/healthcheck.sh
# Cada hora en el minuto 0
0 * * * * /home/user/rotar_logs.sh
Operadores especiales:
| Operador | Significado | Ejemplo |
|---|---|---|
* | Todos los valores | * * * * * = cada minuto |
*/n | Cada n unidades | */5 * * * * = cada 5 minutos |
a,b,c | Lista de valores | 0,30 * * * * = minutos 0 y 30 |
a-b | Rango | 9-17 * * * * = cada hora de 9 a 17 |
a-b/n | Rango con paso | 9-17/2 * * * * = cada 2 horas entre 9 y 17 |
@reboot, @daily, @hourly — Atajos named
Cron ofrece atajos named que simplifican los patrones más comunes:
| Atajo | Equivalente | Cuándo se ejecuta |
|---|---|---|
@reboot | — | Al arrancar el sistema |
@yearly / @annually | 0 0 1 1 * | 1 de enero a medianoche |
@monthly | 0 0 1 * * | Primer día del mes a medianoche |
@weekly | 0 0 * * 0 | Domingo a medianoche |
@daily | 0 0 * * * | Cada día a medianoche |
@hourly | 0 * * * * | Cada hora en el minuto 0 |
# Ejecutar script al arrancar
@reboot /home/user/iniciar_servicio.sh
# Backup diario
@daily /home/user/backup_diario.sh
# Limpieza semanal
@weekly /home/user/limpiar_temporales.sh
⚠️ Atención:
@rebootno tiene equivalente en systemd-timer. Si necesitas ejecutar algo al arranque, cron es la solución más simple. En systemd usarías una unidad.serviceconWantedBy=multi-user.target.
crontab -e, crontab -l, crontab -r
La herramienta crontab gestiona los archivos crontab de cada usuario:
# Editar tu crontab personal
crontab -e
# Listar tus tareas programadas
crontab -l
# Eliminar tu crontab (¡cuidado!)
crontab -r
# Editar el crontab de otro usuario (root)
sudo crontab -u www-data -e
# Listar tareas de otro usuario
sudo crontab -u postgres -l
¿Qué editor usa? crontab -e respeta la variable EDITOR:
export EDITOR=nano
crontab -e # Usa nano
# O para siempre:
echo 'export EDITOR=vim' >> ~/.bashrc
Formato del archivo crontab: Cada línea es una tarea. Las líneas que empiezan con # son comentarios. Las variables de entorno se pueden definir antes de las tareas:
# ─── Variables de entorno ───
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=admin@example.com
# ─── Tareas ───
# Backup nocturno
30 2 * * * /home/user/backup.sh
# Monitorización cada hora
0 * * * * /home/user/healthcheck.sh
El entorno de cron — PATH, SHELL, HOME
Ésta es la causa #1 de errores en cron. Cuando cron ejecuta un script, NO carga tu .bashrc, .bash_profile ni .profile. El entorno es mínimo:
# Lo que cron NO tiene por defecto:
# - PATH personalizado (~/.local/bin, /home/user/.nvm/...)
# - Variables de entorno (DB_HOST, API_KEY, ...)
# - Alias
# - Funciones del shell
# Lo que cron SÍ tiene (usualmente):
PATH=/usr/bin:/bin
SHELL=/bin/sh # ¡No bash!
HOME=/home/user
LOGNAME=user
Solución profesional — nunca confíes en el entorno:
# ❌ Falla porque PATH no incluye /usr/local/bin
# (suponiendo que el script usa comandos allí)
0 3 * * * /home/user/mi_script.sh
# ✅ Fijar PATH explícitamente al inicio del crontab
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 3 * * * /home/user/mi_script.sh
# ✅ O mejor: fijar PATH dentro del propio script
# En mi_script.sh:
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin"
Regla de oro: en tu script, siempre declara PATH completo al inicio. Así el script funciona igual desde cron, desde terminal o desde otro script.
Logging en cron — syslog, MAILTO, logs personalizados
Cron registra sus ejecuciones en el syslog del sistema:
# Ver ejecuciones de cron
grep CRON /var/log/syslog
# Con journalctl (sistemas con systemd)
journalctl -u cron -n 50
# Ver solo errores de cron
journalctl -u cron -p err
MAILTO — notificaciones por correo:
# En el crontab:
MAILTO=admin@example.com
# Si una tarea produce salida (stdout o stderr),
# cron envía esa salida por correo a MAILTO.
# Si no hay salida, no envía correo.
# Desactivar correo:
MAILTO=""
# Enviar a múltiples destinatarios:
MAILTO="admin1@e.com,admin2@e.com"
Logs personalizados — la práctica profesional:
# Redirigir toda la salida a un archivo de log
0 3 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1
# Con timestamp por ejecución
0 3 * * * /home/user/backup.sh >> "/var/log/backup/backup_$(date +\%F).log" 2>&1
# ⚠️ ¡El % en crontab hay que escaparlo! Se escribe \%
# Sin escapar, cron interpreta % como salto de línea
Logging estructurado desde el script:
#!/bin/bash
# En tu script, no en el crontab
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log "Iniciando backup..."
# ... operaciones ...
log "Backup completado"
Patrones comunes de cron
Backup diario con rotación:
# Crontab
PATH=/usr/local/bin:/usr/bin:/bin
30 2 * * * /home/user/backup_diario.sh >> /var/log/backup.log 2>&1
Monitorización cada 5 minutos:
*/5 * * * * /home/user/healthcheck.sh >> /var/log/healthcheck.log 2>&1
Rotación de logs cada hora:
0 * * * * /home/user/rotar_logs.sh
Limpieza semanal de temporales:
0 6 * * 0 /home/user/limpiar_temporales.sh
Sincronización con servidor remoto:
*/30 * * * * /usr/bin/rsync -az /datos/ usuario@remoto:/backups/
Limitaciones de cron
Cron es simple, pero tiene limitaciones importantes:
- No hay dependencias — no puedes decir «ejecuta esto tras aquello»
- No hay logging nativo — dependes de syslog o redirecciones manuales
- Entorno mínimo — causa infinita de bugs silenciosos
- Sin persistencia — si el sistema estaba apagado, la tarea se pierde
- Sin jitter controlado — no hay forma de esparcir aleatoriamente la ejecución
- No puede despertar el sistema — si la máquina está dormida, la tarea no se ejecuta
- Resolución de 1 minuto — no puedes programar tareas cada 30 segundos
Systemd-Timer resuelve todas estas limitaciones.
Systemd-Timer — El sucesor moderno
¿Qué es un timer de systemd?
Un timer de systemd es una unidad que dispara la ejecución de una unidad de servicio (.service) asociada. Funciona con dos archivos:
algo.service— define qué ejecutar (el script, el comando)algo.timer— define cuándo ejecutarlo (el calendario, frecuencia)
Ambos llevan el mismo nombre base y se colocan en /etc/systemd/system/.
Ventajas frente a cron:
| Característica | Cron | Systemd-Timer |
|---|---|---|
| Dependencias | No | Sí (After=, Requires=) |
| Logging | Redirección manual | journalctl nativo |
| Persistencia tras apagado | No | Sí (Persistent=true) |
| Despertar sistema | No | Sí (WakeSystem=true) |
| Jitter aleatorio | No | Sí (RandomizedDelaySec=) |
| Precisión configurable | No | Sí (AccuracySec=) |
| Entorno | Mínimo | Hereda del sistema |
| Transient oneshots | No | Sí (systemd-run) |
Crear una unidad .service
La unidad .service define qué ejecutar. Es un archivo de texto en /etc/systemd/system/.
Ejemplo: backup-diario.service
[Unit]
Description=Backup diario de la base de datos
Documentation=https://example.com/backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup_db.sh
User=postgres
Group=postgres
Environment=DB_HOST=localhost
Environment=DB_NAME=produccion
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Explicación:
| Campo | Descripción |
|---|---|
Type=oneshot | El servicio ejecuta una acción y termina (no es un daemon) |
ExecStart= | Comando a ejecutar (ruta absoluta siempre) |
User= / Group= | ¿Con qué usuario se ejecuta? |
Environment= | Variables de entorno para el script |
StandardOutput=journal / StandardError=journal | La salida va al log de systemd |
Para servicios que sí son daemon (como un servidor web), usarías Type=simple o Type=forking.
Crear una unidad .timer
El archivo .timer define cuándo ejecutar el servicio asociado. Debe tener el mismo nombre base que el .service.
Ejemplo: backup-diario.timer
[Unit]
Description=Timer para backup diario
Requires=backup-diario.service
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
AccuracySec=1m
[Install]
WantedBy=timers.target
Explicación de las opciones del timer:
| Campo | Descripción |
|---|---|
OnCalendar= | Especifica cuándo ejecutar (formato calendario) |
Persistent=true | Si el sistema estaba apagado, ejecuta al encender |
RandomizedDelaySec=1h | Añade hasta 1 hora de retardo aleatorio |
AccuracySec=1m | Precisión de 1 minuto (menos preciso = más eficiente) |
OnBootSec= | Ejecuta N tiempo tras el arranque |
OnUnitActiveSec= | Ejecuta N tiempo tras la última activación |
WakeSystem=true | Despierta el sistema si está dormido |
Calendar syntax — OnCalendar=
La sintaxis de OnCalendar= es más expresiva que cron. Sigue el formato:
OnCalendar=<día_de_semana> <fecha/hora>
Ejemplos de calendar syntax:
# Diario a las 3:30 AM
OnCalendar=daily
# Equivale a: *-*-* 00:00:00
# A las 3:30 AM cada día
OnCalendar=*-*-* 03:30:00
# Cada lunes a las 9 AM
OnCalendar=Mon *-*-* 09:00:00
# Cada hora
OnCalendar=hourly
# Equivale a: *-*-* *:00:00
# Cada 15 minutos
OnCalendar=*:0/15
# Primer día de cada mes a medianoche
OnCalendar=*-*-01 00:00:00
# Días laborables a las 8 AM
OnCalendar=Mon..Fri *-*-* 08:00:00
# Cada 2 horas
OnCalendar=*:0/2
# Varias especificaciones (se evalúan como OR)
OnCalendar=Mon *-*-* 09:00:00
OnCalendar=Fri *-*-* 17:00:00
# Se ejecuta lunes a las 9 y viernes a las 5
Validar calendar syntax:
# Ver cuándo se disparará un timer
systemd-analyze calendar "Mon *-*-* 09:00:00"
# → next el Mon 2026-06-15 09:00:00 UTC
# → luego cada 7 días
# Ver el timer en formato crontab-like
systemd-analyze calendar "Mon..Fri *-*-* 08:00:00" --iterations 3
# Probar una expresión antes de escribirla
systemd-analyze verify /etc/systemd/system/mi-timer.timer
Persistent=true — No perder ejecuciones
Una de las mayores ventajas sobre cron: si el sistema estaba apagado cuando debía ejecutarse la tarea, con Persistent=true se ejecutará inmediatamente al encender.
[Timer]
OnCalendar=daily
Persistent=true
Si el servidor se apagó a las 23:00 y el timer debía ejecutarse a las 3:00, al arrancar al día siguiente a las 8:00, el timer se dispara inmediatamente.
⚠️ Cuidado: si el sistema estuvo apagado varios días, se ejecutará una sola vez, no una por cada ejecución perdida.
WakeSystem=true — Despertar el sistema
¿Quieres que el servidor se encienda para hacer un backup nocturno y se vuelva a dormir? WakeSystem=true usa rtcwake internamente:
[Timer]
OnCalendar=*-*-* 03:00:00
WakeSystem=true
Requiere que el hardware soporte despertar por RTC (reloj en tiempo real). Muy útil en servidores de bajo consumo o estaciones de trabajo.
RandomizedDelaySec y AccuracySec — Jitter y precisión
RandomizedDelaySec: añade un retardo aleatorio para evitar que todas las tareas se ejecuten simultáneamente (el «thundering herd»):
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h # Ejecuta entre las 00:00 y la 01:00
AccuracySec: controla cuán precisa debe ser la ejecución. Mayor precisión = más despertar del sistema:
[Timer]
OnCalendar=hourly
AccuracySec=1s # ⚠️ Alta precisión, más consumo
AccuracySec=1h # ✅ Baja precisión, máximo ahorro (por defecto 1min)
Para tareas que no necesitan precisión de segundos (backups, limpieza), AccuracySec=1h reduce significativamente el consumo energético.
Gestionar timers con systemctl
# Activar e iniciar un timer
sudo systemctl enable backup-diario.timer
sudo systemctl start backup-diario.timer
# Ver estado
sudo systemctl status backup-diario.timer
sudo systemctl status backup-diario.service
# Listar todos los timers activos
systemctl list-timers --all
# Ver solo timers del usuario
systemctl --user list-timers
# Desactivar
sudo systemctl stop backup-diario.timer
sudo systemctl disable backup-diario.timer
# Recargar cambios tras editar unidades
sudo systemctl daemon-reload
# Ejecutar el servicio manualmente (sin esperar al timer)
sudo systemctl start backup-diario.service
# Ver próxima ejecución
systemctl list-timers backup-diario.timer
Salida de systemctl list-timers:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-06-15 03:00:00 UTC 12h left Sun 2026-06-14 03:00:05 UTC 11h ago backup-diario.timer backup-diario.service
Tue 2026-06-16 02:00:00 UTC 1 day 11h Mon 2026-06-15 02:00:04 UTC 39min ago limpieza-log.timer limpieza-log.service
journalctl — Ver el log del timer
Systemd registra automáticamente todo lo que hace el timer y el servicio:
# Ver todas las ejecuciones del timer
journalctl -u backup-diario.timer -n 50
# Ver la salida del servicio
journalctl -u backup-diario.service -n 50
# Seguir en tiempo real
journalctl -u backup-diario.service -f
# Ver solo errores
journalctl -u backup-diario.service -p err
# Ver ejecuciones de ayer
journalctl -u backup-diario.service --since yesterday --until today
# Formato detallado
journalctl -u backup-diario.service -o verbose
Timers transitorios con systemd-run
No necesitas crear archivos para timers de un solo uso o pruebas:
# Ejecutar un script cada hora durante 3 horas (sin crear unidades)
sudo systemd-run --on-calendar='*:0/1' --unit=test-timer /home/user/script.sh
# Ejecutar una vez tras 5 minutos
sudo systemd-run --on-active=5m /home/user/test.sh
# Ver timers transitorios
systemctl list-timers --all | grep run-
Los timers creados con systemd-run son transitorios: desaparecen al reiniciar. Ideales para pruebas o tareas ad-hoc.
Cron vs Systemd-Timer — ¿Cuándo usar cada uno?
| Situación | Recomendación | Motivo |
|---|---|---|
| Script simple, entorno mínimo | Cron | Menos archivos, menos conceptos |
| Necesito logging nativo | Systemd-Timer | journalctl sin configurar |
| El sistema se apaga por las noches | Systemd-Timer | Persistent=true |
| tarea @reboot (al arrancar) | Cron | Atajo directo, sin unidades |
| Necesito dependencias (red, disco, etc.) | Systemd-Timer | After=, Requires= |
| Varias frecuencias para misma tarea | Systemd-Timer | Múltiples .timer → un .service |
| Entorno heredado, servidor sin systemd | Cron | No hay alternativa |
| Precisión de segundos | Systemd-Timer | AccuracySec=1s |
| Despertar sistema dormido | Systemd-Timer | WakeSystem=true |
| Una sola máquina, tarea simple | Cualquiera | El que conozcas mejor |
| Infraestructura como código, reproducible | Systemd-Timer | Archivos versionables |
Regla práctica: si tu tarea cabe en una línea de crontab y no necesita logging sofisticado, usa cron. Si necesitas logging, persistencia, dependencias o precisión, usa systemd-timer.
Funciones profesionales
Funciones reutilizables para tu biblioteca personal (~/.bashrc o librería aparte):
# ──────────────────────────────────────────────
# Funciones profesionales para automatización
# ──────────────────────────────────────────────
# install_cron_job — Añade una tarea a crontab si no existe
# Uso: install_cron_job <expresión_tiempo> <comando> [identificador]
install_cron_job() {
local time_expr="$1"
local command="$2"
local identifier="${3:-$command}"
local crontab_line="${time_expr} ${command}"
if crontab -l 2>/dev/null | grep -Fq "$identifier"; then
echo "✓ Tarea ya existe: $identifier"
return 0
fi
(crontab -l 2>/dev/null; echo "$crontab_line") | crontab -
echo "✓ Tarea añadida: $identifier"
}
# remove_cron_job — Elimina una tarea del crontab por identificador
# Uso: remove_cron_job <identificador>
remove_cron_job() {
local identifier="$1"
if ! crontab -l 2>/dev/null | grep -Fq "$identifier"; then
echo "⚠ Tarea no encontrada: $identifier"
return 1
fi
crontab -l 2>/dev/null | grep -Fv "$identifier" | crontab -
echo "✓ Tarea eliminada: $identifier"
}
# install_systemd_timer — Crea e instala un timer systemd
# Uso: install_systemd_timer <nombre> <script> <calendar> [user]
install_systemd_timer() {
local name="$1"
local script="$2"
local calendar="$3"
local user="${4:-$USER}"
local unit_dir="/etc/systemd/system"
# Crear .service
sudo tee "${unit_dir}/${name}.service" > /dev/null <<SERVICEEOF
[Unit]
Description=Servicio para ${name}
[Service]
Type=oneshot
ExecStart=${script}
User=${user}
StandardOutput=journal
StandardError=journal
SERVICEEOF
# Crear .timer
sudo tee "${unit_dir}/${name}.timer" > /dev/null <<TIMEREOF
[Unit]
Description=Timer para ${name}
Requires=${name}.service
[Timer]
OnCalendar=${calendar}
Persistent=true
RandomizedDelaySec=30m
[Install]
WantedBy=timers.target
TIMEREOF
sudo systemctl daemon-reload
sudo systemctl enable --now "${name}.timer"
echo "✓ Timer instalado: ${name}.timer (${calendar})"
systemctl list-timers "${name}.timer" --no-pager 2>/dev/null
}
# list_all_timers — Muestra todos los timers con formato legible
# Uso: list_all_timers
list_all_timers() {
echo "=== Timers de systemd ==="
systemctl list-timers --all --no-pager 2>/dev/null || echo "(no hay timers o no hay systemd)"
echo ""
echo "=== Tareas de cron ==="
if crontab -l 2>/dev/null; then
:
else
echo "(no hay tareas cron para $USER)"
fi
}
Script completo: gestor_tareas_programadas.sh
Un gestor unificado de tareas programadas que soporta tanto cron como systemd-timer, en ~100 líneas:
#!/bin/bash
# gestor_tareas_programadas.sh — Gestiona tareas cron y systemd-timer
# Uso: ./gestor_tareas_programadas.sh <cmd> [args]
# Comandos: add-cron, add-timer, list, remove, log, test
set -euo pipefail
GRN='\033[0;32m'; YLW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok() { echo -e "${GRN}✓${NC} $*"; }
warn(){ echo -e "${YLW}⚠${NC} $*" >&2; }
err() { echo -e "${RED}✗${NC} $*" >&2; }
# ── Validar script ──
validar_script() {
local s="$1"
if [[ ! -f "$s" ]]; then err "Script '$s' no existe"; return 1; fi
if [[ ! -x "$s" ]]; then warn "Script '$s' no ejecutable — usa chmod +x"; return 1; fi
ok "Script válido: $s"
}
# ── add-cron — Añadir tarea cron ──
cmd_add_cron() {
local expr="${1:?Uso: add-cron <expr> <script> [id]}"
local script="${2:?Falta ruta del script}"
local id="${3:-$script}"; validar_script "$script" || return 1
crontab -l 2>/dev/null | grep -Fq "$id" && { warn "'$id' ya existe"; return 1; }
(crontab -l 2>/dev/null; echo "$expr $script >> /var/log/cron_$(basename "$script").log 2>&1") | crontab -
ok "Cron añadido: $id ($expr)"
}
# ── add-timer — Añadir timer systemd ──
cmd_add_timer() {
local name="${1:?Uso: add-timer <nombre> <script> <cal>}"
local script="${2:?Falta ruta del script}"
local cal="${3:?Falta calendario (ej: daily)}"; validar_script "$script" || return 1
local d="/etc/systemd/system"
[[ -f "$d/$name.timer" ]] && { warn "Timer '$name' ya existe"; return 1; }
sudo tee "$d/$name.service" > /dev/null <<EOF
[Unit]
Description=Servicio para $name
[Service]
Type=oneshot
ExecStart=$script
User=$USER
StandardOutput=journal
StandardError=journal
EOF
sudo tee "$d/$name.timer" > /dev/null <<EOF
[Unit]
Description=Timer para $name
Requires=$name.service
[Timer]
OnCalendar=$cal
Persistent=true
RandomizedDelaySec=15m
AccuracySec=1min
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now "$name.timer"
ok "Timer añadido: $name ($cal)"
}
# ── list — Listar todas las tareas ──
cmd_list() {
echo "═══ CRON ═══"
crontab -l 2>/dev/null || warn "No hay tareas cron para $USER"
echo "═══ SYSTEMD-TIMER ═══"
if command -v systemctl &>/dev/null; then
systemctl list-timers --all --no-pager 2>/dev/null | head -20
else
warn "systemd no disponible"
fi
}
# ── remove — Eliminar tarea por identificador ──
cmd_remove() {
local id="${1:?Falta identificador}"
if crontab -l 2>/dev/null | grep -Fq "$id"; then
crontab -l 2>/dev/null | grep -Fv "$id" | crontab -
ok "Tarea cron '$id' eliminada"; return 0
fi
if [[ -f "/etc/systemd/system/$id.timer" ]]; then
sudo systemctl disable --now "$id.timer" 2>/dev/null || true
sudo rm -f "/etc/systemd/system/$id.timer" "/etc/systemd/system/$id.service"
sudo systemctl daemon-reload
ok "Timer '$id' eliminado"; return 0
fi
err "No se encontró '$id'"
return 1
}
# ── log — Ver log de tarea ──
cmd_log() {
local id="${1:?Falta identificador}"
local lf="/var/log/cron_${id}.log"
[[ -f "$lf" ]] && { echo "=== $lf ==="; tail -20 "$lf"; return 0; }
systemctl list-timers --all --no-pager 2>/dev/null | grep -q "$id" \
&& { journalctl -u "${id}.service" -n 30 --no-pager; return 0; }
err "No se encontró log para '$id'"; return 1
}
# ── test — Validar y probar script ──
cmd_test() {
local script="${1:?Falta ruta del script}"; validar_script "$script" || return 1
bash -n "$script" && ok "Sintaxis correcta" || { err "Error de sintaxis"; return 1; }
echo -n "¿Ejecutar script? (s/N): "; read -r r
[[ "$r" =~ ^[sS]$ ]] && bash "$script"
}
# ── Main ──
main() {
local cmd="${1:-help}"; shift 2>/dev/null || true
case "$cmd" in
add-cron) cmd_add_cron "$@" ;; add-timer) cmd_add_timer "$@" ;;
list) cmd_list ;; remove) cmd_remove "$@" ;;
log) cmd_log "$@" ;; test) cmd_test "$@" ;;
help|*)
echo "Uso: $0 <comando> [args]"
echo " add-cron <expr> <script> [id] — Añadir tarea cron"
echo " add-timer <nombre> <script> <cal> — Añadir timer systemd"
echo " list — Listar tareas"
echo " remove <id> — Eliminar tarea"
echo " log <id> — Ver log"
echo " test <script> — Validar script"
echo "Ej: $0 add-cron '30 2 * * *' /home/user/backup.sh backup"
echo " $0 add-timer backup /home/user/backup.sh daily"
echo " $0 list"
esac
}
main "$@"
Cómo usarlo:
chmod +x gestor_tareas_programadas.sh
# Añadir tarea cron (backup diario a las 2:30 AM)
./gestor_tareas_programadas.sh add-cron '30 2 * * *' /home/user/backup.sh backup
# Añadir timer systemd (cada hora)
./gestor_tareas_programadas.sh add-timer healthcheck /home/user/healthcheck.sh hourly
# Listar todas las tareas
./gestor_tareas_programadas.sh list
# Ver log de una tarea
./gestor_tareas_programadas.sh log backup
# Probar script antes de añadirlo
./gestor_tareas_programadas.sh test /home/user/nuevo_script.sh
# Eliminar tarea
./gestor_tareas_programadas.sh remove backup
Errores comunes (10)
Error 1: Script funciona en terminal pero falla en cron
# ❌ El script usa comandos que no están en el PATH de cron
# En script.sh (funciona en terminal, falla en cron):
#!/bin/bash
docker exec mycontainer backup.sh # docker no está en PATH de cron
# ✅ Fijar PATH al inicio del script
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin"
# O usar rutas absolutas:
/usr/bin/docker exec mycontainer /usr/local/bin/backup.sh
Error 2: No escapar el símbolo % en crontab
# ❌ Cron interpreta % como salto de línea
0 3 * * * /home/user/backup.sh > /var/log/backup_$(date +%Y%m%d).log
# ✅ Escapar con \%
0 3 * * * /home/user/backup.sh > /var/log/backup_$(date +\%Y\%m\%d).log
# ✅ Mejor: poner el date dentro del script, no en el crontab
Error 3: No redirigir salida y no saber por qué falló
# ❌ La salida se pierde (o va a MAILTO si está configurado)
0 3 * * * /home/user/script.sh
# ✅ Redirigir a log
0 3 * * * /home/user/script.sh >> /var/log/script.log 2>&1
Error 4: Confundir el shell de cron
# ❌ El shell por defecto de cron es /bin/sh, no /bin/bash
# Si el script usa sintaxis específica de bash ([[ ]], arrays, etc.)
0 3 * * * /home/user/script_con_arrays.sh # Falla en /bin/sh
# ✅ Fijar SHELL en crontab
SHELL=/bin/bash
0 3 * * * /home/user/script_con_arrays.sh
# ✅ O mejor: usar shebang correcto y hacer el script ejecutable
# El shebang #!/bin/bash prevalece sobre SHELL
Error 5: systemd — Olvidar hacer daemon-reload tras editar unidades
# ❌ Editas .service o .timer y systemctl no reconoce los cambios
sudo systemctl start mi-timer.timer
# Failed to start mi-timer.timer: Unit mi-timer.timer not found.
# ✅ Siempre recargar tras editar
sudo systemctl daemon-reload
sudo systemctl start mi-timer.timer
Error 6: No usar rutas absolutas en ExecStart de systemd
# ❌ Ruta relativa o comando sin ruta completa
[Service]
ExecStart=mi_script.sh # ¡FALLA!
# ✅ Ruta absoluta siempre
[Service]
ExecStart=/usr/local/bin/mi_script.sh
Error 7: Timer systemd no se activa por error en OnCalendar
# ❌ Sintaxis incorrecta (timer nunca se dispara)
[Timer]
OnCalendar=everyday # No es válido
# ✅ Sintaxis correcta
[Timer]
OnCalendar=daily
# O:
OnCalendar=*-*-* 00:00:00
# ✅ Validar antes
systemd-analyze calendar "daily"
Error 8: Confundir enable con start en systemd
# ❌ Solo enable (activa al arranque, pero no ahora)
sudo systemctl enable mi-timer.timer
# El timer no está activo hasta el próximo reinicio
# ✅ Hacer ambos: enable --now
sudo systemctl enable --now mi-timer.timer
# O por separado:
sudo systemctl enable mi-timer.timer
sudo systemctl start mi-timer.timer
Error 9: Asumir que cron respeta la zona horaria del usuario
# ❌ Cron usa la zona horaria del sistema (/etc/timezone), no la del usuario
# El usuario configura TZ=CUBA, pero cron ejecuta en UTC
# ✅ Fijar TZ explícitamente en el script o crontab
TZ=America/Mexico_City
0 9 * * * /home/user/script.sh
# ✅ O al inicio del script:
#!/bin/bash
export TZ="America/Mexico_City"
Error 10: No eliminar timers transitorios de systemd-run
# ❌ Los timers creados con systemd-run son transitorios pero
# pueden acumularse si no se limpian
sudo systemd-run --on-calendar='*:0/5' --unit=test-timer /bin/true
# ✅ Eliminar el timer transitorio cuando ya no se necesita
sudo systemctl stop test-timer.timer 2>/dev/null || true
sudo systemctl reset-failed test-timer.timer 2>/dev/null || true
# ✅ Ver qué timers transitorios hay
systemctl list-timers --all | grep run-
Conclusiones
| Concepto | Sintaxis / Comando | Propósito |
|---|---|---|
| Cron expresión | * * * * * (min hora día mes díaSem) | Programar tareas con 5 campos de tiempo |
| crontab -e | crontab -e | Editar tareas del usuario actual |
| crontab -l | crontab -l | Listar tareas del usuario |
| @reboot | @reboot /ruta/script | Ejecutar script al arranque |
| @daily | @daily /ruta/script | Ejecutar cada día a medianoche |
| @hourly | @hourly /ruta/script | Ejecutar cada hora |
| MAILTO | MAILTO=user@dom.com | Enviar salida de tareas por correo |
| Timer .service | [Service] Type=oneshot ExecStart= | Define qué ejecutar |
| Timer .timer | [Timer] OnCalendar=daily | Define cuándo ejecutar |
| systemctl enable –now | enable --now timer.timer | Activar timer (ahora y al arranque) |
| systemctl list-timers | list-timers --all | Listar todos los timers |
| journalctl -u | journalctl -u servicio.service | Ver log de un servicio |
| OnCalendar=daily | daily, hourly, weekly, monthly | Atajos predefinidos de calendario |
| OnCalendar completo | Mon *-*-* 09:00:00 | Sintaxis completa de calendario |
| Persistent=true | Persistent=true | Ejecutar tareas perdidas por apagado |
| WakeSystem=true | WakeSystem=true | Despertar sistema dormido |
| RandomizedDelaySec | RandomizedDelaySec=1h | Añadir retardo aleatorio |
| AccuracySec | AccuracySec=1m | Precisión de la ejecución |
| systemd-analyze calendar | systemd-analyze calendar "daily" | Validar y calcular próximas ejecuciones |
| systemd-run | systemd-run --on-calendar=... | Timer transitorio (sin archivos) |
| daemon-reload | systemctl daemon-reload | Recargar unidades tras editar |
Más información,
- Cron How-To (Arch Linux Wiki) — Guía completa de cron
- Systemd/Timers (Arch Linux Wiki) — La referencia definitiva de timers
- man systemd.time — Calendar Events — Documentación oficial de OnCalendar
- systemd-analyze verify — Validar unidades de systemd
- Crontab Generator — Generador interactivo de expresiones cron
- Systemd Timer Calculator — Calculadora interactiva de OnCalendar
- Digital Ocean — Cron vs Systemd Timers — Guía práctica de comparación
- Freedesktop — systemd.timer man page — Página de manual oficial
- «The Linux Programming Interface» de Michael Kerrisk — Capítulo de señalización y tiempos de espera, base conceptual para timers
- «UNIX and Linux System Administration Handbook» de Nemeth et al. — Capítulo sobre automatización y cron
- «Bash Cookbook» de Carl Albing — Recetas de automatización con cron
- «Systemd for Administrators» de Lennart Poettering — Serie de artículos que cubren timers en profundidad
- «Linux System Programming» de Robert Love — Conceptos subyacentes de tiempo y eventos en Linux