Volver al índice
root@vps-donweb:~/docs/cron$

Cron Jobs

herramienta cron / systemd timers
config /etc/crontab · crontab -e
categoría Sistema
ESTRUCTURA Anatomía de una línea cron
# ┌─ minuto        (0-59)
# │ ┌─ hora         (0-23)
# │ │ ┌─ día mes    (1-31)
# │ │ │ ┌─ mes      (1-12)
# │ │ │ │ ┌─ día semana (0-7, 0 y 7 = domingo)
# │ │ │ │ │
# * * * * *  comando a ejecutar

# Ejemplos rápidos
30 2 * * *     /ruta/script.sh  # cada día a las 2:30 AM
0  */6 * * *   /ruta/script.sh  # cada 6 horas
0  9 * * 1     /ruta/script.sh  # lunes a las 9 AM
0  0 1 * *     /ruta/script.sh  # primer día de cada mes
*/15 * * * *   /ruta/script.sh  # cada 15 minutos
Minuto
Hora
Día mes
Mes
Día sem.
0–59
0–23
1–31
1–12
0–7
* = cualquiera
*/n = cada n
1,3,5 = lista
1-5 = rango
0 y 7 = dom
OPERADORES Caracteres especiales
#  *   = cualquier valor (wildcard)
* * * * *      # cada minuto

#  ,   = lista de valores
0 9,13,18 * * *  # a las 9, 13 y 18 hs
0 0 * * 1,3,5   # lunes, miércoles y viernes a medianoche

#  -   = rango
0 9-17 * * 1-5  # cada hora de 9 a 17, de lunes a viernes

#  /   = cada N (paso)
*/10 * * * *    # cada 10 minutos
0 */4 * * *     # cada 4 horas
0 0 */2 * *     # cada 2 días a medianoche

#  @   = atajos predefinidos
@reboot         # al iniciar el sistema
@hourly         # cada hora           = 0 * * * *
@daily          # cada día            = 0 0 * * *
@midnight       # medianoche          = 0 0 * * *
@weekly         # cada semana         = 0 0 * * 0
@monthly        # cada mes            = 0 0 1 * *
@yearly         # cada año            = 0 0 1 1 *
EJEMPLOS Referencias rápidas de expresiones comunes
# Cada minuto
* * * * *

# Cada hora en punto
0 * * * *

# Cada día a medianoche
0 0 * * *

# Cada día a las 3:30 AM
30 3 * * *

# Cada 15 minutos
*/15 * * * *

# Cada 6 horas
0 */6 * * *

# Lunes a las 8 AM
0 8 * * 1

# Lunes a viernes a las 9 AM
0 9 * * 1-5

# Primer día de cada mes a las 2 AM
0 2 1 * *

# Cada 30 minutos entre las 8 y las 18
*/30 8-18 * * *

# Los domingos a las 4 AM
0 4 * * 0

# Al reiniciar el sistema
@reboot

# El 1 de enero a medianoche
0 0 1 1 *

Para validar expresiones cron antes de usarlas: crontab.guru — pegar la expresión y ver en lenguaje natural qué hace.

CRONTAB Gestionar el crontab del usuario
# Editar el crontab del usuario actual
crontab -e

# Editar el crontab de otro usuario (como root)
crontab -u www-data -e

# Ver el crontab actual sin editar
crontab -l
crontab -u www-data -l

# Eliminar todos los cron jobs del usuario
crontab -r

# Exportar crontab a archivo (para backup)
crontab -l > ~/crontab_backup_$(date +%Y%m%d).txt

# Importar crontab desde archivo
crontab ~/crontab_backup.txt

# Ver crontabs de todos los usuarios
for USER in $(cut -f1 -d: /etc/passwd); do
  CRON=$(crontab -u $USER -l 2>/dev/null)
  [ -n "$CRON" ] && echo "=== $USER ===" && echo "$CRON"
done

El editor que abre crontab -e depende de la variable EDITOR. Para usar nano: EDITOR=nano crontab -e

ARCHIVOS Ubicaciones de cron en el sistema
# Crontab personal de cada usuario
/var/spool/cron/crontabs/root
/var/spool/cron/crontabs/www-data
/var/spool/cron/crontabs/mi_usuario

# Crontab del sistema (tiene columna de usuario)
/etc/crontab

# Directorios con scripts que cron ejecuta automáticamente
/etc/cron.d/        # archivos con formato crontab completo
/etc/cron.hourly/   # scripts que corren cada hora
/etc/cron.daily/    # scripts que corren cada día
/etc/cron.weekly/   # scripts que corren cada semana
/etc/cron.monthly/  # scripts que corren cada mes
# Formato de /etc/crontab y archivos en /etc/cron.d/
# (igual que crontab personal pero con columna de usuario)

# min hora día mes dsem USUARIO comando
30 2 * * * root /usr/local/bin/backup-diario.sh
0  8 * * 1 www-data /usr/local/bin/limpiar-cache.sh

Para tareas del sistema, preferí crear un archivo en /etc/cron.d/ con un nombre descriptivo en lugar de editar /etc/crontab directamente.

SERVICIO Gestionar el demonio cron
# Ver estado del servicio cron
systemctl status cron

# Recargar después de cambios manuales en /etc/crontab
systemctl reload cron

# Reiniciar si hay problemas
systemctl restart cron

# Habilitar inicio automático
systemctl enable cron

# Ver logs del demonio cron
journalctl -u cron -f
journalctl -u cron --since "1 hour ago"
grep CRON /var/log/syslog | tail -30
ENTORNO El problema más común con cron

Cron ejecuta los scripts en un entorno muy básico — sin las variables de entorno ni el PATH de tu sesión SSH. Un script que funciona en la terminal puede fallar en cron por este motivo.

# El PATH de cron por defecto es mínimo:
# PATH=/usr/bin:/bin
# No incluye /usr/local/bin, /usr/sbin, etc.

# Variables que cron NO tiene automáticamente:
# - HOME (o puede ser distinto al esperado)
# - USER
# - Variables definidas en .bashrc / .bash_profile
# - Variables de entorno de la app (.env)
# - nvm, pyenv, rbenv — no se cargan solos

Si un script falla en cron pero funciona en la terminal, el 90% de las veces es un problema de PATH o variables de entorno.

SOLUCIONES Cómo manejar el entorno correctamente
# OPCIÓN 1: Definir PATH al inicio del crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

0 2 * * * /usr/local/bin/mi-script.sh
# OPCIÓN 2: Usar rutas absolutas en el script y en cron
0 2 * * * /usr/bin/php /var/www/mi-app/artisan schedule:run
0 2 * * * /usr/local/bin/node /var/www/mi-app/cron.js
# OPCIÓN 3: Sourcing del entorno al inicio del script
#!/bin/bash
# Cargar entorno del sistema
source /etc/environment
source /etc/profile

# Cargar variables de la app
set -a
source /var/www/mi-app/.env
set +a

# Agregar rutas de herramientas de versión
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"

# El resto del script...
# OPCIÓN 4: Usar bash -l (login shell) para cargar el entorno completo
0 2 * * * bash -l -c '/usr/local/bin/mi-script.sh'

La opción más robusta es siempre usar rutas absolutas tanto en el crontab como dentro de los scripts.

VARIABLES Variables de entorno en crontab
# Definir variables al inicio del crontab (aplican a todos los jobs)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=tu@email.com        # email para recibir output (vacío = silencio)
HOME=/root

# Deshabilitar emails de output (para jobs silenciosos)
MAILTO=""

# Job que sí envía email solo si hay error
0 2 * * * /usr/local/bin/backup.sh 2>&1 | mail -s "Backup" tu@email.com

# Silenciar completamente el output de un job
* * * * * /usr/local/bin/check.sh > /dev/null 2>&1

# Guardar output en log y también enviarlo
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Por defecto, cron envía el output de cada job por email al usuario. Con MAILTO="" al inicio del crontab lo silenciás globalmente.

LOGS Ver la actividad de cron
# Ver logs de cron en tiempo real
journalctl -u cron -f

# Ver todas las ejecuciones de hoy
journalctl -u cron --since today

# Buscar en syslog (método clásico)
grep CRON /var/log/syslog
grep CRON /var/log/syslog | tail -50
grep CRON /var/log/syslog | grep "mi-script"

# Ver si un job específico corrió
grep "backup-diario" /var/log/syslog

# Output de cron en syslog se ve así:
# May 28 02:00:01 vps CRON[12345]: (root) CMD (/usr/local/bin/backup-diario.sh)

Cron solo registra que ejecutó el comando, no su output. Para ver si el script funcionó correctamente, redirigí su output a un archivo de log.

LOGGING Agregar logs propios a los scripts
# Redirigir todo el output al log del sistema
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# Con timestamp en el log
0 2 * * * /usr/local/bin/backup.sh 2>&1 \
  | while IFS= read -r line; \
    do echo "[$(date '+%Y-%m-%d %H:%M:%S')] $line"; \
    done >> /var/log/backup.log

# Patrón dentro del script para logs con timestamp
#!/bin/bash
LOG="/var/log/mi-script.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG"; }

log "=== Inicio de ejecución ==="
log "Procesando archivos..."
log "=== Fin OK ==="
# Rotación del log para que no crezca sin límite
# /etc/logrotate.d/mi-script
/var/log/mi-script.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    create 640 root adm
}
DEBUG Diagnosticar cron jobs que no funcionan
# PASO 1: Verificar que cron está corriendo
systemctl status cron

# PASO 2: Probar el script manualmente como root
bash /usr/local/bin/mi-script.sh

# PASO 3: Simular el entorno de cron
env -i HOME=/root PATH=/usr/bin:/bin \
  bash /usr/local/bin/mi-script.sh

# PASO 4: Verificar que el script tiene permisos de ejecución
ls -la /usr/local/bin/mi-script.sh
chmod +x /usr/local/bin/mi-script.sh

# PASO 5: Verificar que la primera línea es correcta
head -1 /usr/local/bin/mi-script.sh
# Debe ser: #!/bin/bash o #!/bin/sh

# PASO 6: Buscar errores en syslog después de la ejecución
grep CRON /var/log/syslog | tail -20

# PASO 7: Capturar el output del job temporalmente
* * * * * /usr/local/bin/mi-script.sh > /tmp/cron-debug.log 2>&1
# Esperar 1 minuto y revisar:
cat /tmp/cron-debug.log

# PASO 8: Verificar la sintaxis del crontab
crontab -l
# Debe terminar con una línea en blanco al final

Una causa muy frecuente de fallo silencioso: el crontab no termina en una línea en blanco. Cron ignora la última línea si no tiene newline al final.

LOCK Evitar ejecuciones simultáneas

Si un job tarda más que su intervalo de ejecución, cron lanza otra instancia mientras la anterior sigue corriendo. Esto puede causar conflictos en scripts de backup, deploy, etc.

# Método 1: flock — bloqueo a nivel de archivo
*/5 * * * * flock -n /tmp/mi-script.lock \
  /usr/local/bin/mi-script.sh

# Método 2: con timeout (máximo 4 minutos de ejecución)
*/5 * * * * flock -n -w 0 /tmp/mi-script.lock \
  timeout 240 /usr/local/bin/mi-script.sh

# Método 3: dentro del propio script con PID file
#!/bin/bash
LOCKFILE="/tmp/mi-script.pid"

if [ -f "$LOCKFILE" ]; then
  PID=$(cat "$LOCKFILE")
  if kill -0 "$PID" 2>/dev/null; then
    echo "Ya está corriendo (PID $PID), saliendo"
    exit 0
  fi
fi

echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT

# El resto del script...
SYSTEMD Timers vs Cron — cuándo usar cada uno
CRON — preferir cuando:
  ✓ Tareas simples de una línea
  ✓ El equipo ya está familiarizado con crontab
  ✓ Compatibilidad con sistemas sin systemd
  ✓ Instalación y configuración rápida

SYSTEMD TIMERS — preferir cuando:
  ✓ Necesitás logs integrados (journalctl)
  ✓ Dependencias entre servicios (After=mariadb.service)
  ✓ Recuperar ejecuciones perdidas (Persistent=true)
  ✓ Timeout automático de la tarea
  ✓ Control de reintentos en caso de fallo
  ✓ Monitorear el estado con systemctl
CREAR Crear un timer de systemd paso a paso

Un timer requiere dos archivos: el service (qué ejecutar) y el timer (cuándo ejecutarlo).

# 1. Crear el archivo de servicio
# /etc/systemd/system/limpiar-cache.service
[Unit]
Description=Limpieza de caché de la app
After=network.target

[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/mi-app
ExecStart=/usr/bin/php artisan cache:clear
StandardOutput=journal
StandardError=journal
# 2. Crear el archivo de timer
# /etc/systemd/system/limpiar-cache.timer
[Unit]
Description=Timer para limpieza de caché
Requires=limpiar-cache.service

[Timer]
OnCalendar=*-*-* 03:00:00    # diario a las 3 AM
Persistent=true
RandomizedDelaySec=2min

[Install]
WantedBy=timers.target
# 3. Activar
systemctl daemon-reload
systemctl enable limpiar-cache.timer
systemctl start  limpiar-cache.timer

# 4. Verificar
systemctl list-timers limpiar-cache.timer
systemctl status limpiar-cache.timer

# 5. Ver logs de la última ejecución
journalctl -u limpiar-cache.service -n 30

# 6. Ejecutar manualmente para probar
systemctl start limpiar-cache.service
ONCALENDAR Sintaxis de fechas en systemd
# Formato: DíaSemana Año-Mes-Día Hora:Minuto:Segundo

# Diario a las 2 AM
OnCalendar=*-*-* 02:00:00

# Cada hora
OnCalendar=hourly

# Cada lunes a las 9 AM
OnCalendar=Mon *-*-* 09:00:00

# Primer día de cada mes
OnCalendar=*-*-01 00:00:00

# Cada 15 minutos
OnCalendar=*:0/15

# Lunes a viernes a las 8 AM
OnCalendar=Mon..Fri *-*-* 08:00:00

# Verificar expresión antes de usarla
systemd-analyze calendar "Mon *-*-* 09:00:00"

# Ver todos los timers activos y próxima ejecución
systemctl list-timers --all
CRONTAB Crontab completo para un VPS web típico
# /etc/cron.d/vps-tasks
# Tareas automáticas del VPS

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# ── BACKUPS ─────────────────────────────────────────────────
# Backup completo: bases de datos + archivos + configs (2 AM)
0 2 * * *    root  /usr/local/bin/backup-diario.sh >> /var/log/backup.log 2>&1

# Verificar que el backup se hizo correctamente (8:30 AM)
30 8 * * *   root  /usr/local/bin/verificar-backups.sh

# ── MANTENIMIENTO ───────────────────────────────────────────
# Renovar certificados SSL (Let's Encrypt)
0 3 * * *    root  certbot renew --quiet --post-hook "systemctl reload nginx"

# Rotar logs de Nginx
0 0 * * *    root  /usr/sbin/logrotate /etc/logrotate.conf

# Actualizar base de datos de fail2ban
@daily       root  fail2ban-client reload

# Limpiar sesiones PHP viejas
*/30 * * * * root  find /tmp -name "sess_*" -mmin +60 -delete 2>/dev/null

# ── LIMPIEZA ────────────────────────────────────────────────
# Eliminar logs viejos (más de 30 días)
0 4 * * 0    root  find /var/log/nginx -name "*.gz" -mtime +30 -delete

# Limpiar imágenes Docker sin usar (domingos a las 4 AM)
0 4 * * 0    root  docker system prune -f >> /var/log/docker-prune.log 2>&1

# ── MONITOREO ───────────────────────────────────────────────
# Verificar SSL expiry mensualmente
0 9 1 * *    root  /usr/local/bin/check-ssl-expiry.sh

# Reporte semanal de uso de disco (lunes 8 AM)
0 8 * * 1    root  df -h | mail -s "Uso de disco VPS" tu@email.com
LARAVEL Scheduler de Laravel con cron

Laravel tiene su propio scheduler interno. Solo necesitás un único job en cron que llame al scheduler cada minuto — Laravel se encarga del resto.

# Un solo job en crontab para toda la app Laravel
* * * * * www-data cd /var/www/mi-laravel && php artisan schedule:run >> /dev/null 2>&1

# Con ruta absoluta (más robusto en cron)
* * * * * www-data /usr/bin/php /var/www/mi-laravel/artisan schedule:run >> /dev/null 2>&1

# En app/Console/Kernel.php se definen las tareas:
# $schedule->command('backup:run')->daily()->at('02:00');
# $schedule->command('cache:clear')->hourly();
# $schedule->job(new ProcessEmails)->everyFiveMinutes();

# Ver las tareas programadas definidas en Laravel
php artisan schedule:list

# Ejecutar el scheduler manualmente
php artisan schedule:run

# Correr en modo verbose (ver qué está ejecutando)
php artisan schedule:run -v
NODE Cron jobs en Node.js con node-cron
# Opción A: usar cron del sistema para Node (más simple)
0 * * * * www-data /usr/bin/node /var/www/mi-app/cron/hourly.js >> /var/log/node-cron.log 2>&1

# Opción B: node-cron dentro de la app (programático)
# npm install node-cron
// cron/tasks.js
const cron = require('node-cron');

// Cada día a las 2 AM
cron.schedule('0 2 * * *', async () => {
  console.log('Ejecutando backup...');
  await runBackup();
}, {
  timezone: 'America/Argentina/Buenos_Aires'
});

// Cada 15 minutos
cron.schedule('*/15 * * * *', () => {
  processQueue();
});

Si la app Node corre con PM2, los cron internos se reinician solos si la app cae. Si usás cron del sistema, asegurate de que el script Node existe y tiene permisos correctos.

PYTHON Tareas programadas en Python
# Opción A: cron del sistema ejecuta script Python
0 6 * * * www-data /var/www/mi-app/venv/bin/python /var/www/mi-app/tasks/daily.py >> /var/log/python-tasks.log 2>&1

# Importante: usar el Python del virtualenv, no el del sistema
# ✓ /var/www/mi-app/venv/bin/python
# ✗ python3  (puede ser otro venv o versión diferente)

# Opción B: APScheduler dentro de la app Django/Flask
# pip install apscheduler
# tasks/scheduler.py
from apscheduler.schedulers.background import BackgroundScheduler

scheduler = BackgroundScheduler(timezone="America/Argentina/Buenos_Aires")

@scheduler.scheduled_job('cron', hour=2, minute=0)
def nightly_backup():
    print("Ejecutando backup...")

@scheduler.scheduled_job('interval', minutes=15)
def process_queue():
    print("Procesando cola...")

scheduler.start()
SEGURIDAD Buenas prácticas en cron
# 1. Limitar quién puede usar cron
# /etc/cron.allow — solo los usuarios listados pueden usar cron
echo "root" > /etc/cron.allow
echo "deploy" >> /etc/cron.allow

# 2. Nunca poner contraseñas directamente en el crontab
# MAL:
0 2 * * * mysqldump -u root -pMiContraseña mi_base > backup.sql
# BIEN: leer de archivo con permisos restrictivos
0 2 * * * /usr/local/bin/backup.sh  # el script lee /etc/mysql/backup.pass

# 3. Ejecutar con el usuario mínimo necesario (no siempre root)
0 2 * * *  www-data  /var/www/mi-app/scripts/limpiar.sh

# 4. Verificar permisos de los scripts
chmod 750 /usr/local/bin/backup.sh   # solo root puede ejecutar
chown root:root /usr/local/bin/backup.sh

# 5. Agregar set -euo pipefail al inicio de los scripts
# para que fallen ruidosamente ante errores
#!/bin/bash
set -euo pipefail