Ejecutar múltiples servicios en un contenedor Docker

Al preparar una infraestructura con Docker para producción, pueden dartse problemas con ciertas imágenes que son suficientemente complejas como para no ser fácilmente extendibles, pero que tampoco cumplen todos nuestros requerimientos.

En este caso, lo más normal es extender esa imagen hasta sentirse cómodo con ella. Pero, ¿cómo lo hacemos si necesitamos incluir otros servicios? Modificar la capa ENTRYPOINT puede ser un riesgo y un dolor de cabeza, y tendríamos que mantenernos al tanto de cada cambio en la imagen original. En lugar de ello, puede emplearse supervisord.

Aprendiendo con ejemplos

Un ejemplo que he encontrado es el de Redmine. Esta imagen sirve por defecto la aplicación en el puerto 3000 usando HTTP (de forma insegura). Necesitaremos un proxy inverso que encripte las conexiones y permita la comunicación por HTTPS.

Lo lógico sería crear otro contenedor y unirlo a la misma red para que todas las comunicaciones con el contenedor de Redmine pasen por el proxy, pero sigamos el juego y extendamos la imagen de Redmine para añadirle un servidor nginx:

FROM redmine:5

RUN apt-get update && \
    apt-get install -y \
    nginx \
    apt-get clean && \
    useradd nginx && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY conf/default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
EXPOSE 443

Donde default.conf contiene lo siguiente:

server {
    listen       80;
    listen      [::]:80;
    server_name  redmine.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen      443 ssl;
    listen      [::]:443 ssl;
    server_name redmine.example.com;

    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/certificate.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }

}

Hasta ahora bien, pero Nginx no se inicia en ningún momento. Para eso usaremos supervisord. Si inspeccionamos el Dockerfile de Redmine, podemos ver las siguientes líneas:

WORKDIR /usr/src/redmine
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["rails", "server", "-b", "0.0.0.0"]

Que son toda la información que necesitamos para configurar supervisord para que inicie Redmine.

En primer lugar habrá que instalarlo en nuestra imagen, de forma que nuestro Dockerfile quedaría:

FROM redmine:5

RUN apt-get update && \
    apt-get install -y \
    supervisor \
    nginx \
    apt-get clean && \
    useradd nginx&& \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY conf/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY conf/default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
EXPOSE 443

USER supervisor

ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Con esto, el servicio que iniciamos es supervisord, y es éste el que se encarga de levantar el resto de servicios que necesitemos. En nuestro caso Redmine y Nginx. Esto lo configuramos en el archivo supervisord.conf que estamos copiando a la imagen, que contendría lo siguiente:

[supervisord]
nodaemon=true

[program:nginx]
user=nginx
command=nginx

[program:redmine]
user=redmine
directory=/usr/src/redmine
command=/docker-entrypoint.sh rails server -b 127.0.0.1

La primera sección configura el propio supervisord para que corra en primer plano, lo que hará que el proceso del contenedor no termine sin más.

La segunda sección inicia Nginx empleando el archivo de configuración por defecto, que es el que hemos copiado al crear la imagen con el Dockerfile.

Con la última sección se inicia Redmine, cambiando al directorio /usr/src/redmine, tal y como se hacía en el Dockerfile original de Redmine. Además, se inicia Redmine concatenando las capas ENTRYPOINT y CMD del Dockerfile original. También restringimos las direcciones IP en las que escucha Redmine al bucle local, evitando conexiones directas desde fuera del contenedor.

Referencias