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. Incluso Docker ofrece su propia herramienta y modo de ejecución. También hay herramientas de terceros para orquestar los contenedores, entre ellas encontramos: Kubernetes, Rancher, Apache Mesos, etc.
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 como desplegar contenedores como servicios de Linux utilizando Docker + systemd sin necesidad de utilizar herramientas de terceros.
El tutorial muestra concretamente como desplegar Portainer como un servicio de Linux.
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.
1docker run -d --name portainer --privileged -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/docker/data/portainer:/data portainer/portainer
Una vez creado un contenedor empleando el comando anterior, podemos arrancar, parar y reiniciar el contenedor empleando comandos estándar de docker indicando el nombre que le hemos proporcionado al 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/
. Siguiendo la metodología para desplegar un contenedor con una imagen estándar de Portainer, crearemos un fichero con el nombre portainer.service
con el siguiente contenido.
1[Unit]
2Description=Portainer container
3After=docker.service
4Wants=network-online.target docker.socket
5Requires=docker.socket
6
7[Service]
8Restart=always
9ExecStart=/usr/bin/docker start -a portainer
10ExecStop=/usr/bin/docker stop -t 10 portainer
11
12[Install]
13WantedBy=multi-user.target
El fichero unit define un nuevo servicio y mapea los comandos docker start
and docker stop
a las secuencias service start y stop de systemd.
El fichero describe también dependencias hacia network-online target y docker socket. Es decir, si docker socket no se inicia, este servicio tampoco lo hará. Además se define una dependencia hacia docker.service de modo que este servicio únicamente se iniciará después de que el anterior complete su secuencia de inicio.
Ahora ya podremos arrancar y parar el servicio empleando los comandos correspondientes:
1systemctl start portainer
2systemctl stop portainer
Además, también podremos instalar el servicio para que se arranque al iniciar el sistema empleando el siguiente comando:
1systemctl enable portainer.service
Crear contenedor (si aplica) al iniciar el servicio
Podemos mejorar el fichero unit anterior para que cree el contenedor si este no existe, esto nos permitirá ahorrarnos el primer paso de creación manual del contenedor (docker run…
) y podrá utilizarse para desplegar contenedores simplemente creando el fichero unit con el descriptor del servicio systemd.
1[Unit]
2Description=Portainer container
3After=docker.service
4Wants=network-online.target docker.socket
5Requires=docker.socket
6
7[Service]
8Restart=always
9ExecStartPre=/bin/bash -c "/usr/bin/docker container inspect portainer 2> /dev/null || /usr/bin/docker run -d --name portainer --privileged -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /opt/docker/data/portainer:/data portainer/portainer"
10ExecStart=/usr/bin/docker start -a portainer
11ExecStop=/usr/bin/docker stop -t 10 portainer
12
13[Install]
14WantedBy=multi-user.target
Simplemente hemos añadido una línea adicional con una instrucción ExecStartPre
. Los ficheros unit tienen determinadas limitaciones cuando se configuran comandos de ejecución en las definiciones de ExecStart, ExtecStop, ExecStartPre…. Para evitar estas limitaciones, encapsulamos el comando que realmente queremos ejecutar dentro de un comando /bin/bash -c
que nos permitirá tener un poco más de flexibilidad.
El primero de los comandos (docker container inspect
) comprueba la existencia de un contenedor con el mismo nombre en la misma máquina. Si este comando lanza un error (el contenedor no existe), la siguiente instrucción se ejecutará creando así el contenedor. Este último comando es el mismo que hemos empleado en el ejemplo anterior para crear el contenedor de forma manual.
Es importante destacar que el comandodocker run
debe de ejecutarse con el argumento -d
(detached), de no hacerlo así, el comando no devolverá el control a systemd y por tanto el siguiente paso (ExecStart) no se ejecutará, por lo que el servicio systemd nunca se activará.
Dependiendo de la existencia previa o no del contenedor, la secuencia de inicio del servicio se comportará de un modo u otro:
- Ya existe un contenedor de nombre portainer:
- ExecStartPre no devolverá ningún error en la ejecución del primer comando, por (
docker container inspect…
), por lo que no ejecutará el siguiente (docker run…
). - ExecStart se ejecutará normalmente y el proceso se enganchará al contenedor.
- ExecStartPre no devolverá ningún error en la ejecución del primer comando, por (
- No existe un contenedor de nombre portainer:
- ExecStartPre devolverá un error en la ejecución del primer comando (
docker container inspect…
), con lo que ejecutará el segundo de ellos creando y arrancando el contenedor docker en modo detached devolviendo el control a systemctl. - ExecStart ejecutará igualmente (
docker start
) que no tendrá ningún efecto puesto que el contenedor ya está arrancado, pero se enganchará al contenedor.
- ExecStartPre devolverá un error en la ejecución del primer comando (
Conclusión
En este tutorial hemos mostrado una forma sencilla de desplegar contenedores Docker como servicios systemd de Linux. Esto es una alternativa “barata” a Kubernetes o cualquier otra herramienta de orquestación aunque no tiene la misma fiabilidad y sólo debería de emplearse en casos muy específicos.