Tutoriales de VPS
Tareas programadas de VPS que no se ejecutan (2026): lista de verificación de cron, crontab, systemd timer y variables de entorno
Cuando las tareas de crontab o systemd timer no se ejecutan en un VPS, la causa habitual no es el comando en sí, sino el usuario de ejecución, el entorno PATH, el directorio de trabajo, los permisos, la zona horaria, el servicio no habilitado o los registros no revisados correctamente. Este artículo soluciona el problema capa por capa: servicio cron, sintaxis de tareas, registros, variables de entorno, systemd timer e idempotencia de tareas.

Que las tareas programadas no se ejecuten en un VPS es uno de los errores de diagnóstico más fáciles de cometer.
El mismo comando que ejecutas manualmente por SSH funciona sin problema, pero al ponerlo en crontab simplemente no se ejecuta; el script funciona bien por sí solo, pero la copia de seguridad automática de madrugada no genera ningún archivo; systemctl status tampoco parece mostrar errores en rojo, pero el lado del negocio nunca recibe el resultado. En la mayoría de los casos, esto no es "misticismo de las tareas programadas de Linux", sino que el entorno de ejecución es completamente distinto al de tu inicio de sesión manual.
Este artículo sigue un orden de diagnóstico: primero confirmar si cron o el systemd timer realmente se están ejecutando, luego revisar registros, usuarios, rutas, permisos, variables de entorno y zona horaria, y finalmente abordar la idempotencia y las alertas del propio script.
No empieces modificando el crontab. Primero divide el problema en dos categorías:
- Nunca se activó: no hay registro de ejecución de la tarea en los registros, normalmente es un problema del servicio cron, la sintaxis, el usuario o el timer no habilitado.
- Se activó pero falló: hay registro en los registros, pero el script da error, los permisos son insuficientes, no se encuentra el comando o no se genera el archivo de salida.
- Se ejecutó pero el resultado no es el esperado: por ejemplo, el archivo de copia de seguridad está vacío, falla la sincronización con el almacenamiento de objetos o el script de reinicio se ejecuta repetidamente.
Revisar primero los registros del sistema ahorra mucho tiempo. Si aún no estás familiarizado con journalctl y los registros de servicios, puedes leer primero este artículo: Cómo leer los registros de un VPS (journalctl, systemctl, Nginx, registros de aplicaciones).
En Debian / Ubuntu normalmente es cron, y en RHEL / CentOS / Rocky normalmente es crond.
systemctl status cron
systemctl status crond
Si el servicio no está iniciado:
sudo systemctl enable --now cron
O:
sudo systemctl enable --now crond
Luego revisa los registros recientes:
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"
Si el sistema no tiene el servicio correspondiente, primero confirma si el paquete cron está instalado:
dpkg -l | grep cron
rpm -qa | grep cron
Algunas imágenes de VPS mínimas no traen cron preinstalado, especialmente las imágenes en contenedores o las imágenes cloud muy limpias. No des por hecho que siempre existe.
Este es uno de los errores más comunes.
Ejecutas como usuario normal:
crontab -e
Esto modifica las tareas programadas del usuario actual. Ejecutas como root:
sudo crontab -e
Esto modifica las tareas programadas de root. No son el mismo archivo.
Revisa las tareas del usuario actual:
crontab -l
Revisa las tareas de root:
sudo crontab -l
Si el script necesita acceder a /root/.ssh, leer un directorio de copias de seguridad que solo root puede leer o reiniciar servicios, normalmente debe ir en el crontab de root o cambiarse a un systemd timer. Por el contrario, si el script depende del directorio de proyecto de un usuario normal, de un virtualenv de Python o de un gestor de versiones de Node, no lo pongas sin más en el crontab de root.
Primero añade una tarea que solo escriba un registro, no llames directamente a un script complejo:
* * * * * date >> /tmp/cron-test.log 2>&1
Espera 1-2 minutos y revisa:
cat /tmp/cron-test.log
Si esto tampoco produce salida, el problema está en el servicio cron, el archivo crontab o la hora del sistema.
Si esto produce salida pero tu script no se ejecuta, lo más probable es que el problema esté en la ruta del script, los permisos, las variables de entorno o el usuario de ejecución.
Recuerda eliminar esta tarea después de la prueba para evitar escribir en /tmp de forma permanente.
El crontab de usuario son 5 campos de tiempo más el comando:
分 时 日 月 周 命令
Por ejemplo, para ejecutar a las 3:10 de la madrugada todos los días:
10 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Pero los archivos en /etc/crontab y /etc/cron.d/ tienen un campo adicional de "usuario":
10 3 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Muchas personas copian el formato del crontab de usuario en /etc/cron.d/, o lo copian al revés, lo que hace que las tareas no se ejecuten como se espera.
También hay que tener en cuenta varios puntos:
- Es mejor mantener un salto de línea al final del archivo crontab.
- Los nombres de archivo en
/etc/cron.d/no deben tener sufijos extraños; los permisos normalmente son644. - El
%en los comandos tiene un significado especial en cron; formatos de fecha comodate +%Fdeben escribirse comodate +\%F.
Puedes ejecutar esto por SSH:
node app.js
python script.py
rclone sync ...
Eso no significa que cron también pueda encontrar estos comandos. El entorno de cron es muy limpio, y PATH suele ser solo un segmento muy corto.
Primero busca la ruta completa del comando:
which node
which python3
which rclone
which mysqldump
Escribe la ruta absoluta en el crontab:
10 3 * * * /usr/bin/python3 /opt/jobs/cleanup.py >> /var/log/cleanup.log 2>&1
O establece PATH explícitamente al principio del crontab:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Si usas entornos a nivel de usuario como nvm, pyenv, conda, cron no los carga por defecto. Una forma más fiable es escribir un script wrapper que cargue el entorno dentro del script y luego ejecute el comando real.
Al ejecutar manualmente, puede que estés situado en el directorio del proyecto:
cd /var/www/app
npm run task
Cuando cron se ejecuta, el directorio de trabajo no necesariamente es este. Si el script usa rutas relativas, como ./backup, .env, logs/app.log, es muy fácil que falle.
Se recomienda escribir la tarea así:
15 2 * * * cd /var/www/app && /usr/bin/npm run task >> /var/log/app-task.log 2>&1
Si el script es importante, es mejor fijar el directorio al inicio del script:
#!/usr/bin/env bash
set -euo pipefail
cd /var/www/app
Luego haz que el crontab llame a este script:
15 2 * * * /usr/local/bin/app-task.sh >> /var/log/app-task.log 2>&1
Cuando el script no tiene permiso de ejecución, el registro de cron puede dejar solo una línea como Permission denied.
Revisa:
ls -l /usr/local/bin/app-task.sh
Corrige:
sudo chmod +x /usr/local/bin/app-task.sh
Si la tarea necesita escribir en un directorio de copias de seguridad, un directorio de registros o subir archivos temporales, también confirma que el usuario de ejecución tenga permisos:
sudo -u www-data /usr/local/bin/app-task.sh
sudo -u backup /usr/local/bin/backup.sh
Ejecutarlo manualmente con el usuario objetivo es más útil como referencia que ejecutarlo una vez como root.
Una tarea programada sin registros duplica el coste de diagnóstico.
Como mínimo, escríbela así:
30 1 * * * /usr/local/bin/backup.sh >> /var/log/backup-cron.log 2>&1
Si el registro va a crecer con el tiempo, combínalo con logrotate, o haz que el script escriba registros por fecha. No dejes que las tareas programadas fallen en silencio.
También puedes añadir al inicio del script:
set -euo pipefail
Así se exponen antes las variables no definidas, las tuberías fallidas y los comandos fallidos. Solo que antes de añadir esta línea a scripts en producción, pruébala, para evitar convertir pasos que originalmente toleraban fallos en fallos duros.
La zona horaria predeterminada de las imágenes de VPS suele ser UTC. Crees que se ejecuta a las 2 de la madrugada todos los días, pero en realidad se ejecuta a las 10 de la mañana hora de Pekín.
Revisa la zona horaria:
timedatectl
Si necesitas cambiarla a la zona horaria de Shanghái:
sudo timedatectl set-timezone Asia/Shanghai
Pero recomiendo más: planificar las tareas de producción uniformemente en UTC y escribir claramente la conversión en la documentación. Así es menos probable que haya confusión al migrar a VPS de distintas regiones.
Si tus tareas están relacionadas con la renovación de certificados, las ventanas de copia de seguridad o los periodos de baja carga de la base de datos, la zona horaria es especialmente importante. Para la renovación de certificados puedes consultar: Qué hacer cuando falla la renovación del certificado Let's Encrypt en un VPS.
Si usas un systemd timer, no mires solo .timer.
Revisa el timer:
systemctl list-timers --all
systemctl status my-job.timer
Revisa el service que realmente se ejecuta:
systemctl status my-job.service
journalctl -u my-job.service --since "24 hours ago"
Un timer solo se encarga de activar; lo que realmente se ejecuta es el service con el mismo nombre o el especificado. Los problemas comunes incluyen:
- Solo se habilitó
.service, no.timer. .timerse activó, pero.servicefalló al ejecutarse.OnCalendarestá mal escrito, así que la próxima hora de activación no es la que crees.- No se estableció
WorkingDirectoryen el service, lo que hace que fallen las rutas relativas. - El usuario de ejecución del service es distinto del usuario con el que probaste manualmente.
Un ejemplo de service más fiable:
[Unit]
Description=Daily app cleanup job
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php artisan app:cleanup
Timer correspondiente:
[Unit]
Description=Run app cleanup daily
[Timer]
OnCalendar=*-*-* 03:20:00
Persistent=true
[Install]
WantedBy=timers.target
Habilitar:
sudo systemctl daemon-reload
sudo systemctl enable --now app-cleanup.timer
Persistent=true funciona así: después de que la máquina se apaga y pierde la hora de ejecución, se ejecuta una vez en el siguiente arranque. Es muy útil para tareas de copia de seguridad, limpieza y sincronización, pero hay que tener cuidado con tareas como "enviar notificaciones", "cobrar tarifas" o "reiniciar en lote" para evitar activaciones concentradas tras la recuperación.
Lo que más temen las tareas programadas son dos cosas: que no se ejecuten una vez, o que se ejecuten dos veces al mismo tiempo.
Por ejemplo, un script de copia de seguridad que originalmente termina en 20 minutos, un día la base de datos crece y tarda 90 minutos, y como cron se activa cada hora, aparecen dos procesos de copia de seguridad compitiendo por disco y red.
Puedes usar flock para bloquear:
0 * * * * /usr/bin/flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup-cron.log 2>&1
Si la ejecución anterior aún no ha terminado, la siguiente se omite directamente.
Para copias de seguridad de bases de datos, sincronización con almacenamiento de objetos y compresión por lotes, es mejor añadir bloqueo. Para la estrategia de copias de seguridad en sí, puedes consultar: Soluciones de copia de seguridad automática en VPS.
Si tus tareas se ejecutan dentro de un contenedor Docker, el cron del host y el cron del contenedor son dos cosas distintas.
Errores comunes:
- En el
crontab -ldel host no se ven las tareas del contenedor. - Después de reiniciar el contenedor, el cron dentro del contenedor no se inicia.
- La imagen del contenedor no tiene el paquete cron.
- Las tareas escriben en el sistema de archivos temporal del contenedor, y los archivos se pierden al reconstruir el contenedor.
- La zona horaria dentro del contenedor no coincide con la del host.
Enfoques más recomendados:
- Tareas simples: el cron del host llama a
docker exec. - Tareas del framework de la aplicación: usa el scheduler integrado del framework, pero asegúrate de que el proceso sea residente.
- Tareas con altos requisitos de observabilidad: usa un systemd timer o un worker independiente, en lugar de meterlas en un contenedor temporal.
Cuando te encuentres con "la tarea programada no se ejecutó", revisa en este orden:
systemctl status cronosystemctl status crond.crontab -lysudo crontab -l, confirma que el usuario no está equivocado.- Añade
* * * * * date >> /tmp/cron-test.log 2>&1para una prueba mínima de activación. - Revisa el registro de cron o
journalctl -u cron. - Cambia el comando a ruta absoluta.
- Especifica
cd /path/to/projecto establece el directorio de trabajo dentro del script. - Revisa el permiso de ejecución del script y los permisos del usuario objetivo.
- Escribe la salida en el registro:
>> /var/log/job.log 2>&1. - Revisa
timedatectly la hora programada de la tarea. - Si es un systemd timer, revisa tanto
.timercomo.service. - Añade
flocka las tareas largas para evitar ejecuciones superpuestas.
Cuando las tareas programadas de un VPS no se ejecutan, el problema real normalmente no está en la "expresión de tiempo", sino en el entorno de ejecución: usuario distinto, PATH distinto, directorio distinto, permisos distintos, zona horaria distinta.
Primero usa una tarea mínima para demostrar que el planificador puede activarse, luego divide el comando real en "ruta, usuario, directorio, permisos, registros" y confirma cada punto uno por uno. Siempre que los registros sean visibles, las rutas sean absolutas y el usuario esté claro, la mayoría de los problemas de cron y systemd timer se pueden localizar rápidamente.