El daemon cron lleva décadas haciendo el mismo trabajo: leer una tabla de tareas y ejecutarlas en los momentos exactos que se le indican. No hay magia. En Debian, el proceso cron arranca con el sistema, se queda en segundo plano, y cada minuto comprueba si alguna entrada de algún crontab coincide con la hora actual. Si coincide, lanza el comando en un subshell. Así de mecánico.
Lo que hace que cron sea fiable y al mismo tiempo fuente de frustraciones es precisamente eso: el subshell que usa no hereda tu entorno. Cuando ejecutas algo en tu terminal tienes ~/.bashrc, ~/.profile, variables de entorno configuradas por tu gestor de paquetes, y un $PATH generoso. El subshell de cron arranca con un $PATH mínimo de /usr/bin:/bin y punto. Sin tu configuración. Sin nvm, sin pyenv, sin el Python del virtualenv que tienes en /opt. Esto explica el 80% de los casos en que “funciona en el terminal pero no en cron”.
Usas cron cuando necesitas que algo ocurra en un momento concreto o con una frecuencia fija: backups nocturnos, rotación de datos, envío de reportes, limpieza de temporales. No es la herramienta adecuada para tareas disparadas por eventos (para eso existe systemd con units .path o .socket), pero para scheduling temporal sigue siendo la opción más directa y portable.
Si te equivocas en la sintaxis o en el entorno, el comando falla en silencio a menos que hayas configurado dónde van los errores. No hay notificación por defecto más allá de intentar enviar un mail local, que en la mayoría de servidores nadie lee. Entender los logs y redirigir la salida correctamente es tan importante como la propia sintaxis.
La sintaxis de cinco campos
Cada línea del crontab sigue este formato:
minuto hora día_mes mes día_semana comando
Los campos aceptan:
– Valor específico: 5 — exactamente ese valor
– Wildcard: * — cualquier valor
– Lista: 1,3,5 — cualquiera de esos valores
– Rango: 1-5 — del 1 al 5 inclusive
– Step: */15 — cada 15 unidades; 1-30/5 — del 1 al 30 de 5 en 5
# Abrir el crontab del usuario actual (usa $EDITOR o vi por defecto) crontab -e # Ver el crontab actual sin editarlo crontab -l
Ejemplo completo
El escenario: un servidor con una aplicación Python en /opt/app. Necesitamos tres tareas: sincronizar datos cada 5 minutos durante el horario laboral, hacer un backup completo a las 3 AM, y limpiar logs temporales los domingos a medianoche.
# Contenido del crontab — editar con: crontab -e # # Definir PATH explícitamente porque cron no carga ~/.bashrc ni ~/.profile PATH=/usr/local/bin:/usr/bin:/bin # MAILTO vacío desactiva el envío de mails; redirigimos salida manualmente MAILTO="" # ── Sincronización cada 5 minutos, solo de lunes a viernes, 8h-18h ───────── # El campo día_semana usa 1=lunes ... 5=viernes # Las 8h-18h en "hora" con rango 8-17 cubre :00 hasta las 17:55 # A las 18:00 ya no ejecuta porque 18 no está en 8-17 */5 8-17 * * 1-5 /opt/app/bin/sync_data.py >> /var/log/app/sync.log 2>&1 # ── Backup completo a las 3:00 AM todos los días ──────────────────────────── # 0 en minutos garantiza que solo dispara en punto, no en cada minuto de las 3 0 3 * * * /opt/app/bin/backup.sh >> /var/log/app/backup.log 2>&1 # ── Limpieza de temporales cada domingo a medianoche ──────────────────────── # día_semana=0 es domingo (también se puede usar 7) 0 0 * * 0 find /tmp/app_cache -type f -mtime +7 -delete >> /var/log/app/cleanup.log 2>&1 # ── Al reiniciar el servidor, levantar el proceso de cola ─────────────────── # @reboot equivale a "0 0 1 1 *" pero solo en el boot, no en un momento fijo @reboot /opt/app/bin/start_worker.sh >> /var/log/app/worker_boot.log 2>&1
Para verificar los logs de ejecución:
# Ver qué ha ejecutado cron (incluye errores de sintaxis en el crontab) journalctl -u cron --since "2024-01-15 00:00" --until "2024-01-15 06:00" # Alternativa clásica si rsyslog está activo grep CRON /var/log/syslog | tail -50
Desglose del ejemplo
PATH=/usr/local/bin:/usr/bin:/bin al principio del crontab — esto es lo primero que hay que poner, siempre. Sin esto, /opt/app/bin/sync_data.py funcionaría si tiene shebang (#!/usr/bin/env python3), pero cualquier script que dentro llame a pip, python, o cualquier binario en /usr/local/bin fallará sin mensaje de error útil. Las variables de entorno definidas al inicio del crontab se aplican a todas las entradas.
MAILTO="" — por defecto cron intenta enviar un mail al usuario con la salida estándar de cada job. Si sendmail o msmtp no están configurados, esto genera errores silenciosos o colas de mail que nadie lee. Desactivarlo y redirigir la salida a ficheros de log es más manejable.
>> /var/log/app/sync.log 2>&1 en cada entrada — el 2>&1 es crítico. Sin él, la stderr (donde van los mensajes de error de Python, Bash, o cualquier comando) no se captura. Con >> en lugar de > los logs son acumulativos; si necesitas rotación, configura logrotate apuntando a esos ficheros.
*/5 8-17 * * 1-5 — el rango 8-17 en el campo de hora hace que la tarea ejecute a las 8:00, 8:05, …, 17:55, pero no a las 18:00. Si quisieras incluir las 18:00 cambiarías a 8-18. El campo 1-5 en día_semana son lunes a viernes. 0 y 7 son ambos domingo, comportamiento definido por el estándar POSIX.
@reboot es un string especial que cron interpreta como “una vez al arrancar el daemon”. Práctico para levantar procesos de usuario que no tienen una unit de systemd, pero con una limitación: si el daemon cron se reinicia (por ejemplo, después de apt upgrade), también se disparan los @reboot. Tenlo en cuenta si el script no es idempotente.
Crontabs de sistema vs. usuario — lo que acabamos de editar con crontab -e es /var/spool/cron/crontabs/<usuario>. Para tareas de sistema existe /etc/cron.d/, donde los ficheros tienen un campo extra entre los cinco campos temporales y el comando: el usuario que ejecuta el job. El formato sería */5 * * * * root /usr/local/sbin/mi_tarea.sh. Esto evita tener que hacer sudo crontab -e para el root.
Para scripts que no necesitan precisión de minutos, /etc/cron.daily/, /etc/cron.weekly/, y /etc/cron.monthly/ son carpetas donde run-parts ejecuta todos los scripts presentes. Los horarios exactos están definidos en /etc/crontab y en /etc/cron.d/e2scrub_all. Si lo que necesitas es “una vez al día, no me importa a qué hora exacta”, un script en cron.daily es más simple que calcular campos manualmente.
[Ubuntu]: En Ubuntu, el daemon se llama igualmente cron y el comportamiento es idéntico. La diferencia es que Ubuntu suele tener anacron más integrado para portátiles que no están siempre encendidos, y algunos scripts en cron.daily son específicos de Ubuntu (update-notifier). Los logs están en el mismo sitio: journalctl -u cron o /var/log/syslog.
N° 77