SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

Por qué no se ejecuta su tarea de cron

Revise cinco causas frecuentes: PATH mínimo, % sin escapar, crontab incorrecto, salida enviada por correo y scripts que dependen de su shell.

Por qué no se ejecuta su tarea de cron

Una tarea de cron que «nunca se ejecuta» casi siempre se ha ejecutado. Se ejecutó en un entorno distinto de su shell, falló durante el primer segundo y el mensaje se envió a un lugar que no está revisando. Cinco causas explican casi todos los casos: la ruta de búsqueda, el signo de porcentaje, el archivo crontab incorrecto, la salida enviada por correo y un script que espera una sesión de inicio de sesión.

cron es un daemon (un servicio en segundo plano) que lee los archivos crontab e inicia comandos según una programación. No lee su .bashrc, no abre un terminal, no inicia un shell de inicio de sesión y no le informa cuando falla un comando. Todas las causas siguientes se derivan de esos cuatro hechos.

Revise estas causas en orden y empiece por la pregunta que subyace a todas ellas: ¿cron se activó? «cron nunca inició la tarea» y «la tarea se inició y terminó» son problemas distintos que no tienen nada en común. Responda primero a esa pregunta.

¿Se ejecutó cron?

El daemon tiene un nombre de unidad diferente según la familia de distribuciones. Compruebe ambos y después lea el registro.

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

Debian y Ubuntu llaman cron a la unidad. Fedora, Rocky y Alma la llaman crond. En una máquina determinada sólo existe uno de esos nombres, por lo que es normal que uno de los dos comandos informe de una unidad desconocida. No es un error.

Lea las entradas que haya escrito su propio sistema. No busque una línea copiada de una guía, porque el texto varía entre las implementaciones de cron y las configuraciones de registro. Sólo debe comprobar dos cosas: si existe una entrada en el minuto indicado por su programación y si esa entrada incluye el nombre de su comando. Una entrada que incluye el nombre de su comando significa que cron hizo su parte y que el fallo está dentro del comando. La ausencia total de entradas significa que cron nunca cargó su programación. Esa es la causa 3 que se explica a continuación.

Algunas imágenes envían los mensajes de cron mediante rsyslog a un archivo en lugar de enviarlos al journal. Busque en /var/log un archivo cuyo nombre haga referencia a cron o syslog y lea el final del archivo.

ls -l /var/log
sudo tail -n 50 /var/log/syslog

Si no existe ni la unidad ni el registro, es posible que cron no esté instalado. Las imágenes mínimas de servicios cloud y los contenedores suelen omitirlo.

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

Causa 1: cron no tiene su PATH

El shell interactivo construye PATH a partir de /etc/profile, ~/.profile, ~/.bashrc y de todo lo que esos archivos cargan. Nada de eso se ejecuta para un trabajo de cron. cron inicia el comando con su propio entorno reducido, por lo que no encuentra un programa ubicado fuera de los directorios estándar del sistema. Cualquier elemento situado en /usr/local/bin, /opt, un gestor de versiones de lenguajes, un entorno virtual de Python o un espacio de trabajo de Go puede ser la causa. El trabajo falla en su primera línea y el shell escribe un error del tipo "not found". El texto exacto depende del shell que lo haya ejecutado.

Busque la ruta real de cada comando que use el trabajo.

command -v docker
command -v node
readlink -f "$(command -v node)"

Después, escriba esas rutas absolutas en el trabajo o establezca PATH una sola vez al principio del crontab.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

Obtenga esa lista en su propia máquina con echo "$PATH" y elimine todo lo que exista únicamente dentro de una sesión interactiva. Aquí se aplica una regla importante: cron no expande variables en estas líneas de asignación. PATH=$PATH:/usr/local/bin almacena el texto literal $PATH:/usr/local/bin, por lo que el trabajo termina con una ruta de búsqueda que no contiene ningún directorio utilizable. Escriba la lista completa.

Un gestor de versiones necesita algo más que una ruta. nvm, pyenv, rbenv y asdf instalan una función de shell o un directorio de shims desde .bashrc, y un trabajo de cron nunca lee ese archivo. Llame al binario versionado mediante su ruta absoluta o cargue el script de inicialización del gestor como primera línea de su propio script.

Causa 2: el signo de porcentaje termina el comando

En el campo de comando de un crontab, % no es un carácter ordinario. El primer % sin escapar termina el comando. Todo lo que aparece después se entrega al comando como entrada estándar, y cada % adicional se convierte en una nueva línea. Esta es una función real de cron para proporcionar entradas breves a un programa. También explica por qué un nombre de archivo con fecha es la entrada de crontab que suele fallar.

Escriba 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site y tar nunca recibe una fecha formateada. cron corta la línea en el primer %. Por eso, el shell recibe una sustitución de comandos incompleta y el resto de la línea llega como entrada estándar. Escápese cada signo de porcentaje con una barra inversa.

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

Dos capas leen esa línea, en este orden. \% es una regla de cron que cron aplica antes de iniciar cualquier proceso. $(date +\%F) es sustitución de comandos, que el shell iniciado por cron aplica después. La clave consiste en saber qué capa interpreta cada carácter.

La práctica más segura es mantener toda la lógica fuera del crontab. Colóquela en un script, donde el signo de porcentaje no tiene ningún significado especial.

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

La línea del crontab sólo contiene una ruta y una redirección. Un crontab que se puede leer de un vistazo también se puede depurar.

Causa 3: ¿qué crontab editó?

No existe un único crontab. Hay varios archivos, con distintos propietarios y distinto número de campos. Un trabajo escrito en el archivo incorrecto no se ejecuta y puede pasar inadvertido.

  • crontab -e edita el crontab del usuario que ejecuta el comando. sudo crontab -e edita el de root. Dos personas que depuran el mismo equipo suelen terminar leyendo dos archivos distintos.
  • sudo crontab -l -u deploy muestra el crontab de otro usuario. Así puede confirmar qué está instalado para la cuenta que debe ejecutar el trabajo.
  • /etc/crontab y todos los archivos de /etc/cron.d tienen un campo adicional entre la planificación y el comando: el usuario con el que se ejecutará. Si pega una línea de cinco campos de un crontab de usuario en /etc/cron.d, la primera palabra del comando se interpreta como un nombre de usuario.
  • Los archivos de /etc/cron.d deben tener nombres compuestos por letras, dígitos, guiones bajos y guiones. Un archivo llamado backup.sh o site.conf se omite sólo por su nombre. Cámbiele el nombre a backup y vuelva a revisar el registro.
  • Los archivos de /etc/cron.d deben pertenecer a root y no deben tener permisos de escritura para el grupo ni para otros usuarios. ls -l /etc/cron.d muestra ambos datos a la vez.
  • Los scripts colocados en /etc/cron.daily y sus directorios equivalentes siguen la misma regla de nombres y también deben tener el bit de ejecución. Si falta el bit de ejecución, se omiten sin mostrar ningún error.
  • /etc/cron.allow y /etc/cron.deny determinan quién puede instalar un crontab. Si alguno existe en su equipo, léalo antes de suponer que su usuario tiene permiso para instalar uno.

Instale un crontab de usuario con el comando crontab en lugar de editar manualmente el archivo de spool, porque crontab analiza el archivo antes de instalarlo. Al guardar, lea lo que el comando muestra. Si rechaza el archivo, la versión anterior sigue activa y el cambio nunca se aplica. Esto parece exactamente que cron ignora sus instrucciones.

El propietario también determina los permisos. Un trabajo del crontab de root crea archivos propiedad de root que la aplicación que los lee quizá no pueda modificar. Un trabajo del crontab de un usuario normal no puede leer un directorio accesible sólo para root. Asigne el propietario según el trabajo: el mantenimiento de una aplicación debe ejecutarse con la cuenta de la propia aplicación. Este es el motivo de reemplazar wp-cron de WordPress por un trabajo de cron del sistema. El modo de los archivos que crea el trabajo procede del umask que hereda. Ese valor tampoco es el de su shell. Por eso conviene leer cómo umask establece los permisos de los archivos si la salida de un trabajo queda sin permisos de lectura.

Causa 4: la salida se envía a un buzón que nadie lee

cron recopila todo lo que un trabajo escribe en la salida estándar y en el error estándar. Si el trabajo escribió algo, cron entrega ese texto al sistema de correo local, dirigido al propietario del crontab o a lo que indique MAILTO. En un VPS básico normalmente no hay ningún MTA (agente de transferencia de correo) instalado, por lo que nada entrega ese mensaje. El error existió durante un momento y después no llegó a ninguna parte. Esa es la razón por la que un trabajo defectuoso parece no producir ninguna salida.

Envíe la salida a un archivo que pueda controlar.

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> añade la salida estándar al archivo. 2>&1 dirige el error estándar al destino al que apunte la salida estándar en ese momento, por lo que debe ir después de la redirección. Si se escribe en el orden contrario, como 2>&1 >> file, el error estándar conserva su destino original y el error que está buscando es precisamente la parte que nunca llega al archivo.

El journal es el otro destino adecuado. logger escribe en syslog con la etiqueta que elija.

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

Vuelva a leerlo con journalctl -t backup-site. Así, la salida del propio trabajo queda junto a las entradas de cron y es fácil seguir la secuencia temporal. Si también necesita registrar qué persona ejecutó cada comando en el sistema, se trata de otro mecanismo, que se explica en auditar los comandos de usuario en el servidor.

MAILTO="" al principio de un crontab desactiva el correo para los trabajos que aparecen debajo. Establecer MAILTO en una dirección real sólo sirve si existe un MTA operativo, así que compruebe que el correo sale del sistema antes de depender de él.

Durante la depuración, siga una regla: no añada nunca > /dev/null 2>&1. Es la línea más habitual de todos los crontab y descarta la única evidencia disponible. Vuelva a añadirla más tarde, si quiere, cuando el trabajo funcione.

Causa 5: el script supone un entorno que cron no le proporciona

Una vez localizado el comando y capturada su salida, queda todo lo demás que la sesión le proporciona automáticamente.

  • Es posible que el shell no sea bash. Compruébelo con ls -l /bin/sh. En Debian y Ubuntu apunta a dash, por lo que la prueba con dobles corchetes, los arrays y source fallan con un error de sintaxis. Añada al script una línea #!/bin/bash y ejecute el script, o establezca SHELL al principio del crontab.
  • El directorio de trabajo no es aquel en el que se encontraba. Use rutas absolutas en todo el script, o cd al directorio en la primera línea del script. Una ruta relativa es la causa más habitual de que un trabajo «funcione cuando lo ejecuto manualmente».
  • La configuración regional no es la de su sesión. Cualquier comando que dé formato a una fecha o un número, o que ordene texto, puede producir una salida diferente con otro LANG. Si un paso posterior analiza esa salida, establezca la configuración regional en el script en lugar de darla por supuesta.
  • No existe una TTY (terminal). Un comando que solicite confirmación, abra un editor o muestre una barra de progreso puede quedar bloqueado o terminar. Añada la opción no interactiva que ofrezca la herramienta.
  • No existe un agente SSH. SSH_AUTH_SOCK no está presente en el entorno de cron, por lo que un comando ssh o rsync que funcionaba porque el agente estaba cargado ahora falla al autenticarse. Asigne al trabajo su propia clave, propiedad del usuario que ejecuta el trabajo.
  • No existe un bus de sesión de usuario, por lo que systemctl --user de un trabajo de cron falla hasta que se establezca XDG_RUNTIME_DIR. Una unidad del sistema es una solución mejor.

En Fedora, Rocky y Alma hay otra posibilidad. SELinux confina los trabajos de cron, por lo que deniega un trabajo que accede a una ruta con una etiqueta inesperada, aunque los permisos del archivo parezcan correctos. Busque las denegaciones con sudo ausearch -m avc -ts recent y lea Conceptos básicos de SELinux para un servidor antes de desactivarlo.

La prueba de un minuto que muestra el entorno de cron

Deje de adivinar qué contiene el entorno de cron y léalo. Escriba un script que vuelque todo, prográmelo cada minuto, espere y después lea el archivo.

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

Añada una línea al crontab del usuario con el que se ejecuta el trabajo real, usando rutas absolutas a ambos lados.

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

Espere un minuto y lea /home/deploy/cron-probe.log. Compárelo con los mismos comandos ejecutados en su propia shell. La línea PATH, el directorio de trabajo y la configuración regional suelen explicar por sí solos el fallo. Observe dos detalles de esta configuración: los signos de porcentaje están dentro del script, donde no se aplica la regla de cron, y la ruta del registro permite escribir al usuario del trabajo.

Elimine esa línea del crontab en cuanto tenga la respuesta. Un trabajo que se ejecuta cada minuto y añade contenido a un archivo llenará un disco pequeño, y lo hará sin generar avisos.

¿La programación es la que pretendía?

Una línea del crontab de un usuario comienza con cinco campos: minuto, hora, día del mes, mes y día de la semana. Dos de ellos interactúan de una forma que suele causar confusión.

Cuando el día del mes y el día de la semana están restringidos, es decir, ninguno de los dos es *, cron ejecuta la tarea cuando coincide cualquiera de los dos campos. 0 0 13 * 5 no significa «viernes 13». Se ejecuta a medianoche el día 13 de cada mes y a medianoche todos los viernes. Para obtener un día específico, deje uno de los dos campos como * y compruebe el otro dentro del script.

cron usa la zona horaria del sistema. Muchas imágenes de VPS vienen configuradas con UTC (tiempo universal coordinado), por lo que una tarea programada para las 03:00 se ejecuta a las 03:00 UTC, que puede ser la mitad de la tarde en su zona horaria. timedatectl muestra la configuración que usa realmente su servidor. Consulte ese valor en lugar de asumir que coincide con el de su portátil.

Conviene conocer otras dos trampas de las programaciones. @reboot se ejecuta cuando se inicia cron, que no es necesariamente el momento en que la red ya está disponible. Por eso, una tarea que necesita DNS o un host remoto puede fallar durante el arranque y ejecutarse correctamente en todas las ejecuciones manuales posteriores. Además, nada impide que una tarea lenta vuelva a iniciarse mientras la copia anterior sigue ejecutándose. Envuélvala en un bloqueo.

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

flock -n termina inmediatamente cuando el bloqueo ya está activo, por lo que la ejecución simultánea se detiene en lugar de acumularse sobre la primera.

Cuándo systemd timer es la herramienta adecuada

cron sirve bien para una cosa: ejecutar este comando a esta hora. En todo lo demás es limitado. Un timer proporciona el journal sin necesidad de redirigir la salida, un estado de salida que puede consultar más tarde, ordenación respecto a network-online.target y un retraso aleatorio para que cien servidores no se inicien en el mismo segundo. Cuando el trabajo necesita alguna de estas funciones, un servicio y un timer de systemd en un VPS requieren menos trabajo que mantener una línea de crontab. El comportamiento de los reintentos también debe definirse ahí, porque las políticas de reinicio de systemd determinan qué ocurre después de un fallo, mientras que cron no ofrece ninguna respuesta a esa situación.

Mantenga cron para los trabajos pequeños. Traslade a un timer todo lo que tenga dependencias o una política de reintentos. Ambos pueden ejecutarse en el mismo servidor, por lo que no tiene que completar esta migración en una sola sesión.

FAQ

¿Por qué mi tarea de cron funciona manualmente, pero falla cuando la ejecuta cron?

Porque el entorno de su shell y el de cron son diferentes. Su shell de inicio de sesión lee /etc/profile y ~/.bashrc, que establecen PATH, la configuración regional y las variables de su agente. cron inicia el comando sin nada de eso, desde un directorio de trabajo diferente y, en ocasiones, con otro shell. Use rutas absolutas para cada comando, establezca lo necesario al principio del crontab o dentro del script y programe una tarea de prueba que se ejecute cada minuto. Haga que ejecute env | sort, pwd y id y escriba la salida en un archivo de registro. Así podrá leer el entorno real de cron en lugar de hacer suposiciones.

¿Cómo compruebo si cron ejecutó realmente mi tarea?

Lea el registro del daemon. Use journalctl -u cron en Debian y Ubuntu, o journalctl -u crond en Fedora, Rocky y Alma. Algunas imágenes envían los mensajes mediante rsyslog a un archivo situado bajo /var/log. Busque una entrada en el minuto indicado por la programación y compruebe que incluya el nombre de su comando. Si no hay ninguna entrada, cron nunca recibió esa programación. Confirme que modificó el crontab correcto. Una entrada sin resultado indica que el comando se inició y terminó con un error. Capture su salida mediante una redirección.

¿Por qué date +%Y falla dentro de un crontab?

cron trata % como un carácter especial en el campo del comando. El primer % no escapado termina el comando. Todo lo que aparece después se pasa a ese comando como entrada estándar, y cada % adicional se convierte en un salto de línea. Por eso, un nombre de archivo con la fecha formateada nunca llega al programa para el que lo escribió. Escape cada signo de porcentaje como \%, o mueva el comando a un script y llame al script desde cron. Dentro de un script, el signo de porcentaje no tiene un significado especial.

¿Adónde va la salida de mi tarea de cron?

Al sistema de correo local, dirigida al propietario del crontab o a la dirección que indique MAILTO. La mayoría de las imágenes de VPS no tienen instalado ningún agente de transferencia de correo, por lo que el mensaje se descarta y la tarea parece no producir salida. Redirija la salida a un archivo con >> /path/to/log 2>&1. Mantenga ese orden para que el error estándar siga a la salida estándar. También puede canalizarla mediante logger -t myjob y leerla de nuevo con journalctl -t myjob. No use > /dev/null 2>&1 mientras siga depurando.

¿Debo usar cron o un temporizador de systemd?

Use cron para ejecutar un comando sencillo a una hora fija, especialmente si necesita trasladarlo a una máquina que no ejecute systemd. Use un temporizador cuando quiera guardar la salida en el journal sin redirigirla, consultar el estado de salida, ejecutar la tarea después de que la red esté disponible, aplicar un retraso de inicio aleatorio o reintentarla después de un fallo. Ambos pueden ejecutarse en el mismo servidor. Puede trasladar las tareas una por una cuando sea necesario.