Tienes un programa que funciona en tu terminal y quieres que quede corriendo en el servidor: que arranque solo al encender, que vuelva a levantarse si se cae y que puedas ver qué hizo anoche. O quieres que un respaldo se haga solo cada madrugada. En Linux, las dos cosas se resuelven con systemd y cron. Esta guía te lleva desde cero hasta un servicio propio y tareas programadas, con los errores que te vas a encontrar. Todas las órdenes se ejecutaron de verdad en un Debian 13 limpio, y las salidas están copiadas tal cual.
- Qué es systemd y por qué es el proceso 1
- Las unidades y systemctl
- Tu programa como servicio
- Que se reinicie solo: Restart=
- Sin privilegios y con variables de entorno
- Ver lo que dice tu programa: journalctl
- Dependencias y orden: After= y Wants=
- Errores típicos con servicios
- Servicios de usuario, sin sudo
- Temporizadores de systemd: la alternativa a cron
- cron: la forma clásica
- Las trampas de cron
- ¿systemd o cron?
- Hoja de referencia
Qué es systemd y por qué es el proceso 1
Cuando enciendes un equipo con Linux, el kernel arranca un único programa y le da el número de proceso 1. Ese programa se encarga de arrancar todo lo demás: la red, el servidor SSH, la base de datos y, si se lo pides, tus propios programas. En casi todas las distribuciones actuales (Debian, Ubuntu, Fedora, Red Hat, Arch, openSUSE) ese proceso es systemd.
Terminal$ ps -p 1 -o pid,comm
PID COMMAND
1 systemd
$ systemctl --version | head -1
systemd 257 (257.13-1~deb13u1)
$ systemctl is-system-running
runningsystemd no solo arranca programas: los vigila mientras corren, los reinicia si se caen, guarda lo que escriben en su diario (el journal) y los detiene en orden al apagar. Por eso, cuando quieres que un programa tuyo quede funcionando para siempre en un servidor, lo correcto no es dejarlo abierto en una terminal ni lanzarlo con nohup: es convertirlo en un servicio de systemd.
--restart). Esta guía es para servidores y máquinas virtuales; para contenedores, mira la guía de Docker.Las unidades y systemctl
Todo lo que systemd maneja se llama unidad y se describe en un archivo de texto. El final del nombre dice el tipo:
- .service: un programa que corre (un servidor web, una base de datos, tu script).
- .timer: una alarma que arranca un servicio en cierto momento; es la alternativa de systemd a cron.
- .target: un grupo de unidades que marcan una etapa del arranque, como
multi-user.target(«el sistema ya está listo para usuarios»). - .socket, .mount, .path y otras, menos frecuentes al empezar.
Para hablar con systemd se usa systemctl. Estas son las órdenes que vas a usar casi siempre:
systemctl status nombre: estado, PID, memoria y las últimas líneas del diario.sudo systemctl start nombre/stop/restart: arrancar, detener o reiniciar ahora.sudo systemctl enable nombre/disable: que arranque (o no) solo al encender el equipo.enable --nowhace las dos cosas a la vez.systemctl is-active nombreyis-enabled nombre: respuestas de una palabra, útiles en scripts.sudo systemctl daemon-reload: releer los archivos de unidad después de editarlos.systemctl list-units --type=serviceysystemctl --failed: qué servicios hay y cuáles fallaron.
En el equipo de prueba, recién arrancado, corren solo cuatro servicios. Uno de ellos es cron:
Terminal$ systemctl list-units --type=service --state=running
UNIT LOAD ACTIVE SUB DESCRIPTION
cron.service loaded active running Regular background program processing daemon
dbus.service loaded active running D-Bus System Message Bus
systemd-journald.service loaded active running Journal Service
systemd-logind.service loaded active running User Login Management
Legend: LOAD → Reflects whether the unit definition was properly loaded.
ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
SUB → The low-level unit activation state, values depend on unit type.
4 loaded units listed.Terminal$ sudo systemctl status cron
● cron.service - Regular background program processing daemon
Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
Active: active (running) since Sat 2026-11-07 10:56:34 CST; 8s ago
Invocation: 0c1a666d0fb34a34a6f0ffba28f19455
Docs: man:cron(8)
Main PID: 73 (cron)
Tasks: 1 (limit: 150)
Memory: 300K (peak: 2M)
CPU: 10ms
CGroup: /system.slice/cron.service
└─73 /usr/sbin/cron -f
Nov 07 10:56:34 servidor systemd[1]: Started cron.service - Regular background program processing daemon.
Nov 07 10:56:34 servidor cron[73]: (CRON) INFO (pidfile fd = 3)
Nov 07 10:56:34 servidor cron[73]: (CRON) INFO (Running @reboot jobs)Cómo se lee status: Loaded dice dónde está el archivo de la unidad y si está habilitada (enabled: arranca sola al encender). Active dice si corre ahora y desde cuándo. Main PID es el proceso principal, y CGroup muestra todos sus procesos: systemd los agrupa para poder detenerlos todos juntos, incluidos los hijos que el programa haya lanzado. Abajo van las últimas líneas de su diario. Si no usas sudo, algunas de esas líneas no aparecen por permisos.
Tu programa como servicio
Vamos a convertir en servicio un programa pequeño en Python que escribe un renglón cada 5 segundos. Sirve igual para un servidor web, un bot o cualquier binario: lo único que cambia es la línea ExecStart.
#!/usr/bin/env python3
# Programa de ejemplo de la guía de systemd y cron de decodigo.com: escribe un renglón cada 5 segundos.
import os, pwd, time
mensaje = os.environ.get("MENSAJE", "sin mensaje")
usuario = pwd.getpwuid(os.getuid()).pw_name
print(f"latido arranca: usuario={usuario} pid={os.getpid()}", flush=True)
n = 0
while True:
n += 1
print(f"{mensaje} #{n}", flush=True)
time.sleep(5)La unidad de servicio es un archivo con tres secciones. Va en /etc/systemd/system/, que es la carpeta para las unidades que creas tú; las que traen los paquetes viven en /usr/lib/systemd/system/ y no conviene editarlas.
[Unit] Description=Latido, programa de ejemplo de decodigo.com After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/bin/python3 /opt/latido/latido.py User=latido EnvironmentFile=/etc/latido/latido.env Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.target
- [Unit]: la descripción y el orden respecto a otras unidades (lo vemos en dependencias).
- ExecStart: la orden que arranca el programa, con rutas absolutas. No pasa por una shell: nada de
cd, tuberías ni&&. Si los necesitas, ponlos en un script y llama al script. - User: el usuario con el que corre. Sin esta línea, corre como root.
- EnvironmentFile: un archivo con variables de entorno, una por línea.
- Restart y RestartSec: qué hacer si se cae y cuánto esperar.
- [Install] WantedBy=multi-user.target: a qué etapa del arranque se engancha cuando lo habilitas.
El archivo de variables es así de simple:
MENSAJE=Hola desde systemd
Se crea un usuario de sistema para el servicio (sin carpeta personal y sin poder iniciar sesión), se copian los archivos a su lugar y se avisa a systemd:
Terminal$ sudo useradd --system --no-create-home --shell /usr/sbin/nologin latido $ sudo install -D -m 755 latido.py /opt/latido/latido.py $ sudo install -D -m 644 latido.env /etc/latido/latido.env $ sudo install -m 644 latido.service /etc/systemd/system/latido.service
Terminal$ sudo systemctl daemon-reload $ sudo systemctl enable --now latido Created symlink '/etc/systemd/system/multi-user.target.wants/latido.service' → '/etc/systemd/system/latido.service'.
El mensaje de enable enseña lo que hace en realidad: crea un enlace en la carpeta multi-user.target.wants. Al arrancar el equipo, systemd ve ese enlace y arranca el servicio. Doce segundos después:
Terminal$ sudo systemctl status latido
● latido.service - Latido, programa de ejemplo de decodigo.com
Loaded: loaded (/etc/systemd/system/latido.service; enabled; preset: enabled)
Active: active (running) since Sat 2026-11-07 10:56:43 CST; 12s ago
Invocation: d3ee236529c148fba9bd57ec3d9a46f3
Main PID: 236 (python3)
Tasks: 1 (limit: 150)
Memory: 2.8M (peak: 3.3M)
CPU: 14ms
CGroup: /system.slice/latido.service
└─236 /usr/bin/python3 /opt/latido/latido.py
Nov 07 10:56:43 servidor systemd[1]: Started latido.service - Latido, programa de ejemplo de decodigo.com.
Nov 07 10:56:43 servidor python3[236]: latido arranca: usuario=latido pid=236
Nov 07 10:56:43 servidor python3[236]: Hola desde systemd #1
Nov 07 10:56:48 servidor python3[236]: Hola desde systemd #2
Nov 07 10:56:53 servidor python3[236]: Hola desde systemd #3El programa corre con el usuario latido, no con root, y systemd guarda en su diario cada renglón que imprime. El mensaje con espacios, «Hola desde systemd», llegó entero desde el EnvironmentFile.
Que se reinicie solo: Restart=
La razón principal para usar systemd es que vigile el programa. Para probarlo, matamos el proceso a la fuerza con kill -9, como si se hubiera quedado sin memoria o tuviera un error grave:
Terminal$ sudo kill -9 236 $ sudo journalctl -u latido -n 6 Nov 07 10:56:55 servidor systemd[1]: latido.service: Main process exited, code=killed, status=9/KILL Nov 07 10:56:55 servidor systemd[1]: latido.service: Failed with result 'signal'. Nov 07 10:56:58 servidor systemd[1]: latido.service: Scheduled restart job, restart counter is at 1. Nov 07 10:56:58 servidor systemd[1]: Started latido.service - Latido, programa de ejemplo de decodigo.com. Nov 07 10:56:58 servidor python3[291]: latido arranca: usuario=latido pid=291 Nov 07 10:56:58 servidor python3[291]: Hola desde systemd #1 $ systemctl show latido -p MainPID -p NRestarts MainPID=291 NRestarts=1
systemd vio morir el proceso, esperó los 3 segundos de RestartSec y lo volvió a arrancar con otro PID. El contador NRestarts lleva la cuenta. Los valores de Restart= más útiles son:
- no (el de fábrica): nunca lo reinicia.
- on-failure: lo reinicia si termina con un código de error, por una señal como la de
kill -9o por tardar demasiado. Es el más usado. - always: lo reinicia siempre que termine, aunque haya terminado bien. Sirve para programas que nunca deberían salir.
Hay un freno: si el servicio se cae 5 veces en 10 segundos (los valores de fábrica de StartLimitBurst y StartLimitIntervalSec), systemd deja de intentarlo y lo marca como failed, para no quedarse en un bucle infinito. Lo verás en los errores típicos.
Y cuando eres tú quien lo detiene, no lo reinicia:
Terminal$ sudo systemctl stop latido $ systemctl is-active latido inactive $ sudo journalctl -u latido -n 2 Nov 07 10:57:00 servidor systemd[1]: latido.service: Deactivated successfully. Nov 07 10:57:00 servidor systemd[1]: Stopped latido.service - Latido, programa de ejemplo de decodigo.com.
Sin privilegios y con variables de entorno
Un programa que corre como root puede leer y borrar cualquier cosa del sistema: si tiene un fallo de seguridad, el atacante se queda con todo. Por eso cada servicio debería tener su propio usuario con los permisos mínimos, como hicimos con latido:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin latido
El diario y ps lo confirman:
Terminal$ sudo journalctl -u latido -n 4 Nov 07 10:56:43 servidor python3[236]: latido arranca: usuario=latido pid=236 Nov 07 10:56:43 servidor python3[236]: Hola desde systemd #1 Nov 07 10:56:48 servidor python3[236]: Hola desde systemd #2 Nov 07 10:56:53 servidor python3[236]: Hola desde systemd #3 $ ps -o user,pid,cmd -C python3 USER PID CMD latido 236 /usr/bin/python3 /opt/latido/latido.py
Para las variables de entorno hay dos formas:
- Environment=, directo en la unidad:
Environment="PUERTO=8080". Cómodo para valores públicos. - EnvironmentFile=, en un archivo aparte: mejor para contraseñas y claves de API, porque ese archivo puede ser de root con permisos
600. systemd lo lee como root antes de cambiar de usuario, así que el servicio recibe las variables sin poder leer el archivo.
Environment=MENSAJE=Hola desde el usuario deja la variable en «Hola» y descarta el resto. Hay que encerrar toda la asignación entre comillas: Environment="MENSAJE=Hola desde el usuario". Lo verás pasar de verdad en servicios de usuario. En un EnvironmentFile, en cambio, la línea sin comillas llegó completa.Ver lo que dice tu programa: journalctl
Todo lo que tu programa escribe en la salida estándar o en la de errores termina en el diario de systemd. No hace falta que el programa escriba su propio archivo de log: basta con imprimir. Para leerlo:
journalctl -u latido: todo el diario de esa unidad.journalctl -u latido -n 50: las últimas 50 líneas.journalctl -u latido -f: seguirlo en vivo, comotail -f. Se sale con Ctrl+C.journalctl -u latido --since "1 hour ago"o--since 10:00 --until 11:00: por horario.journalctl -u latido -b: solo desde el último arranque del equipo.journalctl -p err: solo errores, de todo el sistema.
Por permisos, un usuario normal solo ve su propio diario: usa sudo o agrega tu usuario al grupo systemd-journal. Si quieres entender qué debe llevar una buena línea de log y qué no, lo explicamos en logs y archivos de configuración.
Dependencias y orden: After= y Wants=
Muchos programas necesitan que algo esté listo antes: la red, una base de datos, un disco montado. En la sección [Unit] se dice de dos formas distintas, y conviene no confundirlas:
- Wants= (o Requires=): «cuando me arranques, arranca también esto».
Wantslo intenta y sigue aunque falle;Requiresdetiene tu servicio si la otra unidad falla. - After=: «arráncame después de esto». Solo ordena: no arranca nada por sí mismo.
Por eso casi siempre van juntos. After=network-online.target con Wants=network-online.target significa «arranca la espera de la red y no me arranques hasta que haya red». Con solo After, si nadie más pidió esa unidad, tu servicio no espera nada. Así quedó nuestro servicio:
Terminal$ systemctl show latido -p After -p Wants -p WantedBy Wants=network-online.target WantedBy=multi-user.target After=network-online.target systemd-journald.socket basic.target system.slice sysinit.target
systemd agregó por su cuenta varias dependencias básicas (sysinit.target, basic.target, el socket del diario). Para una base de datos local sería, por ejemplo, After=postgresql.service y Wants=postgresql.service.
Errores típicos con servicios
Editaste la unidad y no pasó nada
systemd lee los archivos de unidad una sola vez. Si los editas, tienes que pedirle que los relea con daemon-reload; si no, sigue usando la versión vieja y te avisa:
Terminal$ sudo sed -i 's/RestartSec=3/RestartSec=5/' /etc/systemd/system/latido.service $ sudo systemctl start latido Warning: The unit file, source configuration file or drop-ins of latido.service changed on disk. Run 'systemctl daemon-reload' to reload units. $ sudo systemctl daemon-reload $ sudo systemctl restart latido $ systemctl show latido -p RestartUSec RestartUSec=5s
«Start request repeated too quickly»
Este servicio intenta correr el mismo programa, pero guardado en la carpeta personal de ana, con el usuario latido:
Terminal$ sudo systemctl daemon-reload
$ sudo systemctl start roto
$ sudo systemctl status roto -n 4
× roto.service - Latido desde la carpeta de ana
Loaded: loaded (/etc/systemd/system/roto.service; static)
Active: failed (Result: exit-code) since Sat 2026-11-07 10:57:03 CST; 883ms ago
Duration: 33ms
Invocation: 696755edc2604a6ba656a52c28681699
Process: 471 ExecStart=/usr/bin/python3 /home/ana/latido.py (code=exited, status=2)
Main PID: 471 (code=exited, status=2)
Nov 07 10:57:03 servidor systemd[1]: roto.service: Scheduled restart job, restart counter is at 5.
Nov 07 10:57:03 servidor systemd[1]: roto.service: Start request repeated too quickly.
Nov 07 10:57:03 servidor systemd[1]: roto.service: Failed with result 'exit-code'.
Nov 07 10:57:03 servidor systemd[1]: Failed to start roto.service - Latido desde la carpeta de ana.El estado solo dice que el programa terminó con el código 2 y que systemd se rindió tras cinco intentos. La causa de verdad está en el diario:
Terminal$ sudo journalctl -u roto | grep -m1 python3 Nov 07 10:57:01 servidor python3[454]: /usr/bin/python3: can't open file '/home/ana/latido.py': [Errno 13] Permission denied $ ls -ld /home/ana drwx------ 1 ana ana 4096 Nov 7 10:56 /home/ana
La carpeta personal es drwx------: solo ana puede entrar, y el servicio corre como latido. La solución es poner el programa en un lugar del sistema, como /opt, que es lo que hicimos con el bueno. Después de arreglar algo, sudo systemctl reset-failed nombre borra el estado de fallo y el contador de intentos.
Otros que vas a ver
- status=203/EXEC: systemd no pudo ejecutar lo de
ExecStart. La ruta está mal, el archivo no tiene permiso de ejecución o un script no empieza con#!. - status=217/USER: el usuario de
User=no existe. - El servicio arranca a mano pero no al encender: falta
enable, o falta la sección[Install]. - Funciona en tu terminal y no como servicio: el servicio no carga tu
.bashrc, ni tuPATH, ni tu carpeta actual. Usa rutas absolutas y pon las variables enEnvironment. Si necesitas una carpeta de trabajo, usaWorkingDirectory=.
La regla para depurar: systemctl status para ver qué pasó y journalctl -u nombre para ver por qué.
Servicios de usuario, sin sudo
Si no tienes acceso de administrador, o el programa es solo tuyo, puedes tener tus propios servicios. Van en ~/.config/systemd/user/ y se manejan con systemctl --user, sin sudo. Esta es la primera versión del archivo, con un error a propósito:
[Unit] Description=Latido de ana [Service] ExecStart=/usr/bin/python3 %h/latido.py Environment=MENSAJE=Hola desde el usuario Restart=on-failure [Install] WantedBy=default.target
%h es un atajo de systemd para tu carpeta personal. Por defecto, tus servicios se detienen al cerrar tu última sesión. Para que sigan corriendo sin sesión abierta, y arranquen al encender el equipo, el administrador tiene que activar el linger de tu usuario una vez:
Terminal$ sudo loginctl enable-linger ana $ sleep 3; loginctl show-user ana -p Linger -p State State=lingering Linger=yes
Terminal$ systemctl --user daemon-reload
$ systemctl --user enable --now latido
Created symlink '/home/ana/.config/systemd/user/default.target.wants/latido.service' → '/home/ana/.config/systemd/user/latido.service'.
$ systemctl --user status latido
● latido.service - Latido de ana
Loaded: loaded (/home/ana/.config/systemd/user/latido.service; enabled; preset: enabled)
Active: active (running) since Sat 2026-11-07 10:57:07 CST; 7s ago
Invocation: 8b993acbdf474231a598c99d027e83f8
Main PID: 554 (python3)
Tasks: 1 (limit: 150)
Memory: 3M (peak: 3.6M)
CPU: 13ms
CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/latido.service
└─554 /usr/bin/python3 /home/ana/latido.py
Nov 07 10:57:07 servidor systemd[514]: Started latido.service - Latido de ana.
Nov 07 10:57:07 servidor python3[554]: latido arranca: usuario=ana pid=554
Nov 07 10:57:07 servidor python3[554]: Hola #1
Nov 07 10:57:12 servidor python3[554]: Hola #2
$ journalctl --user -u latido -n 2
Nov 07 10:57:07 servidor python3[554]: Hola #1
Nov 07 10:57:12 servidor python3[554]: Hola #2Mira la salida: dice «Hola #1», no «Hola desde el usuario #1». Es el error de los espacios en Environment= que mencionamos antes. Con la línea entre comillas, Environment="MENSAJE=Hola desde el usuario":
Terminal$ systemctl --user daemon-reload $ systemctl --user restart latido $ journalctl --user -u latido -n 2 Nov 07 10:57:14 servidor python3[599]: latido arranca: usuario=ana pid=599 Nov 07 10:57:14 servidor python3[599]: Hola desde el usuario #1
systemctl --user. Si usas su o sudo -u para cambiar de usuario, en cambio, verás «Failed to connect to user scope bus»: entra directamente con ese usuario, por ejemplo por SSH.Temporizadores de systemd: la alternativa a cron
Un temporizador son dos archivos: el servicio que hace el trabajo y el .timer que dice cuándo. Este ejemplo empaqueta una carpeta cada día a las 3 de la mañana. Primero el script:
#!/bin/sh # Respaldo de ejemplo: empaqueta /srv/datos con la fecha en el nombre. destino=/var/respaldos/datos-$(date +%F-%H%M%S).tar.gz tar -czf "$destino" -C /srv datos echo "respaldo listo: $destino"
El servicio es de tipo oneshot: corre, termina y ya. No lleva sección [Install] porque no se habilita él, sino su temporizador:
[Unit] Description=Respaldo de /srv/datos [Service] Type=oneshot ExecStart=/usr/local/bin/respaldo.sh
[Unit] Description=Respaldo diario de /srv/datos [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true [Install] WantedBy=timers.target
El temporizador se llama igual que el servicio, y así systemd sabe cuál arrancar. Persistent=true es la gran ventaja sobre cron: si el equipo estaba apagado a las 3:00, el respaldo se hace en cuanto vuelva a encender, en lugar de saltárselo.
Terminal$ sudo install -m 755 respaldo.sh /usr/local/bin/respaldo.sh $ sudo install -m 644 respaldo.service respaldo.timer /etc/systemd/system/ $ sudo systemctl daemon-reload $ sudo systemctl enable --now respaldo.timer Created symlink '/etc/systemd/system/timers.target.wants/respaldo.timer' → '/etc/systemd/system/respaldo.timer'.
La sintaxis de OnCalendar es día-de-la-semana año-mes-día hora:minuto:segundo, y el asterisco significa «cualquiera». Lo mejor es que se puede probar antes de usarla: systemd-analyze calendar te dice cuándo se disparará.
Terminal$ systemd-analyze calendar '*-*-* 03:00:00'
Normalized form: *-*-* 03:00:00
Next elapse: Sun 2026-11-08 03:00:00 CST
(in UTC): Sun 2026-11-08 09:00:00 UTC
From now: 16h left
$ systemd-analyze calendar 'Mon..Fri 08:30'
Original form: Mon..Fri 08:30
Normalized form: Mon..Fri *-*-* 08:30:00
Next elapse: Mon 2026-11-09 08:30:00 CST
(in UTC): Mon 2026-11-09 14:30:00 UTC
From now: 1 day 21h left
$ systemd-analyze calendar --iterations=3 '*:0/15'
Original form: *:0/15
Normalized form: *-*-* *:00/15:00
Next elapse: Sat 2026-11-07 11:00:00 CST
(in UTC): Sat 2026-11-07 17:00:00 UTC
From now: 2min 42s left
Iteration #2: Sat 2026-11-07 11:15:00 CST
(in UTC): Sat 2026-11-07 17:15:00 UTC
From now: 17min left
Iteration #3: Sat 2026-11-07 11:30:00 CST
(in UTC): Sat 2026-11-07 17:30:00 UTC
From now: 32min leftTambién acepta palabras: hourly, daily, weekly, monthly. Para ver todos los temporizadores y cuándo toca el siguiente:
Terminal$ systemctl list-timers respaldo.timer NEXT LEFT LAST PASSED UNIT ACTIVATES Sun 2026-11-08 03:00:00 CST 16h - - respaldo.timer respaldo.service 1 timers listed. Pass --all to see loaded but inactive timers, too.
Y para probar el trabajo sin esperar a las 3 de la mañana, se arranca el servicio a mano. Su salida queda en el diario, como la de cualquier servicio:
Terminal$ sudo systemctl start respaldo.service $ sudo journalctl -u respaldo -n 3 Nov 07 10:57:17 servidor respaldo.sh[738]: respaldo listo: /var/respaldos/datos-2026-11-07-105717.tar.gz Nov 07 10:57:17 servidor systemd[1]: respaldo.service: Deactivated successfully. Nov 07 10:57:17 servidor systemd[1]: Finished respaldo.service - Respaldo de /srv/datos. $ ls /var/respaldos datos-2026-11-07-105717.tar.gz
Otras formas de decir cuándo: OnBootSec=5min (cinco minutos después de encender), OnUnitActiveSec=1h (una hora después de la última vez) y RandomizedDelaySec=15min, que reparte la hora al azar para que cien servidores no hagan su respaldo en el mismo segundo.
cron: la forma clásica
cron existe desde los años setenta y sigue en casi todos los sistemas Unix. Cada usuario tiene su crontab, una tabla con una tarea por línea: cinco campos que dicen cuándo y, después, la orden.
En cada campo se puede poner un número, un asterisco (cualquiera), una lista (1,15), un rango (1-5) o un paso (*/10, «cada 10»). Ojo con un detalle: si pones día del mes y día de la semana a la vez, cron ejecuta la tarea cuando se cumpla cualquiera de los dos, no ambos.
Las órdenes para manejar tu crontab:
crontab -e: editarla (abre tu editor).crontab -l: ver lo que tiene.crontab -r: borrarla entera, sin preguntar. Larestá junto a laeen el teclado; antes de editar, guarda una copia concrontab -l > respaldo-crontab.txt.
Además de la crontab de cada usuario están /etc/crontab y los archivos de /etc/cron.d/, que llevan un campo más, el usuario, entre la hora y la orden. Las carpetas /etc/cron.daily, cron.weekly y cron.monthly ejecutan los scripts que pongas ahí.
Para ver las trampas de cron, preparamos cinco tareas que se ejecutan cada minuto. Una guarda su entorno, dos escriben la fecha (una mal y otra bien) y dos llaman a un script de ana que está en ~/bin:
Terminal$ crontab -l no crontab for ana $ crontab -e $ crontab -l # minuto hora día-del-mes mes día-de-la-semana orden * * * * * env > /home/ana/env-cron.txt * * * * * date +%F >> /home/ana/fecha.txt * * * * * date +\%F >> /home/ana/fecha-bien.txt 2>&1 * * * * * respaldo-mio.sh * * * * * respaldo-mio.sh >> /home/ana/cron-errores.log 2>&1
En la terminal de ana, el script se encuentra sin problema:
Terminal$ echo $PATH /home/ana/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games $ command -v respaldo-mio.sh /home/ana/bin/respaldo-mio.sh
Un minuto después, esto es lo que pasó:
Terminal$ ls *.txt *.log cron-errores.log env-cron.txt fecha-bien.txt $ grep PATH env-cron.txt PATH=/usr/bin:/bin $ cat fecha.txt cat: fecha.txt: No such file or directory $ cat fecha-bien.txt 2026-11-07 $ cat cron-errores.log /bin/sh: 1: respaldo-mio.sh: not found
Terminal$ sudo journalctl -t CRON --since 10:58 | grep -E 'CMD|MTA' Nov 07 10:58:01 servidor CRON[820]: (ana) CMD (env > /home/ana/env-cron.txt) Nov 07 10:58:01 servidor CRON[821]: (ana) CMD (respaldo-mio.sh) Nov 07 10:58:01 servidor CRON[822]: (ana) CMD (date +) Nov 07 10:58:01 servidor CRON[824]: (ana) CMD (respaldo-mio.sh >> /home/ana/cron-errores.log 2>&1) Nov 07 10:58:01 servidor CRON[825]: (ana) CMD (date +%F >> /home/ana/fecha-bien.txt 2>&1) Nov 07 10:58:01 servidor CRON[811]: (CRON) info (No MTA installed, discarding output) Nov 07 10:58:01 servidor CRON[813]: (CRON) info (No MTA installed, discarding output)
Las trampas de cron
Cada una de las tareas anteriores muestra una trampa:
- El PATH es mínimo. En cron vale
/usr/bin:/biny nada más, así querespaldo-mio.sh, que vive en~/bin, no se encuentra. Usa rutas absolutas (/home/ana/bin/respaldo-mio.sh) o define el PATH en la primera línea de la crontab:PATH=/home/ana/bin:/usr/local/bin:/usr/bin:/bin. - El % corta la orden. Para cron, un
%significa «salto de línea»: lo que va detrás se manda a la entrada de la orden. Por esodate +%F >> fecha.txtse quedó endate +, como dice el diario, yfecha.txtnunca se creó. Se escribe\%, como en la línea que sí funcionó. - La salida se pierde. cron envía por correo lo que imprime una tarea. Si el equipo no tiene servidor de correo, la tira: «No MTA installed, discarding output». Así se perdió el error de la tarea sin redirigir. La que terminaba en
>> /home/ana/cron-errores.log 2>&1sí lo guardó: «respaldo-mio.sh: not found». - No carga tu .bashrc. Ni tus alias ni las variables que definas ahí. Y la shell es
/bin/sh, no bash: si tu orden usa cosas propias de bash, ponla en un script que empiece con#!/bin/bash. - La hora es la del sistema. Si el servidor está en UTC,
0 3 * * *son las 3:00 UTC, que en el centro de México son las 21:00 del día anterior.
Cómo depurar una tarea de cron
- Confirma que cron la ejecutó:
sudo journalctl -t CRON(ogrep CRON /var/log/syslogen sistemas sin journal). Busca la líneaCMDcon tu orden. - Redirige siempre la salida a un archivo:
>> /ruta/tarea.log 2>&1. Es la única forma de ver el error. - Para probarla con el mismo entorno que cron, ponla una vez como
* * * * *conenv > /tmp/env-cron.txty compara ese archivo con tuenv. - Usa rutas absolutas para todo: el programa, los archivos y las carpetas.
Un ejemplo completo de Bash con cron, paso a paso, está en programar tareas con cron desde Bash.
¿systemd o cron?
Para un programa que debe estar siempre corriendo (un servidor, un bot, un trabajador de colas), la respuesta es un servicio de systemd: cron no vigila ni reinicia nada.
Para una tarea que se ejecuta de vez en cuando, los dos sirven:
- Elige un temporizador de systemd si quieres que no se pierda la ejecución cuando el equipo estaba apagado (
Persistent=true), tener la salida en el diario sin hacer nada, evitar que se encime con la anterior (systemd no arranca el servicio si sigue corriendo), limitar su memoria o CPU, o correrla con un usuario sin privilegios definido en la unidad. - Elige cron si quieres algo rápido de una línea, si el sistema no tiene systemd (contenedores, algunos BSD, routers) o si mantienes scripts que ya funcionan con él.
Una regla práctica: tareas nuevas en un servidor con systemd, con temporizadores; cron para lo sencillo y lo heredado.
Hoja de referencia
- Crear un servicio: archivo en
/etc/systemd/system/nombre.serviceconExecStart(ruta absoluta),User,Restart=on-failureyWantedBy=multi-user.target; despuéssudo systemctl daemon-reloadysudo systemctl enable --now nombre. - Ver qué pasa:
systemctl status nombreyjournalctl -u nombre -f. - Tras editar:
daemon-reloadyrestart. - Variables:
Environment="CLAVE=valor con espacios"oEnvironmentFile=con permisos 600 para secretos. - Temporizador:
nombre.servicede tipooneshotmásnombre.timerconOnCalendaryPersistent=true; prueba la fecha consystemd-analyze calendary revísalo consystemctl list-timers. - cron:
minuto hora día mes día-semana orden, concrontab -e; rutas absolutas,\%en vez de%y la salida a un archivo con>> archivo.log 2>&1.
Si estás empezando con la terminal, antes te servirá la guía de los comandos de Linux que de verdad se usan.
Todo se probó en un contenedor desechable con Debian 13 «trixie», systemd 257 como proceso 1, cron 3.0 y Python 3.13, sin conexión a la red y con un usuario normal con sudo. Las salidas están copiadas de esa ejecución; las horas son las del equipo de prueba. Imagen de portada: logotipo de systemd, de Tobias Bernard, con licencia CC BY-SA 4.0.
