Configurar un cron job: planificación, permisos y errores frecuentes

Publicado el 21 min de lectura

El job funciona en la shell, pero no en cron. Esta guía explica los cinco campos de tiempo, la diferencia entre la crontab de usuario y /etc/cron.d, la trampa del PATH y cómo demostrar que una ejecución terminó de verdad.

Un cron job se configura en cinco minutos y después suele costar horas. El comando funciona sin problemas en la shell, desde cron no pasa nada, y en el log aparece como mucho que cron ha iniciado algo. Este artículo trata justo los puntos donde terminan las guías habituales: los mensajes de error reales, las diferencias entre distribuciones y la pregunta de cómo saber si un job llegó de verdad hasta el final y no solo se inició.

Todos los datos se refieren a Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS y Ubuntu 22.04 LTS. Estas cuatro usan el mismo cron (la variante Debian de Vixie Cron), no cronie como Red Hat y Fedora. Esa diferencia resulta decisiva más adelante, al llegar a la zona horaria.

¿Se está ejecutando siquiera el servicio cron?

En instalaciones completas cron está presente. En imágenes cloud mínimas, imágenes base de contenedores e instalaciones netinst reducidas falta el paquete con frecuencia. Esa es la primera pregunta que deberías aclarar antes de depurar cualquier otra cosa.

command -v crontab
systemctl status cron

A propósito command -v crontab y no command -v cron: el servicio en sí está en /usr/sbin, y en Debian ese directorio solo aparece en la ruta de búsqueda de root. Como usuario normal simplemente no obtienes ninguna salida, aunque cron esté instalado. En Ubuntu 24.04 la consulta también funciona con un usuario sin privilegios. crontab, en cambio, está en /usr/bin en todas partes y por tanto resulta visible para cualquiera, y systemctl status cron responde de todos modos a la pregunta más importante, es decir, si el servicio también se está ejecutando.

Si el servicio no está, lo instalas:

apt-get update
apt-get install -y cron
systemctl enable --now cron

En Debian y Ubuntu el servicio se llama cron, no crond. Quien viene acostumbrado a Red Hat recibe aquí un Unit crond.service could not be found. y busca en el sitio equivocado.

En funcionamiento normal no hace falta reiniciar nada: el cron de Debian vigila los directorios de crontab mediante inotify y lee los cambios por sí mismo. Después de editar una crontab no tienes que reiniciar nada. Las excepciones son el cambio de la zona horaria del sistema y las modificaciones en /etc/default/cron.

Los cinco campos, y la trampa del quinto

Cada línea empieza con cinco campos de tiempo, después viene el comando.

CampoRangoNota
Minuto0 a 59
Hora0 a 23formato de 24 horas, sin zona horaria
Día del mes1 a 31
Mes1 a 12también de jan a dec
Día de la semana0 a 70 y 7 son ambos domingo, también de sun a sat
30 4 * * *      /usr/local/bin/backup.sh      # a diario a las 04:30
*/10 * * * *    /usr/local/bin/check.sh       # cada 10 minutos
0 2 * * 0       /usr/local/bin/weekly.sh      # los domingos a las 02:00
15 3 1 * *      /usr/local/bin/monthly.sh     # el día 1 a las 03:15
0 9-17 * * 1-5  /usr/local/bin/business.sh    # cada hora en días laborables, de 9 a 17

Dos detalles se malinterpretan casi siempre.

El día del mes y el día de la semana son un O, no un Y. En cuanto ambos campos están restringidos, es decir, ninguno contiene un asterisco, el job se ejecuta cuando coincide uno de los dos. 0 3 13 * 5 no significa "viernes 13", sino "todos los días 13 y además todos los viernes". Si de verdad quieres el viernes 13, lo compruebas dentro del propio script.

Los intervalos no reparten de forma uniforme. */7 * * * * se ejecuta en los minutos 0, 7, 14, 21, 28, 35, 42, 49 y 56, y después el contador salta a la hora siguiente. Entre el 56 y el 0 pasan por tanto solo cuatro minutos. Esto vale para cualquier intervalo que no divida de forma exacta 60 o 24. Para "cada 90 minutos" no existe una notación limpia en cron, aquí un timer de systemd es la mejor opción.

Usar crontab -e correctamente

La crontab de usuario no la editas nunca directamente en /var/spool/cron/crontabs/, siempre a través de la herramienta:

crontab -e

En la primera llamada el programa responde con no crontab for root - using an empty one. En sistemas recién instalados aparece a continuación una selección de editor. Si no hay ningún editor, por ejemplo en una imagen reducida, la llamada aborta con un mensaje como /usr/bin/sensible-editor: 25: editor: not found. Solución: instalar un editor o indicar de forma explícita el editor deseado.

apt-get install -y nano
EDITOR=nano crontab -e

La gran ventaja de crontab -e frente a escribir el archivo directamente es la comprobación al guardar. Si una línea está rota, ves:

"/tmp/crontab.7hK2mn/crontab":3: bad minute
errors in crontab file, can't install.
Do you want to retry the same edit? (y/n)

El número de línea es correcto, el campo que nombra el mensaje no siempre: bad minute aparece también cuando sencillamente falta un campo, porque entonces el parser lee todo desplazado hacia la izquierda. Si todo va bien, el proceso termina con crontab: installing new crontab. Solo esa línea significa que el cambio se ha aplicado.

Otros comandos que deberías conocer:

crontab -l                         # mostrar
crontab -l > /root/crontab.bak     # guardar copia
crontab -u www-data -l             # leer la crontab de otro usuario (como root)
crontab -i -r                      # borrar, con confirmación (solo en el terminal)

Una advertencia sobre la copia de seguridad: crontab -l > /root/crontab.bak informa no crontab for root y devuelve el código de salida 1 mientras no exista todavía ninguna crontab para ese usuario. El archivo de destino se crea igualmente, entonces con 0 bytes. Siguiendo el orden de este artículo no llama la atención, pero en un script que comprueba el código de retorno o que sobrescribe un backup más antiguo, desde luego que sí.

crontab -r sin -i borra la crontab completa de inmediato y sin preguntar. Como r y e no están lejos en el teclado, este es un caso real de pérdida de datos. Acostúmbrate por eso a usar crontab -i -r en el terminal y haz antes una copia con crontab -l.

La palabra clave aquí es terminal. crontab -i -r es un comando puramente interactivo. Si la entrada estándar no cuelga de un terminal, es decir, dentro de un script, dentro de un cron job o en una línea suelta como ssh host "crontab -i -r", la pregunta de confirmación gira sin fin: crontab: really delete root's crontab? (y/n) Please enter Y or N: Please enter Y or N: ... se repite sin límite y escribe cientos de kilobytes de salida en segundos; la llamada solo termina si la interrumpes desde fuera. Para todo lo automatizado usas por eso la variante no interactiva y haces antes una copia:

crontab -l > /root/crontab.bak     # copia antes de borrar
crontab -r                         # borrar, sin confirmación
printf "y\n" | crontab -i -r       # mantener la pregunta, dar la respuesta

Si existen /etc/cron.allow o /etc/cron.deny, ellos deciden quién puede crear crontabs. Los usuarios afectados reciben: You (username) are not allowed to use this program (crontab). Si /etc/cron.allow está presente, se aplica en exclusiva y todo usuario que no aparezca en la lista queda bloqueado.

Crontab de usuario, /etc/crontab y /etc/cron.d

Hay cuatro sitios donde pueden estar los planes de ejecución, y tienen formatos distintos. Justo aquí nacen los errores más difíciles de encontrar.

Crontab de usuario: cinco campos

Se mantiene con crontab -e, se ejecuta bajo el usuario al que pertenece y no tiene campo de usuario. Si aun así escribes uno, cron intenta ejecutar el nombre de usuario como comando y en el correo o en el log encuentras:

/bin/sh: 1: root: not found

/etc/cron.d: seis campos

Los archivos en /etc/cron.d/ son crontabs del sistema y llevan un campo de usuario entre los campos de tiempo y el comando. Este es el lugar adecuado para jobs que pertenecen a una aplicación o a una gestión de configuración, porque cada archivo se puede reemplazar por separado.

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

0 3 * * * root /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

Si olvidas el campo de usuario, cron interpreta la primera palabra del comando como nombre de usuario y la línea falla en silencio, porque ese usuario no existe.

Tres reglas para /etc/cron.d/ se incumplen con regularidad:

  • El nombre del archivo no puede contener un punto. Solo se permiten letras, cifras, guiones bajos y guiones. backup.cron o kh-backup.sh se ignoran sin comentario alguno. La razón es la gestión de paquetes: así, restos como .dpkg-dist o .dpkg-old nunca se ejecutan. Llama al archivo sencillamente kh-backup.
  • Los permisos y el propietario tienen que ser correctos. Se esperan root:root y modo 0644. De lo contrario, en el log aparecen mensajes como (*system*) WRONG FILE OWNER, (*system*) BAD FILE MODE o un aviso sobre un modo inseguro, con permiso de escritura para el grupo o para otros. El job entonces no se ejecuta.
  • El archivo tiene que terminar con un salto de línea. cron exige que cada entrada acabe con un newline. Si la última línea termina sin salto, se pasa por alto, y sin comentario alguno: en una prueba de control con un archivo por lo demás idéntico pero sin salto final, el job no se ejecutó ni una sola vez, sin mensaje de error y sin línea de log. Quien genera el archivo con echo -n, con un printf sin \n final o desde una plantilla sin línea vacía al final, pierde justo ese último job.
chown root:root /etc/cron.d/kh-backup
chmod 0644 /etc/cron.d/kh-backup

/etc/crontab y los directorios cron.*

/etc/crontab tiene igualmente seis campos y pertenece a la distribución. Cámbialo solo si sabes por qué. A través de run-parts llama a los directorios /etc/cron.hourly, cron.daily, cron.weekly y cron.monthly. Los scripts que hay ahí necesitan el bit de ejecución y tampoco pueden llevar un punto en el nombre. Un backup.sh en /etc/cron.daily/ no se ejecuta nunca, un backup sí. Puedes probarlo sin ejecutar nada:

run-parts --test /etc/cron.daily

Solo se muestran los scripts que run-parts iniciaría realmente. Si el tuyo falta en la lista, la causa está en el nombre o en el bit de ejecución.

Por qué el PATH dentro de cron es otro

Esta es con diferencia la causa más frecuente de "funciona en la shell, pero no en cron". cron no inicia una shell de login. No se leen ni ~/.bashrc ni ~/.profile ni /etc/profile. Para las crontabs de usuario, el cron de Debian establece una ruta de búsqueda mínima:

PATH=/usr/bin:/bin

Con eso faltan /usr/local/bin y /usr/sbin. En Debian 12, Debian 13, Ubuntu 22.04 y Ubuntu 24.04, /usr/bin y /usr/sbin siguen siendo directorios separados, ya que el usr merge solo afecta a /bin y /sbin. Todo lo que hayas puesto tú mismo en /usr/local/bin, todo lo que venga de pip install, todo lo de un Node gestionado con nvm y herramientas del sistema como ufw o iptables sencillamente no existen para cron. El mensaje de error dice entonces:

/bin/sh: 1: backup.sh: not found

La segunda parte de la trampa: cron establece SHELL=/bin/sh. En Debian y Ubuntu, /bin/sh es un enlace a dash, no a bash. Cualquier construcción propia de bash en la cabecera del script o directamente en la línea de la crontab falla:

/bin/sh: 1: [[: not found
/bin/sh: 1: source: not found
/bin/sh: 1: Syntax error: "(" unexpected

Tres contramedidas, en este orden:

  1. Usar rutas absolutas. /usr/local/bin/backup.sh en lugar de backup.sh, /usr/bin/php en lugar de php. Es la variante más robusta, porque funciona con independencia de cualquier variable de entorno. Pero lee la ruta del sistema en vez de escribirla de memoria: command -v date devuelve en Debian 13, Debian 12, Ubuntu 24.04 y Ubuntu 22.04 /usr/bin/date, mientras que en sistemas más antiguos como Debian 11 devuelve /bin/date. Una línea con una ruta equivocada la acepta crontab sin comentarios y sin avisos; solo falla en tiempo de ejecución y además en silencio. Para comprobarlo, ejecuta una vez crontab -l y una vez env -i /bin/sh -c "/usr/bin/date", que informa de una ruta equivocada al instante con not found.
  2. Definir PATH y SHELL al principio de la crontab. Las asignaciones valen para todas las líneas siguientes. Importante: cron no expande variables ahí, PATH=$PATH:/opt/bin no funciona, escribe la ruta completa.
  3. Usar un script wrapper. La línea de la crontab solo llama al script, y el script establece su propio entorno.
#!/bin/bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /srv/app
exec ./do-the-work.sh

Si quieres saber qué le entrega cron realmente a tu job, haz que te lo muestre. Añade durante un minuto:

* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1

Solo esta vía muestra el entorno que cron establece de verdad. Una shell reconstruida con env -i se queda simplemente cerca, porque trae su propia ruta por defecto. Después comparas /tmp/cron-env.txt con tu entorno interactivo. Lo llamativo no suelen ser solo PATH y SHELL, sino también las variables de locale que faltan. Sin LANG todo corre en la locale C, lo que cambia el orden de clasificación, los formatos de fecha y la salida de caracteres acentuados. Los scripts que dependen de LANG=de_DE.UTF-8 se comportan de otra manera dentro de cron. También falta el ssh-agent, por lo que los jobs con acceso SSH necesitan una clave sin passphrase y un -i explícito.

La última pequeña maldad de esta categoría: el signo de porcentaje. En una línea de crontab, un % sin escapar se convierte en un salto de línea, y todo lo que venga después llega al comando como entrada estándar. Los formatos de fecha hay que escaparlos, por tanto.

0 2 * * * /usr/bin/tar -czf /backup/web-$(date +\%F).tar.gz /var/www

Redirigir la salida, correos y "No MTA installed"

Por defecto, cron envía por correo al propietario de la crontab todo lo que un job escribe en stdout o stderr. En un servidor sin sistema de correo eso acaba en el log:

(CRON) info (No MTA installed, discarding output)

No es un fallo del job. Solo significa que hubo salida y nadie pudo recogerla. Un job silencioso no genera esta línea. Por eso es incluso una señal útil: si aparece de repente, tu job ha empezado a producir salida, normalmente un mensaje de error.

Para la redirección hay tres patrones razonables:

# todo a un archivo de log, incluidos los errores
0 3 * * * /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

# descartar la salida normal, los errores siguen por correo
0 3 * * * /usr/local/bin/backup.sh > /dev/null

# todo al journal, bien etiquetado
0 3 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t kh-backup

La variante > /dev/null 2>&1 es popular y peligrosa: tira también todos los mensajes de error. Un job que lleva cuatro meses fallando tiene entonces exactamente el mismo aspecto que uno que funciona. Si quieres prescindir de los correos, pon mejor MAILTO="" al principio de la crontab y escribe la salida en un archivo. Si quieres correos a una dirección concreta, defines MAILTO=alerts@example.org, pero para eso necesitas un sistema de correo instalado.

Tus propios archivos de log en /var/log/ crecen sin límite. Crea para ellos una pequeña regla en /etc/logrotate.d/, si no, el cron job con más éxito acabará llenándote el disco.

Cómo saber que realmente se ejecutó

Aquí se separa la guía rápida de la comprobación fiable. cron registra el inicio de un job. No registra ni el final ni el código de salida. Una línea CMD en el log demuestra por tanto solo que cron ha iniciado la shell, no que tu script haya tenido éxito.

Los logs se leen de forma distinta según el sistema:

journalctl -u cron --since "30 min ago"
journalctl -t CRON --since today

En Debian 12 y Debian 13 este es el único camino, porque desde bookworm rsyslog ya no se instala por defecto. Allí, por lo general, ya no existe un archivo /var/log/syslog. Quien lo busca y no lo encuentra da el job por no iniciado sin razón.

En Ubuntu 22.04 y 24.04 rsyslog suele estar presente, así que allí funciona además:

grep CRON /var/log/syslog

Un /var/log/cron.log propio no viene de fábrica en ninguna de las cuatro versiones. La regla correspondiente en /etc/rsyslog.d/50-default.conf está comentada. No busques ese archivo, solo existe si alguien lo ha activado.

Una prueba sólida viene por eso del propio job. Haz que el script escriba al final una marca de tiempo y el código de salida:

#!/bin/bash
set -euo pipefail
trap 'echo "$(date -Is) kh-backup finalizado, exit $?" >> /var/log/kh-backup.log' EXIT
# el trabajo propiamente dicho

Con eso tienes tres pruebas en lugar de una: la línea CRON en el journal (cron lo ha iniciado), la línea final en tu archivo de log (el script ha llegado hasta el final) y el código de salida (ha terminado limpiamente). Solo cuando las tres coinciden, el job funciona.

Si un job puede durar más que su propio intervalo, protégelo además contra solapamientos. Si no, en algún momento arrancan diez instancias en paralelo y tumban el servidor:

*/5 * * * * /usr/bin/flock -n /var/lock/kh-sync.lock /usr/local/bin/sync.sh

flock -n termina de inmediato si ya hay una instancia en marcha. La herramienta está en util-linux y viene en los cuatro sistemas.

@reboot y por qué systemd suele ser mejor

Con @reboot se puede ejecutar un comando al arrancar. Además existen @daily, @hourly, @weekly, @monthly y @yearly, que sustituyen cada uno a los cinco campos de tiempo.

@reboot /usr/local/bin/start-app.sh

Parece cómodo y tiene tres debilidades serias:

  • El momento no es "después del arranque", sino "cuando cron arranca". Que la red, la base de datos o un punto de montaje estén listos en ese instante es pura suerte. El apaño habitual es un sleep 30 por delante, lo que solo aplaza el problema.
  • Un reinicio del servicio cron vuelve a disparar @reboot. Un systemctl restart cron, por ejemplo tras una actualización de paquetes, arranca tu aplicación una segunda vez aunque la primera siga en marcha.
  • No hay supervisión. Ni código de salida, ni reinicio tras una caída, ni estado.

Todo lo que deba ejecutarse de forma permanente corresponde a un servicio de systemd, no a un cron job. Para tareas recurrentes, un timer es la mejor opción. Bastan dos archivos:

[Unit]
Description=KernelHost Backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
[Unit]
Description=KernelHost Backup diario

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=900

[Install]
WantedBy=timers.target

Se activa el timer, no el servicio:

systemctl daemon-reload
systemctl enable --now kh-backup.timer
systemctl list-timers kh-backup.timer

Las tres ventajas decisivas frente a cron: Persistent=true recupera una ejecución perdida después del siguiente arranque, mientras que cron la salta sin decir nada. El código de salida acaba en systemctl status kh-backup.service, así que ves sin logging propio si ha funcionado. Y la salida completa está disponible con journalctl -u kh-backup.service, sin redirección y sin correo. Por cierto, la ruta de búsqueda en systemd es más generosa que la de cron e incluye por defecto también /usr/local/bin, aunque sin añadir nada propio sigue quedándose corta.

Para tareas de arranque sustituyes @reboot por un servicio con dependencias claras:

[Unit]
After=network-online.target
Wants=network-online.target

Así tu job se ejecuta con garantía solo cuando la red está realmente levantada. Encontrarás más sobre las units y su estructura en crear un servicio de systemd.

Zona horaria, UTC y el horario de verano

cron calcula siempre en la zona horaria del sistema tomada de /etc/localtime. En muchos servidores y en casi todas las imágenes cloud eso es UTC. Un job a las 0 3 * * * se ejecuta entonces en verano a las 05:00, hora de verano de Europa Central, y no a las 03:00. Comprueba primero a qué te enfrentas:

readlink -f /etc/localtime
timedatectl show -p Timezone --value
date
date -u

readlink -f /etc/localtime nombra la ruta dentro de /usr/share/zoneinfo y con ella la zona realmente vigente, por ejemplo /usr/share/zoneinfo/Etc/UTC o /usr/share/zoneinfo/Europe/Vienna. Funciona en las cuatro versiones y también allí donde systemd no está en marcha. timedatectl show -p Timezone --value da la misma información de forma corta y legible por máquina, pero requiere systemd.

Lo que deberías quitarte de encima es mirar en /etc/timezone. Debian 13 ya no entrega ese archivo, un cat /etc/timezone termina allí con cat: /etc/timezone: No such file or directory, y tampoco una instalación de tzdata lo devuelve. En Debian 12, Ubuntu 24.04 y Ubuntu 22.04 todavía existe, así que la respuesta varía según la versión. Determinante es en todo caso únicamente el enlace simbólico /etc/localtime: un valor escrito a mano en /etc/timezone no cambia nada en la zona horaria del sistema y, por tanto, tampoco nada en el momento de disparo de tus jobs.

Puedes cambiar la zona horaria del sistema en cualquier momento; después deberías reiniciar cron para que el servicio adopte el cambio con seguridad:

timedatectl set-timezone Europe/Vienna
systemctl restart cron

En sistemas sin systemd en marcha estableces el enlace simbólico directamente, el resultado es el mismo y se puede verificar al instante con readlink -f /etc/localtime:

ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime

Ahora el punto que en muchas guías está mal: el cron de Debian y Ubuntu no conoce CRON_TZ. Esa variable viene de cronie, el cron de Red Hat, Fedora y AlmaLinux. En Debian 13, Debian 12, Ubuntu 24.04 y Ubuntu 22.04 no existe una zona horaria por usuario ni por crontab. Si pones ahí TZ=Europe/Vienna en la crontab, eso afecta exclusivamente al entorno de los comandos ejecutados, así que un date dentro del script muestra la hora de Viena. El momento de disparo en sí queda totalmente intacto y se sigue rigiendo por la zona horaria del sistema. Quien pasa esto por alto tiene un job que parece bien configurado y que aun así se ejecuta con dos horas de desfase.

Quedan tres caminos limpios: ajustar la zona horaria del sistema, convertir tú mismo las horas a UTC, o cambiar a un timer de systemd, que acepta una zona horaria directamente en la planificación. Se puede comprobar sin riesgo:

systemd-analyze calendar "Mon..Fri 03:00 Europe/Vienna"

La salida indica el próximo disparo como una fecha concreta. Es la comprobación más fiable que puedes hacer antes de activar nada.

Sobre el horario de verano, un consejo práctico: no planifiques jobs entre las 02:00 y las 03:00. En marzo esa hora no existe, en octubre existe dos veces. Según el job, eso significa una ejecución perdida o una duplicada, ambas una vez al año y ambas difíciles de reproducir. Las 01:30 o las 03:30 son alternativas discretas. Con los timers de systemd ayuda además Persistent=true, para que una ejecución perdida se recupere.

Búsqueda rápida de fallos en siete pasos

Si un cron job no se ejecuta, ve por esta lista en orden. Cubre prácticamente todos los casos.

  1. ¿Se está ejecutando el servicio? systemctl status cron. Sin servicio no hay job.
  2. ¿Ha llegado la entrada? crontab -l para jobs de usuario, si no, mira el archivo en /etc/cron.d/. Comprueba el nombre de archivo sin punto, el modo 0644, el propietario root y el salto de línea final.
  3. ¿Lo ha iniciado cron siquiera? journalctl -t CRON --since today. Si falta la línea CMD, el problema está en la planificación o en el archivo, no en el script.
  4. ¿El número de campos es correcto? Cinco campos en la crontab de usuario, seis en /etc/cron.d y /etc/crontab.
  5. ¿Rutas absolutas? Introduce cada comando y cada script con la ruta completa, y averigua antes esa ruta con command -v en lugar de escribirla a mano.
  6. ¿Entorno comprobado? Añade una sola vez * * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1, espera un minuto y compara.
  7. Probar en el contexto de cron. No ejecutes el script en tu shell, sino con un entorno vacío: env -i PATH=/usr/bin:/bin /bin/sh -c '/usr/local/bin/backup.sh'. La ruta de búsqueda la indicas a propósito. env -i /bin/sh por sí solo vacía el entorno, pero dash establece después su propia ruta por defecto /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, y esa es más generosa que la de cron. Justo los errores de los que trata este artículo quedarían así sin detectar. Si la llamada falla, has encontrado la causa, sin esperar al siguiente momento de disparo.

El último punto es el más valioso. Casi todo cron job que "inexplicablemente" no funciona falla de forma reproducible en cuanto se inicia con un entorno vacío. Así, un misterio que solo aparece cada 24 horas se convierte en un fallo que puedes arreglar en diez segundos.

Quien hace backups con regularidad por cron job debería asegurar además el sistema de destino. Encontrarás indicaciones adecuadas en nuestro artículo sobre la protección de un servidor Linux.

Preguntas frecuentes

¿Por qué mi script funciona en la shell, pero no como cron job?
En la inmensa mayoría de los casos la causa es el entorno. cron no inicia una shell de login, así que no lee ni ~/.bashrc ni /etc/profile, y para las crontabs de usuario solo define PATH=/usr/bin:/bin. Con eso faltan /usr/local/bin y /usr/sbin. Además, SHELL apunta a /bin/sh, que en Debian y Ubuntu es dash y no entiende la sintaxis de bash. Prueba el job exactamente en ese entorno: env -i PATH=/usr/bin:/bin /bin/sh -c, seguido de la ruta del script entre comillas simples ('/ruta/al/script.sh'), y fallará al instante de forma reproducible. La ruta de búsqueda la indicas de forma expresa, porque un simple env -i /bin/sh aplica la ruta por defecto más generosa de dash y oculta justo ese error.
¿Qué significa el mensaje No MTA installed, discarding output?
Tu job ha escrito algo en stdout o stderr, cron quiso entregarlo por correo, pero no hay ningún sistema de correo instalado. El job en sí no se ve afectado y puede haber terminado con éxito igualmente. O rediriges la salida a un archivo de log, o pones MAILTO="" al principio de la crontab. Atención: el mensaje suele aparecer justo cuando un job hasta entonces silencioso empieza a producir errores.
¿Debian o Ubuntu admiten la variable CRON_TZ?
No. CRON_TZ viene de cronie, el cron de Red Hat y Fedora. Debian 13, Debian 12, Ubuntu 24.04 y Ubuntu 22.04 usan la variante Debian de Vixie Cron y no conocen una zona horaria por crontab. Un TZ= en la crontab solo actúa sobre el entorno de los comandos, no sobre el momento de disparo. Ajusta la zona horaria del sistema con timedatectl, convierte las horas a UTC o usa un timer de systemd, que acepta una zona horaria en la expresión OnCalendar.
¿Por qué se ignora mi archivo en /etc/cron.d?
Casi siempre por el nombre del archivo. Solo se permiten letras, cifras, guiones bajos y guiones; un punto vuelve el archivo invisible para cron. Así, backup.cron se ignora y backup no. Otras razones: permisos incorrectos (hacen falta root:root y 0644), un campo de usuario que falta entre los campos de tiempo y el comando, o una última línea sin salto de línea final.
¿Dónde encuentro los logs de cron en Debian 12 y 13?
En journalctl, porque desde Debian 12 rsyslog ya no se instala por defecto y allí normalmente no existe un archivo /var/log/syslog. Usa journalctl -u cron o journalctl -t CRON. En Ubuntu 22.04 y 24.04 funciona además grep CRON /var/log/syslog. Ninguno de los cuatro sistemas trae de fábrica un /var/log/cron.log propio.
¿Una línea CMD en el log demuestra que el job tuvo éxito?
No. cron registra solo el inicio de un job, ni el final ni el código de salida. Un script puede haberse caído un segundo después y la línea de log tendrá exactamente el mismo aspecto. Para una prueba real, haz que el propio script escriba una marca de tiempo y el código de salida en un archivo de log, por ejemplo mediante un trap sobre EXIT, o cambia a un timer de systemd, donde systemctl status muestra el código de salida.
¿Debería usar @reboot o un servicio de systemd?
En casi todos los casos, un servicio de systemd. @reboot no se ejecuta después del arranque, sino al iniciarse el servicio cron, sin garantía de que la red o la base de datos estén listas. Además se vuelve a disparar con cada systemctl restart cron, por ejemplo tras una actualización de paquetes. Un servicio con After=network-online.target y Wants=network-online.target resuelve ambos problemas y ofrece además indicación de estado y código de salida.

Cron Crontab systemd Administración de Linux Debian Ubuntu Automatización Gestión de servidores