systemd es el gestor de servicios y proceso de arranque (PID 1) de Ubuntu, Debian, Rocky Linux y la mayoría de distribuciones actuales. Todo lo que gestiona se describe como una unit: servicios, sockets, temporizadores, puntos de montaje y grupos de units llamados targets. En este tutorial construirás en Ubuntu 24.04 una pequeña aplicación compuesta por varias units para ver en la práctica cómo funcionan los tipos de unit, las dependencias, los targets, las instancias, la activación por socket y los timers.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS (vale igual en Debian 12 o Rocky Linux 9), por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Soltura básica con systemctl start, stop y status.

Conceptos: tipos de unit y dónde viven

Cada unit es un fichero de texto con formato INI cuya extensión indica el tipo:

TipoExtensiónPara qué sirve
Service.serviceEjecutar y vigilar un proceso
Socket.socketEscuchar en un socket y arrancar un servicio al recibir conexiones
Timer.timerLanzar otra unit según un calendario o un intervalo
Target.targetAgrupar units y marcar puntos de sincronización
Mount.mountMontar un sistema de ficheros
Path.pathActivar una unit cuando cambia un fichero
Slice.sliceAgrupar procesos en cgroups para repartir recursos

systemd busca las units en varios directorios con esta prioridad (de mayor a menor):

  • /etc/systemd/system/: units y ajustes del administrador. Aquí trabajarás.
  • /run/systemd/system/: units creadas en tiempo de ejecución.
  • /usr/lib/systemd/system/: units que instalan los paquetes. No las edites, se sobrescriben al actualizar.

Para ver el fichero completo de una unit, incluidos los ficheros de ajuste que la modifican, usa systemctl cat:

systemctl cat ssh.service
# /usr/lib/systemd/system/ssh.service
[Unit]
Description=OpenBSD Secure Shell server
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target auditd.service
...

Paso 1: Crear un servicio

Vas a crear un servidor web mínimo con el módulo http.server de Python, que viene instalado en Ubuntu. Prepara el directorio que servirá:

sudo mkdir -p /srv/demo-web
echo 'hola desde systemd' | sudo tee /srv/demo-web/index.html

Crea la unit:

sudo nano /etc/systemd/system/demo-web.service
[Unit]
Description=Servidor web de demostración
After=network.target

[Service]
Type=exec
ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1 --directory /srv/demo-web
DynamicUser=yes
Restart=on-failure
RestartSec=5s
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

Lo más importante de cada sección:

  • [Unit] describe la unit y sus relaciones con otras. After=network.target solo ordena el arranque.
  • [Service] define el proceso. Type=exec da el servicio por iniciado cuando el binario se ha ejecutado correctamente. DynamicUser=yes crea un usuario temporal sin privilegios para el proceso, y las líneas Protect* hacen el sistema de ficheros de solo lectura para él.
  • [Install] solo se usa al ejecutar enable: crea un enlace para que multi-user.target arranque el servicio en cada inicio.

Valida el fichero, recarga systemd y arranca el servicio:

sudo systemd-analyze verify /etc/systemd/system/demo-web.service
sudo systemctl daemon-reload
sudo systemctl enable --now demo-web.service

systemd-analyze verify no imprime nada si la unit es correcta. Comprueba el estado y que responde:

systemctl status demo-web.service --no-pager
curl http://127.0.0.1:8080/
● demo-web.service - Servidor web de demostración
     Loaded: loaded (/etc/systemd/system/demo-web.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 10:20:11 UTC; 3s ago
   Main PID: 2154 (python3)
hola desde systemd

La salida estándar del proceso va al journal. Consulta los accesos:

journalctl -u demo-web.service -n 5 --no-pager

Paso 2: Entender las dependencias

systemd separa dos ideas que es fácil confundir: la dependencia (qué otras units se activan junto a esta) y el orden (cuál arranca antes). Una directiva de dependencia sin After= o Before= arranca ambas units en paralelo.

DirectivaEfecto
Wants=Activa también la otra unit; si falla, esta sigue adelante. Es la opción recomendada por defecto.
Requires=Activa la otra unit; si no arranca, esta tampoco. Si se detiene explícitamente, esta también.
BindsTo=Como Requires=, y además esta se detiene si la otra deja de estar activa por cualquier motivo.
PartOf=Solo propaga stop y restart desde la otra unit hacia esta. No activa nada.
Conflicts=Arrancar una detiene la otra.
After= / Before=Solo orden de arranque (y el inverso en la parada).

Un ejemplo habitual: una aplicación que necesita PostgreSQL debe declarar las dos cosas, porque Requires= sin After= podría arrancar la aplicación antes de que la base de datos acepte conexiones:

[Unit]
Requires=postgresql.service
After=postgresql.service

Para ver el árbol de dependencias de una unit, y a la inversa, quién depende de ella:

systemctl list-dependencies demo-web.service --no-pager
systemctl list-dependencies --reverse demo-web.service --no-pager
demo-web.service
● └─multi-user.target

La salida inversa muestra que multi-user.target la arrastra, gracias al enlace que creó enable.

Paso 3: Agrupar servicios en un target

Un target es una unit sin proceso que agrupa otras. multi-user.target y graphical.target son los equivalentes modernos a los antiguos runlevels 3 y 5. Puedes crear tus propios targets para arrancar y parar una aplicación compuesta con un solo comando.

Crea un target para la aplicación de demostración:

sudo nano /etc/systemd/system/demo.target
[Unit]
Description=Aplicación de demostración completa
Wants=demo-web.service
After=demo-web.service

[Install]
WantedBy=multi-user.target

Añade a demo-web.service la directiva PartOf= para que parar o reiniciar el target se propague al servicio. En lugar de editar el fichero, usa un fichero de ajuste (drop-in), que es la forma correcta de modificar units, incluidas las de los paquetes:

sudo systemctl edit demo-web.service

Escribe en el espacio indicado por el editor:

[Unit]
PartOf=demo.target

systemctl edit guarda el cambio en /etc/systemd/system/demo-web.service.d/override.conf y recarga systemd. Habilita el target y prueba la propagación:

sudo systemctl enable --now demo.target
sudo systemctl stop demo.target
systemctl is-active demo-web.service
inactive

Vuelve a arrancar todo con sudo systemctl start demo.target. Para consultar o cambiar el target con el que arranca el sistema:

systemctl get-default
graphical.target

En servidores sin entorno gráfico es habitual fijarlo en multi-user.target con sudo systemctl set-default multi-user.target.

Paso 4: Lanzar varias instancias con una plantilla

Una unit cuyo nombre acaba en @ es una plantilla. Al arrancar [email protected], systemd sustituye %i por texto. Es útil para varios trabajadores iguales con distinto parámetro. Crea la plantilla:

sudo nano /etc/systemd/system/[email protected]
[Unit]
Description=Trabajador de demostración %i
PartOf=demo.target

[Service]
Type=exec
DynamicUser=yes
ExecStart=/bin/sh -c 'while true; do echo "trabajador %i activo"; sleep 30; done'
Restart=on-failure

[Install]
WantedBy=demo.target

Arranca dos instancias y compruébalas:

sudo systemctl daemon-reload
sudo systemctl enable --now [email protected] [email protected]
systemctl list-units 'demo-worker@*' --no-pager
  UNIT                     LOAD   ACTIVE SUB     DESCRIPTION
  [email protected]    loaded active running Trabajador de demostración 1
  [email protected]    loaded active running Trabajador de demostración 2

Como WantedBy=demo.target, las instancias habilitadas arrancan y se detienen junto al target.

Paso 5: Activar un servicio por socket

Con la activación por socket, systemd abre el puerto y solo arranca el servicio cuando llega una conexión. Así el servicio no consume recursos mientras nadie lo usa y el puerto está disponible desde el primer momento del arranque. Crea un servidor de eco que devuelve lo que recibe. Primero el socket:

sudo nano /etc/systemd/system/demo-eco.socket
[Unit]
Description=Socket del servidor de eco

[Socket]
ListenStream=127.0.0.1:9999
Accept=yes

[Install]
WantedBy=sockets.target

Con Accept=yes, systemd arranca una instancia del servicio por cada conexión, así que el servicio debe ser una plantilla con el mismo nombre base:

sudo nano /etc/systemd/system/[email protected]
[Unit]
Description=Servidor de eco para una conexión

[Service]
ExecStart=/usr/bin/cat
StandardInput=socket
DynamicUser=yes

StandardInput=socket conecta la entrada y la salida del proceso a la conexión, así que cat devuelve al cliente todo lo que recibe. Habilita solo el socket:

sudo systemctl daemon-reload
sudo systemctl enable --now demo-eco.socket
echo 'hola eco' | nc -N 127.0.0.1 9999
hola eco

Comprueba el socket y cuántas conexiones ha aceptado:

systemctl status demo-eco.socket --no-pager | grep -E 'Active|Listen|Accepted'
     Active: active (listening) since Thu 2026-09-25 10:31:40 UTC; 20s ago
     Listen: 127.0.0.1:9999 (Stream)
   Accepted: 1; Connected: 0;

Para un servicio de larga duración que gestione todas las conexiones él mismo (por ejemplo un servidor web con soporte de sd_listen_fds), usa Accept=no y un servicio normal con el mismo nombre que el socket.

Paso 6: Programar tareas con un timer

Los timers sustituyen a cron con ventajas: la salida queda en el journal, las ejecuciones perdidas mientras el equipo estaba apagado se recuperan con Persistent=true y puedes ver la próxima ejecución con un comando. Un timer siempre activa otra unit, normalmente un servicio oneshot. Crea el servicio:

sudo nano /etc/systemd/system/demo-limpieza.service
[Unit]
Description=Limpieza de ficheros temporales antiguos de la demo

[Service]
Type=oneshot
ExecStart=/usr/bin/find /var/tmp -maxdepth 1 -name 'demo-*' -mtime +7 -delete

Crea el timer con el mismo nombre base:

sudo nano /etc/systemd/system/demo-limpieza.timer
[Unit]
Description=Limpieza diaria de la demo

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15min
Persistent=true

[Install]
WantedBy=timers.target

Antes de activarlo, comprueba cómo interpreta systemd la expresión de calendario:

systemd-analyze calendar '*-*-* 03:30:00'
  Original form: *-*-* 03:30:00
Normalized form: *-*-* 03:30:00
    Next elapse: Fri 2026-09-26 03:30:00 UTC
       From now: 17h left

Activa el timer y lanza el servicio una vez a mano para probarlo:

sudo systemctl daemon-reload
sudo systemctl enable --now demo-limpieza.timer
sudo systemctl start demo-limpieza.service
systemctl list-timers demo-limpieza.timer --no-pager

Otras expresiones útiles son hourly, daily, weekly, Mon..Fri *-*-* 08:00 o, para intervalos relativos, OnBootSec=5min combinado con OnUnitActiveSec=1h.

Paso 7: Analizar el arranque

systemd registra cuánto tarda cada unit en arrancar. Para ver el tiempo total y las units más lentas:

systemd-analyze
systemd-analyze blame --no-pager | head -n 5
Startup finished in 2.101s (kernel) + 6.842s (userspace) = 8.943s
graphical.target reached after 6.801s in userspace.
3.012s cloud-init.service
1.204s systemd-networkd-wait-online.service
...

blame suma tiempos que ocurren en paralelo, así que para saber qué retrasa de verdad el arranque usa la cadena crítica:

systemd-analyze critical-chain demo.target

Para eliminar las units de prueba al terminar, deshabilítalas y borra sus ficheros:

sudo systemctl disable --now demo.target demo-web.service '[email protected]' '[email protected]' demo-eco.socket demo-limpieza.timer
sudo rm -r /etc/systemd/system/demo* /srv/demo-web
sudo systemctl daemon-reload

Solución de problemas

Unit demo-web.service not found. Ejecuta sudo systemctl daemon-reload después de crear o editar una unit y comprueba el nombre con systemctl list-unit-files 'demo*'.

El servicio arranca y cae en bucle. Consulta los errores con journalctl -u demo-web.service -b --no-pager -n 50. Si systemd deja de reintentarlo con el mensaje Start request repeated too quickly, ha alcanzado el límite StartLimitBurst; corrige la causa y ejecuta sudo systemctl reset-failed demo-web.service.

Un cambio en la unit no se aplica. systemd trabaja con la copia cargada en memoria. Tras editar, ejecuta daemon-reload y reinicia el servicio. systemctl status avisa con Warning: The unit file ... changed on disk cuando falta la recarga.

DynamicUser=yes y el servicio no puede escribir. El usuario dinámico no tiene permisos en tu sistema de ficheros. Usa StateDirectory=nombre, que crea /var/lib/nombre con los permisos correctos, o ReadWritePaths= para una ruta concreta.

Conclusión

Has creado un servicio endurecido, un target que lo agrupa, una plantilla con varias instancias, un servicio activado por socket y un timer, y has visto la diferencia entre dependencias y orden de arranque. Como siguientes pasos, limita la CPU y la memoria de tus servicios con directivas como MemoryMax= y CPUQuota=, revisa la puntuación de seguridad de cada servicio con systemd-analyze security y migra tus tareas de cron a timers.