Docker: Configurando un contenedor como un servicio de Linux
Introducción
Existen múltiples formas de orquestar la gestión, iniciación, despliegue, etc. de los contenedores de Docker. Docker ofrece su propia herramienta Compose y modo Swarm. También hay herramientas de terceros para orquestar los contenedores, como Kubernetes, Rancher y Apache Mesos.
Para despliegues sencillos en un único host Linux, puede que no necesites ninguna de estas soluciones complejas. El demonio de Docker ofrece un sencillo mecanismo para arrancar, parar y consultar el estado de los contenedores desplegados en el sistema. En esta publicación veremos cómo desplegar contenedores como servicios de Linux utilizando Docker + systemd sin necesidad de utilizar herramientas de terceros.
El tutorial muestra concretamente cómo desplegar Portainer CE como un servicio de Linux.

Políticas de reinicio de Docker
Antes de profundizar en la integración con systemd, vale la pena mencionar que Docker tiene políticas de reinicio integradas que pueden gestionar los reinicios de contenedores automáticamente.
El flag --restart soporta varias políticas:
| Política | Descripción |
|---|---|
no | No reiniciar el contenedor automáticamente (por defecto) |
on-failure[:max-retries] | Reiniciar solo si el contenedor sale con un estado distinto de cero |
always | Siempre reiniciar el contenedor independientemente del estado de salida |
unless-stopped | Como always, pero no reinicia contenedores que fueron detenidos explícitamente |
Por ejemplo, para ejecutar Portainer con reinicios automáticos:
docker run -d --name portainer --restart unless-stopped \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:ltsEste enfoque es más simple que la integración con systemd y funciona bien para la mayoría de los casos. Sin embargo, la integración con systemd ofrece beneficios adicionales:
- Gestión de dependencias: Iniciar contenedores después de servicios específicos (red, bases de datos, etc.)
- Logging unificado: Los logs del contenedor aparecen en journald junto con los logs del sistema
- Gestión consistente: Usar comandos
systemctlfamiliares para todos los servicios - Orden de arranque: Control granular sobre la secuencia de inicio
Contenedor existente previamente
El método más sencillo para desplegar un contenedor como un servicio es crear un contenedor de Docker con un nombre concreto y mapear cada una de las operaciones de Docker (start y stop) con comandos de servicio de systemd.
docker run -d --name portainer \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:ltsUna vez creado el contenedor, podemos arrancarlo, pararlo y reiniciarlo empleando comandos estándar de Docker indicando el nombre del contenedor (docker stop portainer, docker start portainer, docker restart portainer).
El siguiente paso es crear un fichero systemd unit de configuración con la descripción del servicio en el directorio /etc/systemd/system/.
Para este ejemplo, crearemos un fichero con el nombre portainer.service con el siguiente contenido:
[Unit]
Description=Portainer container
After=docker.service
Wants=network-online.target docker.socket
Requires=docker.socket
[Service]
TimeoutStartSec=0
Restart=always
ExecStart=/usr/bin/docker start -a portainer
ExecStop=/usr/bin/docker stop -t 10 portainer
[Install]
WantedBy=multi-user.targetEl fichero unit define un nuevo servicio y mapea los comandos docker start y docker stop a las secuencias start y stop de systemd.
Configuración explicada:
- After=docker.service: Asegura que este servicio se inicie solo después de que Docker esté listo
- Requires=docker.socket: Crea una dependencia fuerte hacia el socket de Docker
- Wants=network-online.target: Espera a que la red esté disponible (dependencia suave)
- TimeoutStartSec=0: Deshabilita el timeout de inicio (útil si la imagen necesita descargarse)
- Restart=always: Reinicia automáticamente el contenedor si se detiene
Después de crear el fichero unit, recarga systemd para que detecte el nuevo servicio:
systemctl daemon-reloadAhora ya podremos arrancar y parar el servicio empleando los comandos correspondientes:
systemctl start portainer
systemctl stop portainerPara comprobar el estado del servicio:
systemctl status portainerPara habilitar el servicio para que se arranque automáticamente al iniciar el sistema:
systemctl enable portainer.serviceCrear contenedor (si aplica) al iniciar el servicio
Podemos mejorar el fichero unit anterior para que cree el contenedor si este no existe.
Este enfoque permite omitir el paso de creación manual del contenedor (docker run…) y desplegar contenedores simplemente creando el fichero unit con el descriptor del servicio.
[Unit]
Description=Portainer container
After=docker.service
Wants=network-online.target docker.socket
Requires=docker.socket
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull portainer/portainer-ce:lts
ExecStart=/usr/bin/docker run --rm --name %n \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:lts
ExecStop=/usr/bin/docker stop -t 10 %n
[Install]
WantedBy=multi-user.targetEste fichero unit mejorado utiliza varias buenas prácticas:
- Variable %n: Se expande al nombre del servicio (e.g.,
portainer.service), haciendo el fichero unit más portable - Prefijo guión (-): El
-antes de los comandosExecStartPresignifica "no fallar si este comando falla". Esto es útil porque el contenedor podría no existir en la primera ejecución - docker pull: Asegura que siempre ejecutes la última versión de la imagen
- Flag --rm: Elimina automáticamente el contenedor cuando se detiene, asegurando un estado limpio al reiniciar
- Modo foreground: A diferencia del ejemplo anterior, esto ejecuta
docker rundirectamente (no en modo detached), permitiendo a systemd monitorizar correctamente el proceso
Alternativa: Creación condicional del contenedor
Si prefieres mantener un contenedor existente en lugar de recrearlo cada vez, usa este enfoque:
[Unit]
Description=Portainer container
After=docker.service
Wants=network-online.target docker.socket
Requires=docker.socket
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=/bin/bash -c "/usr/bin/docker container inspect portainer 2> /dev/null || /usr/bin/docker run -d --name portainer -p 9443:9443 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:lts"
ExecStart=/usr/bin/docker start -a portainer
ExecStop=/usr/bin/docker stop -t 10 portainer
[Install]
WantedBy=multi-user.targetLos ficheros unit tienen determinadas limitaciones cuando se configuran comandos de ejecución en las definiciones de ExecStart, ExecStop, ExecStartPre.
Para evitar estas limitaciones, encapsulamos el comando dentro de un comando /bin/bash -c que nos permite más flexibilidad.
El primer comando (docker container inspect) comprueba si existe un contenedor con el mismo nombre.
Si lanza un error (el contenedor no existe), se ejecuta el siguiente comando para crearlo.
Es importante ejecutar el comando docker run con el argumento -d (detached) cuando usas este enfoque.
De no hacerlo, el comando no devolverá el control y el siguiente paso (ExecStart) no se ejecutará, impidiendo que systemd se active.
Dependiendo de si el contenedor existe, la secuencia de inicio se comporta diferente:
- El contenedor ya existe:
- ExecStartPre no devuelve error en el primer comando (
docker container inspect…), por lo que no ejecutadocker run… - ExecStart se ejecuta normalmente y el proceso se engancha al contenedor
- ExecStartPre no devuelve error en el primer comando (
- El contenedor no existe:
- ExecStartPre devuelve un error en el primer comando, ejecutando el segundo para crear y arrancar el contenedor en modo detached
- ExecStart ejecuta (
docker start) que se engancha al contenedor ya en ejecución
Ver logs del contenedor
Un beneficio de ejecutar contenedores a través de systemd es el logging unificado.
La salida del contenedor es capturada por journald, y puedes ver los logs usando el comando estándar journalctl:
# Ver todos los logs del servicio
journalctl -u portainer.service
# Seguir los logs en tiempo real
journalctl -u portainer.service -f
# Ver logs desde el último arranque
journalctl -u portainer.service -bAlternativa con Podman
Si buscas una integración más estrecha con systemd, considera Podman como alternativa a Docker. Podman es un motor de contenedores sin demonio que se integra nativamente con systemd a través de una característica llamada Quadlet.
Con Quadlet, creas ficheros declarativos simples (.container, .pod, .network) en lugar de escribir ficheros unit de systemd manualmente.
Colócalos en /etc/containers/systemd/ para contenedores root o en ~/.config/containers/systemd/ para contenedores sin privilegios.
Ejemplo de fichero Quadlet para Portainer:
[Container]
ContainerName=portainer
Image=docker.io/portainer/portainer-ce:lts
PublishPort=9443:9443
Volume=/run/podman/podman.sock:/var/run/docker.sock
Volume=portainer_data:/data
[Service]
Restart=always
[Install]
WantedBy=default.targetDespués de crear este fichero, ejecuta systemctl daemon-reload y el contenedor será gestionado como un servicio nativo de systemd.
Las ventajas de Podman para la integración con systemd incluyen:
- Arquitectura sin demonio: Sin punto único de fallo como dockerd
- Contenedores sin root: Ejecutar contenedores sin privilegios de root
- Integración nativa con systemd: Quadlet genera ficheros unit optimizados automáticamente
- Soporte de auto-actualización: Descarga y reinicia automáticamente cuando hay nuevas imágenes disponibles
Conclusión
En este tutorial, he mostrado varias formas de desplegar contenedores Docker como servicios de Linux systemd:
- Políticas de reinicio de Docker: El enfoque más simple para gestión básica de reinicios
- Ficheros unit de systemd: Para gestión de dependencias, logging unificado y orden de arranque
- Podman con Quadlet: La solución más integrada para gestión de contenedores basada en systemd
El enfoque Docker + systemd funciona bien para:
- Despliegues en un único host que no necesitan orquestación completa
- Servidores donde quieres que los contenedores se gestionen junto a otros servicios systemd
- Entornos con dependencias de servicios complejas
- Equipos familiarizados con systemd que quieren gestión consistente en todos los servicios
Para despliegues en producción a escala, considera Kubernetes o Docker Swarm. Para escenarios más simples en un único host, los enfoques descritos en esta publicación proporcionan una alternativa ligera que aprovecha tu conocimiento existente de Linux.
