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

  1. Qué es un sistema de init y qué resuelve systemd
  2. PID 1, cgroups y los tipos de unidad
  3. systemctl: consultar, controlar y habilitar
  4. Dónde viven las unidades y qué precedencia tienen
  5. Escribir una unidad .service sección por sección
  6. Endurecer la unidad y ponerle techo con cgroups v2
  7. Targets y niveles de ejecución
  8. Timers: el sustituto de cron
  9. Analizar el arranque y lanzar trabajos transitorios
  10. Caso Tramontana: el servicio, el timer y el despliegue completo

  1. 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.

  1. 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

  1. 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) o disabled. Un servicio puede estar active y disabled: funciona ahora, pero no volverá tras un reinicio.
  • Active: active (running) para un demonio, active (exited) para un oneshot que terminó bien, failed con 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=running

enable 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 original

edit 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.

  1. 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.

  1. Escribir una unidad .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=mixed
  • Restart=: 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 queda failed. 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.
  • TimeoutStopSec y KillMode=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.

  1. 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.0K

Ese 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.

  1. 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.

  1. 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.target

Verifica 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.timer

Persistent=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.

  1. 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-run

blame 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.

  1. 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.target

Decisiones 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
200

El 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/tramontana

Type=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/tramontana

inactive (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"
    fi

El 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-reload antes que nada. Y enable no arranca el servicio, solo crea el enlace: usa enable --now.
  • Requires= sin After=. No garantiza ningún orden: las dos unidades arrancan en paralelo. Prefiere Wants= 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 & o nohup en ExecStart. systemd lo pierde de vista y lo da por muerto: deja que corra en primer plano.
  • Restart=always sin StartLimitBurst. Una aplicación que no arranca entra en un bucle que consume CPU y llena el disco de logs. Y no te rindas ante ProtectSystem=strict: el fallo te dice dónde escribe tu aplicación, así que añade la ruta a ReadWritePaths en 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

  1. Diagnóstico de un servicio que no arranca. tramontana.service está en failed. Enumera, en orden, los cinco comandos que ejecutarías para averiguar la causa, y di qué te dice cada uno.
  2. Un timer con recuperación. Escribe el par .service + .timer que ejecute /home/operador/scripts/purgar_releases.sh todos 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.
  3. Drop-in en lugar de edición. El proveedor pide que Tramontana arranque con TRAMONTANA_MAX_HILOS=8 y con 768 MiB de límite de memoria, sin modificar tramontana.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 referencias

El 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.timer

Conflicts= 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=8

systemctl 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

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados