En 03-06 te enseñé ps, kill y nohup, y te dije con todas las letras que nada de eso sirve para gestionar un servicio de verdad. En 03-07 montaste la copia nocturna con cron y te avisé de que existía algo mejor. Esta es la lección donde se saldan las dos promesas, y es el corazón del módulo. Hoy Tramontana Reservas se arranca a mano: si srv-tramontana se reinicia, la aplicación no vuelve; si el proceso 1284 muere, nadie lo levanta; desplegar.sh cambia el enlace simbólico y no reinicia nada; y la copia de las 4:20 depende de una línea de cron sin control real de solapamiento ni registro decente. Al terminar tendrás tramontana.service escrito, endurecido y habilitado, y respaldo_tramontana.sh convertido en un servicio con su timer.
Contenido
- Qué es un sistema de init y qué resuelve systemd
- PID 1, cgroups y los tipos de unidad
systemctl: consultar, controlar y habilitar- Dónde viven las unidades y qué precedencia tienen
- Escribir una unidad
.servicesección por sección - Endurecer la unidad y ponerle techo con cgroups v2
- Targets y niveles de ejecución
- Timers: el sustituto de cron
- Analizar el arranque y lanzar trabajos transitorios
- Caso Tramontana: el servicio, el timer y el despliegue completo
- Qué es un sistema de init y qué resuelve systemd
El kernel arranca, monta la raíz y ejecuta un proceso: el init, con PID 1, del que desciende todo lo demás. Su trabajo es levantar el sistema, mantener los servicios vivos y apagarlo con orden.
| Aspecto | SysV init | systemd |
|---|---|---|
| Arranque | Scripts secuenciales /etc/init.d/* |
Paralelo, guiado por dependencias declaradas |
| Descripción | Código shell que reinventa arrancar, parar y recargar | Fichero declarativo de pocas líneas |
| Supervisión y seguimiento | Ninguna; ficheros PID que mienten | Reinicio automático con política y límites; cgroups, que el kernel sí conoce |
| Registro / tareas | Cada uno a su fichero; cron aparte | Journal estructurado (05-06); timers integrados |
| Aislamiento | Nada | Espacios de nombres, ProtectSystem, capabilities |
La consecuencia práctica de los cgroups es enorme: si tu aplicación lanza cinco hijos y uno queda huérfano, systemctl stop los mata a todos, porque el kernel los contabiliza en el mismo grupo de control. Con un fichero PID, ese huérfano seguía escribiendo mientras tú creías haber parado el servicio.
- PID 1, cgroups y los tipos de unidad
$ ps -p 1 -o comm= ; systemd-cgls --no-pager | sed -n '4,5p'
systemd
│ ├─tramontana.service
│ │ └─1284 /opt/tramontana/app/bin/tramontana --config ...Todo en systemd es una unidad, y el sufijo del fichero dice de qué tipo:
| Tipo | Para qué sirve | Ejemplo |
|---|---|---|
.service |
Un proceso gestionado: arrancar, parar, supervisar | tramontana.service |
.socket / .path |
Activa el servicio a la primera conexión / al cambiar un fichero | ssh.socket |
.target / .timer |
Punto de sincronización que agrupa unidades / dispara otra unidad a una hora | multi-user.target, tramontana-respaldo.timer |
.mount / .slice |
Punto de montaje (uno por línea de fstab) / grupo que reparte CPU, memoria y E/S |
srv-tramontana-backups.mount |
systemctl: consultar, controlar y habilitar
systemctl: consultar, controlar y habilitar$ systemctl status tramontana.service
● tramontana.service - Tramontana Reservas (aplicación web)
Loaded: loaded (/etc/systemd/system/tramontana.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-08-18 04:31:02 CEST; 7h 12min ago
Main PID: 1284 (tramontana)
Tasks: 12 (limit: 4571)
Memory: 214.8M (max: 512.0M available: 297.1M)
CGroup: /system.slice/tramontana.service
└─1284 /opt/tramontana/app/bin/tramontana --config /etc/tramontana/app.conf
ago 18 11:02:17 srv-tramontana tramontana[1284]: db_timeout tras 30s (conexiones=200)Hay que leer la salida entera:
Loaded: de qué fichero viene y si estáenabled(arranca solo al iniciar la máquina) odisabled. Un servicio puede estaractiveydisabled: funciona ahora, pero no volverá tras un reinicio.Active:active (running)para un demonio,active (exited)para unoneshotque terminó bien,failedcon el código de salida y la señal si murió.Tasks,Memory,CGroup: consumo real del cgroup y árbol completo de procesos, hijos incluidos, no una estimación. Las últimas líneas son log del journal, que exprimirás en 05-06.
sudo systemctl start|stop|restart tramontana.service
sudo systemctl reload-or-restart tramontana.service # recarga (si hay ExecReload) o reinicia
systemctl is-active|is-enabled|is-failed tramontana.service # scripts: código 0/3
systemctl --failed ; systemctl list-units --type=service --state=runningenable es literalmente un enlace simbólico
Aquí se cierra el círculo con 02-06:
$ sudo systemctl enable tramontana.service
Created symlink /etc/systemd/system/multi-user.target.wants/tramontana.service → /etc/systemd/system/tramontana.service.
$ sudo systemctl mask tramontana.service
Created symlink /etc/systemd/system/tramontana.service → /dev/null.enable crea el enlace dentro del .wants del target que indique [Install] y disable lo borra: nada más. Por eso enable no arranca el servicio (para eso, enable --now) y disable no lo para. mask enlaza la unidad a /dev/null, y así no se puede arrancar en absoluto —ni a mano, ni como dependencia, ni por un socket—, que es lo que quieres durante un mantenimiento; unmask lo revierte.
| Estado | ¿Arranca al iniciar? | ¿A mano? |
|---|---|---|
enabled / disabled |
Sí / no | Sí en ambos casos |
masked / static |
No / solo como dependencia (no tiene [Install]) |
No, de ninguna forma / sí |
sudo systemctl daemon-reload es obligatorio tras crear o editar cualquier fichero de unidad: si no, systemd sigue trabajando con la versión anterior en memoria y te volverás loco. Y recargar la configuración no reinicia el servicio: eso es un restart aparte.
systemctl cat tramontana.service # la unidad efectiva, con sus drop-ins
systemctl show tramontana.service -p Restart -p User -p MemoryMax
systemctl list-dependencies tramontana.service
sudo systemctl edit tramontana.service # crea un DROP-IN sin tocar el originaledit abre /etc/systemd/system/tramontana.service.d/override.conf, donde escribes solo la sección y las directivas a cambiar. Un drop-in es mejor que editar la unidad original: sobrevive a la actualización del paquete que la trajo, deja explícito qué has cambiado tú y se elimina borrando un fichero. Aviso con las directivas de lista (ExecStart, Environment): para sustituir hay que vaciarlas antes con ExecStart= en blanco, o se acumulan.
- Dónde viven las unidades y qué precedencia tienen
Por prioridad decreciente: /etc/systemd/system/ (donde escribes tú), /run/systemd/system/ (unidades transitorias, en RAM) y /lib/systemd/system/ (las de los paquetes). Tus unidades van en /etc/systemd/system/; las de los paquetes no se tocan, se ajustan con drop-ins. Un fichero con el mismo nombre en /etc sustituye por completo al de /lib.
- Escribir una unidad
.service sección por sección
.service sección por sección[Unit]: qué es y con quién se relaciona
Aquí se lía todo el mundo, porque hay dos ejes independientes: el orden (cuándo arranca respecto a otra unidad) y la dependencia (si la necesita para existir).
| Directiva | Eje | Significado |
|---|---|---|
After= / Before= |
Orden | «Arráncame después/antes de X», sin obligar a que X exista |
Wants= |
Dependencia débil | Intenta arrancar X; si X falla, yo arranco igual |
Requires= / BindsTo= |
Dependencia fuerte | Si X falla, yo no arranco; con BindsTo, además me paro si X se para |
Conflicts= |
Exclusión | X y yo no podemos estar activos a la vez |
El error clásico es poner Requires=postgresql.service y creer que garantiza que la base de datos esté lista antes que tú. No lo garantiza: Requires sin After arranca las dos en paralelo, así que hacen falta las dos directivas. Como norma usa Wants=: si el servicio de logs remoto falla, prefieres que tu aplicación arranque igual.
[Service]: cómo se ejecuta
Type= |
Cuándo se considera arrancado | Cuándo usarlo |
|---|---|---|
simple / exec |
Al lanzar ExecStart / cuando el execve() tiene éxito |
Proceso en primer plano; exec detecta además un binario inexistente |
forking |
Cuando el padre sale y deja al hijo | Demonios clásicos; requiere PIDFile= |
oneshot |
Cuando el proceso termina | Copias y migraciones; con RemainAfterExit=yes queda como activo |
notify |
Cuando el proceso avisa por sd_notify() |
Servicios que saben decir «ya estoy listo» |
Un servicio moderno en Go, Java o Python casi siempre es simple o exec: no lo mandes al fondo con & ni uses nohup, porque systemd ya se encarga de eso. Las demás directivas del día a día:
ExecStartPre=/usr/bin/test -r /etc/tramontana/app.conf # si falla, no arranca
ExecStart=/opt/tramontana/app/bin/tramontana --config /etc/tramontana/app.conf
ExecReload=/bin/kill -HUP $MAINPID # qué hace 'systemctl reload'
ExecStop=/opt/tramontana/app/bin/tramontana --parada-limpia
User=svc-tramontana
Group=tramontana
Environment=TRAMONTANA_ENTORNO=produccion
EnvironmentFile=-/etc/tramontana/entorno # el '-': si no existe, sigue igual
Restart=on-failure
RestartSec=5
StartLimitBurst=5
TimeoutStopSec=30
KillMode=mixedRestart=:no(por defecto),on-failure(código distinto de cero o señal fatal: la opción correcta para una aplicación),always,on-abnormal.RestartSec+StartLimitBurst+StartLimitIntervalSec: reinicia a los 5 s, y si lo hace 5 veces en 300 s se rinde y quedafailed. Sin ese límite, una aplicación que no arranca entra en un bucle infinito que consume CPU y llena el log; para volver,systemctl reset-failed.TimeoutStopSecyKillMode=mixed: cuánto se espera tras el SIGTERM antes del SIGKILL, y que este alcance a todo el cgroup. Es la secuencia «SIGTERM → esperar → verificar →-9» de 03-06, automatizada.
[Install]: cuándo arrancar solo
Suele tener una línea, WantedBy=multi-user.target, y es lo que lee enable para saber en qué directorio .wants crear el enlace. Una unidad sin [Install] no se puede habilitar: aparecerá como static.
- Endurecer la unidad y ponerle techo con cgroups v2
Es una de las cosas más valiosas de systemd, y es gratis: unas líneas dejan tu servicio con muchísima menos superficie de ataque de la que lograrías a mano.
| Directiva | Qué hace |
|---|---|
NoNewPrivileges=yes |
El proceso no puede ganar privilegios: anula SUID y setcap de 05-02 |
PrivateTmp= / PrivateDevices= / ProtectHome= / ProtectKernelTunables= |
/tmp propio; solo dispositivos básicos; /home y /root invisibles; /proc/sys de solo lectura |
ProtectSystem=strict + ReadWritePaths= |
Todo el FS en solo lectura salvo la lista mínima que abras |
RestrictAddressFamilies= |
Qué familias de sockets puede usar (AF_INET AF_INET6 AF_UNIX) |
CapabilityBoundingSet= / AmbientCapabilities= |
Techo de capabilities (vacío = ninguna) / las que sí concede, p. ej. CAP_NET_BIND_SERVICE |
LockPersonality=yes, MemoryDenyWriteExecute=yes |
Cierran vías de explotación conocidas |
systemd-analyze security tramontana.service puntúa el resultado: la unidad que escribiremos al final de la lección saca 2.9 OK, frente al 9.6 UNSAFE que sacaba sin endurecer. Nota práctica: ProtectSystem=strict romperá tu servicio la primera vez, porque escribe en algún sitio que no has declarado. No lo desactives: mira el log, añade la ruta a ReadWritePaths y vuelve a probar. Ese ejercicio te obliga a saber exactamente dónde escribe tu aplicación.
A la misma sección [Service] pertenecen los límites de recursos, que systemd aplica con cgroups v2:
MemoryMax=512M # duro: al superarlo, el OOM killer actúa DENTRO del servicio
MemoryHigh=384M # a partir de aquí se presiona para que libere, sin matar
CPUQuota=150% # como mucho, 1,5 núcleos de los 2; TasksMax=64 contiene una fork bomb
IOWeight=50 # prioridad relativa de E/S$ systemd-cgtop --iterations=1 | sed -n '3p'
system.slice/tramontana.service 12 2.8 214.8M 12.0K 840.0KEse MemoryMax impide que una fuga de memoria se lleve por delante la base de datos y el propio SSH: al superarlo, el kernel mata dentro del cgroup del servicio y Restart=on-failure lo levanta.
- Targets y niveles de ejecución
| Target | Qué es |
|---|---|
multi-user.target (runlevel 3) |
Sistema completo en red sin entorno gráfico: lo normal en un servidor; graphical.target añade el escritorio |
rescue.target / emergency.target |
Monousuario con FS locales / solo una shell con / en lectura: adonde te manda un fstab roto |
También existe network-online.target, el punto de sincronización que indica que hay red utilizable. systemctl get-default dice cuál es el predeterminado, set-default lo cambia y sudo systemctl isolate rescue.target cambia de target ahora mismo, cortando los servicios que sobran.
- Timers: el sustituto de cron
Un timer es una unidad que activa otra: por convención, tramontana-respaldo.timer dispara tramontana-respaldo.service (mismo nombre, otro sufijo).
| Aspecto | cron | Timer de systemd |
|---|---|---|
| Sintaxis | Cinco campos crípticos | OnCalendar= legible y verificable |
| Ejecución perdida (máquina apagada) | Se pierde | Persistent=true la ejecuta al arrancar |
| Registro y entorno | Lo que redirijas; PATH mínimo |
Journal (journalctl -u); el entorno de la unidad |
| Solapamiento y dependencias | flock a mano; ninguna dependencia |
No se relanza si sigue activo; After=, Requires= y el endurecimiento de la sección 6 |
| Ver lo programado | crontab -l por usuario |
systemctl list-timers, global |
# /etc/systemd/system/tramontana-respaldo.timer
[Unit]
Description=Copia diaria de Tramontana Reservas
[Timer]
OnCalendar=*-*-* 04:20:00
Persistent=true
RandomizedDelaySec=180
Unit=tramontana-respaldo.service
[Install]
WantedBy=timers.targetVerifica siempre la expresión antes de confiar en ella:
$ systemd-analyze calendar '*-*-* 04:20:00'
Next elapse: Wed 2026-08-19 04:20:00 CEST (From now: 17h left)Otras formas de disparo: OnBootSec=5min (tras arrancar la máquina), OnUnitActiveSec=1h (cada hora desde la última ejecución) y OnStartupSec=. Y expresiones legibles: daily, weekly, Mon..Fri 08:00, *-*-01 03:00 (día 1 de cada mes).
$ systemctl list-timers --all | tail -1
Wed 2026-08-19 04:20:00 CEST 17h left Tue 2026-08-18 04:21:43 CEST tramontana-respaldo.timerPersistent=true es la razón principal para migrar de cron: si el servidor estaba apagado a las 4:20, la copia se ejecuta en cuanto arranque en lugar de perderse hasta el día siguiente.
- Analizar el arranque y lanzar trabajos transitorios
$ systemd-analyze
Startup finished in 3.412s (kernel) + 11.807s (userspace) = 15.219s
$ systemd-analyze blame | head -1
6.104s [email protected]
$ systemd-analyze critical-chain tramontana.service | head -3
tramontana.service +412ms
└─postgresql.service @5.201s +6.104s
$ sudo systemd-run --unit=purga --property=MemoryMax=256M /home/operador/scripts/purgar_releases.sh --dry-runblame ordena por duración; critical-chain muestra la cadena que de verdad retrasa el arranque de una unidad concreta, que es lo que importa: un servicio lento del que no depende nadie no retrasa nada. Y systemd-run lanza algo puntual como unidad transitoria, registrado en el journal y con sus límites aplicados, a diferencia de un nohup.
- Caso Tramontana: el servicio, el timer y el despliegue completo
tramontana.service
# /etc/systemd/system/tramontana.service
[Unit]
Description=Tramontana Reservas (aplicación web)
Documentation=https://intranet.tramontana.example/runbook
Wants=network-online.target
After=network-online.target postgresql.service
[Service]
Type=exec
User=svc-tramontana
Group=tramontana
UMask=0027
WorkingDirectory=/opt/tramontana/app
Environment=TRAMONTANA_ENTORNO=produccion
ExecStartPre=/usr/bin/test -r /etc/tramontana/app.conf
ExecStart=/opt/tramontana/app/bin/tramontana --config /etc/tramontana/app.conf
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitBurst=5
TimeoutStopSec=30
KillMode=mixed
# Endurecimiento
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/log/tramontana /opt/tramontana/shared/uploads
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
CapabilityBoundingSet=
MemoryMax=512M
[Install]
WantedBy=multi-user.targetDecisiones que conviene entender: Type=exec porque la aplicación se queda en primer plano y así un binario inexistente se detecta al arrancar; User=svc-tramontana con Group=tramontana, las identidades de 05-01, y UMask=0027 por convención del curso; ReadWritePaths con solo el log y las subidas, porque todo lo demás —incluido /opt/tramontana— debe ser inmutable para la aplicación; y CapabilityBoundingSet= vacío porque escucha en el 8080 (si pasara al 80 se añadiría AmbientCapabilities=CAP_NET_BIND_SERVICE, sustituyendo al setcap de 05-02, que se perdía en cada despliegue).
$ sudo systemctl daemon-reload && sudo systemctl enable --now tramontana.service
$ systemctl is-active tramontana.service && curl -sf -o /dev/null -w '%{http_code}\n' http://10.0.2.15:8080/salud
active
200El timer de la copia
# /etc/systemd/system/tramontana-respaldo.service
[Unit]
Description=Copia de seguridad de Tramontana Reservas
RequiresMountsFor=/srv/tramontana/backups
[Service]
Type=oneshot
User=operador
Group=tramontana
UMask=0027
ExecStart=/home/operador/scripts/respaldo_tramontana.sh -q
TimeoutStartSec=30min
Nice=10
IOSchedulingClass=idle
ProtectSystem=strict
ReadWritePaths=/srv/tramontana/backups /var/log/tramontanaType=oneshot porque el script termina, y sin [Install] porque no se habilita: lo dispara el timer, y por eso aparecerá como static. RequiresMountsFor= es la joya de esta unidad: si el volumen LVM de 05-04 no está montado, la copia no se ejecuta en lugar de escribir alegremente en el directorio vacío de /, un fallo silencioso que solo descubrirías el día de la restauración. Nice=10 e IOSchedulingClass=idle hacen que la copia ceda el paso a la aplicación, algo que en 05-07 resultará importante.
$ sudo systemctl daemon-reload && sudo systemctl enable --now tramontana-respaldo.timer
$ sudo systemctl start tramontana-respaldo.service # probarla YA, sin esperar a las 4:20
$ systemctl status tramontana-respaldo.service | sed -n '3p'
Active: inactive (dead) since Tue 2026-08-18 11:41:09 CEST; 8s ago
$ sudo sed -i.bak-$(date +%F) '/respaldo_tramontana.sh/s/^/#MIGRADO A TIMER /' /etc/cron.d/tramontanainactive (dead) tras un oneshot es éxito: el script terminó con código 0; un failed mostraría el código de salida que definiste en 04-03. Y ese sed final retira la línea de cron, que es la mitad del trabajo que la gente olvida: dejar las dos cosas activas significa dos copias simultáneas peleando por el flock cada madrugada.
desplegar.sh reinicia por fin
La deuda del Módulo 4: tras el ln -sfn atómico, el script comprobaba la salud contra un proceso que seguía ejecutando el release anterior, porque el binario ya estaba cargado en memoria. Ahora:
ln -sfn "releases/${version}" "${TRAMONTANA_BASE}/app.nuevo"
mv -T "${TRAMONTANA_BASE}/app.nuevo" "${TRAMONTANA_BASE}/app"
sudo systemctl restart tramontana.service || morir 74 "systemctl restart falló"
for _ in 1 2 3 4 5 6; do # esperar a que esté arriba antes de medir la salud
systemctl is-active --quiet tramontana.service && break; sleep 2
done
if ! curl -fsS --max-time 5 "http://127.0.0.1:8080/salud" >/dev/null; then
error "la ${version} no responde; revirtiendo"
ln -sfn "releases/${anterior}" "${TRAMONTANA_BASE}/app.nuevo"
mv -T "${TRAMONTANA_BASE}/app.nuevo" "${TRAMONTANA_BASE}/app"
sudo systemctl restart tramontana.service
morir 75 "rollback a ${anterior} completado"
fiEl sudo systemctl restart funciona sin contraseña interactiva gracias a la regla de /etc/sudoers.d/tramontana que escribiste en 05-02: aquí es donde las tres lecciones encajan. Y systemctl is-active --quiet en bucle evita el fallo clásico de comprobar la salud antes de que el proceso haya abierto el puerto.
Errores Comunes y Consejos
- Olvidar
daemon-reload. Editas la unidad, reinicias y no cambia nada: si has tocado un fichero de unidad,daemon-reloadantes que nada. Yenableno arranca el servicio, solo crea el enlace: usaenable --now. Requires=sinAfter=. No garantiza ningún orden: las dos unidades arrancan en paralelo. PrefiereWants=salvo que de verdad no puedas funcionar sin la otra. Y al migrar a un timer, comenta la línea de cron en el mismo cambio o tendrás dos ejecuciones simultáneas.- Mandar el proceso al fondo con
&onohupenExecStart. systemd lo pierde de vista y lo da por muerto: deja que corra en primer plano. Restart=alwayssinStartLimitBurst. Una aplicación que no arranca entra en un bucle que consume CPU y llena el disco de logs. Y no te rindas anteProtectSystem=strict: el fallo te dice dónde escribe tu aplicación, así que añade la ruta aReadWritePathsen lugar de quitar la protección.- Editar la unidad de un paquete en
/lib/systemd/system. La próxima actualización se lleva tu trabajo: drop-in en/etc/systemd/system/<unidad>.d/. - Consejo: guarda tus unidades en git junto a los scripts. Una unidad es código de producción y se revisa como tal.
Ejercicios
- Diagnóstico de un servicio que no arranca.
tramontana.serviceestá enfailed. Enumera, en orden, los cinco comandos que ejecutarías para averiguar la causa, y di qué te dice cada uno. - Un timer con recuperación. Escribe el par
.service+.timerque ejecute/home/operador/scripts/purgar_releases.shtodos los lunes a las 05:10, que se recupere si el servidor estaba apagado, que no se solape con la copia y que quede registrado. Verifica la expresión de calendario. - Drop-in en lugar de edición. El proveedor pide que Tramontana arranque con
TRAMONTANA_MAX_HILOS=8y con 768 MiB de límite de memoria, sin modificartramontana.service. Hazlo y demuestra que ha surtido efecto.
Soluciones
1.
systemctl status tramontana.service # 1. Estado, código de salida y últimas líneas
journalctl -u tramontana.service -n 50 # 2. El log completo del arranque fallido
systemctl cat tramontana.service # 3. La unidad EFECTIVA, con sus drop-ins
sudo -u svc-tramontana /opt/tramontana/app/bin/tramontana --config /etc/tramontana/app.conf
systemd-analyze verify /etc/systemd/system/tramontana.service # 5. Sintaxis y referenciasEl cuarto comando lanza la aplicación a mano con su usuario: ¿falla la app o la unidad?
Es el paso que más tiempo ahorra. Si a mano funciona y por systemd no, el culpable suele ser el endurecimiento —una ruta que falta en ReadWritePaths— o el entorno, porque la unidad no hereda tus variables.
2.
# /etc/systemd/system/tramontana-purga.service
[Unit]
Description=Purga de releases antiguos de Tramontana
After=tramontana-respaldo.service
Conflicts=tramontana-respaldo.service
[Service]
Type=oneshot
User=operador
Group=tramontana
ExecStart=/home/operador/scripts/purgar_releases.sh
TimeoutStartSec=15min
ProtectSystem=strict
ReadWritePaths=/opt/tramontana/releases /var/log/tramontana
# /etc/systemd/system/tramontana-purga.timer
[Unit]
Description=Purga semanal de releases
[Timer]
OnCalendar=Mon 05:10
Persistent=true
RandomizedDelaySec=300
Unit=tramontana-purga.service
[Install]
WantedBy=timers.target$ systemd-analyze calendar 'Mon 05:10' | grep 'Next elapse'
Next elapse: Mon 2026-08-24 05:10:00 CEST
$ sudo systemctl daemon-reload && sudo systemctl enable --now tramontana-purga.timerConflicts= más After= garantizan que no coincidan, el registro es automático en el journal y no hace falta flock: systemd no relanza una unidad que ya está activa.
3. Con sudo systemctl edit tramontana.service, que crea el drop-in:
### /etc/systemd/system/tramontana.service.d/override.conf
[Service]
Environment=TRAMONTANA_MAX_HILOS=8
MemoryMax=768M$ sudo systemctl daemon-reload && sudo systemctl restart tramontana.service
$ systemctl show tramontana.service -p MemoryMax -p Environment
MemoryMax=805306368
Environment=TRAMONTANA_ENTORNO=produccion TRAMONTANA_MAX_HILOS=8systemctl show da el valor efectivo (en bytes) y systemctl cat mostraría el drop-in al final de la unidad sin haber tocado el original. Y Environment= es acumulativa: la variable nueva se suma a las que ya definía la unidad.
Conclusión
srv-tramontana ya se gobierna solo. Sabes qué es un init y qué aporta systemd frente a SysV —arranque paralelo por dependencias, supervisión real, cgroups que no mienten, registro estructurado y aislamiento—; conoces los tipos de unidad y manejas systemctl de punta a punta: lees un status línea a línea, controlas con start/restart/reload, distingues enabled de active y disable de mask, sabes que enable es literalmente crear un enlace simbólico y que daemon-reload es obligatorio tras tocar cualquier fichero de unidad. Ajustas con drop-ins en lugar de editar unidades ajenas.
Escribes una unidad desde cero y entiendes las dos cosas donde todo el mundo tropieza: la diferencia entre orden (After/Before) y dependencia (Wants/Requires/BindsTo), y qué Type= corresponde a cada forma de arrancar. Endureces con NoNewPrivileges, ProtectSystem=strict y ReadWritePaths, lo compruebas con systemd-analyze security, pones techo con MemoryMax, CPUQuota y TasksMax vigilándolo con systemd-cgtop, manejas targets, analizas el arranque con blame y critical-chain y lanzas trabajos transitorios con systemd-run. Y las deudas están saldadas: tramontana.service existe, endurecido, con reinicio automático y límites, y arranca solo tras un reinicio del servidor; respaldo_tramontana.sh es ahora tramontana-respaldo.service disparado por un timer a las 04:20 con Persistent=true y RequiresMountsFor sobre el volumen de 05-04, con la línea de cron ya retirada; y desplegar.sh hace el systemctl restart que le faltaba, apoyándose en la regla de sudoers de 05-02. Falta la memoria del sistema. Tu servicio ya escribe en el journal, pero no sabes consultarlo; /var/log/tramontana/acceso.log lleva 412 líneas y creciendo sin que nadie lo rote, que fue la primera deuda que anuncié al cerrar el Módulo 4 y sigue sin pagar; y cuando Marta pregunte «¿qué pasó anoche a las 03:00?», ahora mismo no sabrías responder con precisión. En Registros del Sistema: journald y syslog verás las dos capas de registro que conviven en Ubuntu, exprimirás journalctl con todos sus filtros, harás el journal persistente, escribirás un /etc/logrotate.d/tramontana correcto y probado en simulación, conectarás lib/comunes.sh con el journal mediante logger, y aprenderás qué nunca debe acabar escrito en un log.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
