Caso real de despliegue con Docker Compose

Hago éste post con muchísima ilusión por tener la sensación de haber hecho un muy buen trabajo y haber asentado los conocimientos adquiridos en el curso de DevOPs que hice ya hace unas 3 semanas. Ya lo he hecho personalmente y ahora lo hago públicamente, pero le doy las infinitas gracias a Sens Solutions por confiar en mi y por la gran oportunidad de hacer un trabajo de éste tipo sabiendo que partía de unos conocimientos muy básicos! También a Alberto García por la formación ofrecida y el cuál no he tenido que contactar para resolver dudas (de momento).

Así que mi aportación de retorno es explicar como lo he hecho y que pueda servir a otras personas que como yo se están adentrando a éste mundo de Docker.

Para éste post recomiendo antes de todo revisar éstos dos posts que hice hace ya unas semanas: Comandos básicos de Docker y Introducción al trabajo con Dockerfile.

Con Dockerfile tenemos una receta para desplegar un contenedor (o container). Con docker-compose tenemos pues una receta para desplegar un conjunto de contenedores que interactuarán entre ellos. El caso (real) que voy a desarrollar en éste post considero que es muy didáctico porqué desplegaremos contenedores sin usar Dockerfile y otros los vamos a desplegar usando Dockerfile, una vez desplegados los vamos a vincular entre ellos. Prometo un post extenso y completo :)

El entorno que necesitamos desplegar consiste en:
– 1 contenedor con mariadb (dockerhub)
– 1 contenedor con una aplicación django, backend (Dockerfile)
– 1 contenedor con una aplicación vuejs, frontend (Dockerfile)
– 1 contenedor con nginx para exponer el backend y el frontend (dockerhub)
– 1 contenedor con adminer para tener una administración web para la base de datos mariadb (dockerhub)
– 3 contenedores para construir un cluster de rabbitmq (dockerhub)
– 1 contenedor con logstash (Dockerfile)

Se ha trabajado en todo momento pensando en que el despliegue haga uso de contenedores “slim”, basados en alpine linux que es un sistema operativo muy liviano a nivel de consumo de disco (~200-300Mb). Al revés de otros posts que he ido viendo sobre el tema, empiezo la casa por el tejado y pongo el fichero docker-compose.yml con la receta aquí al principio e iré desgranando debajo cada uno de los items.

version: '3.8'

services:
  db:
    container_name: db
    #image: mariadb:10.5.6
    image: interlegis/mariadb-slim
    restart: always
    env_file:
      - project.env
    volumes:
      - mysql_data:/var/lib/mysql

  api:
     container_name: api
     build: backend/
     command: gunicorn --config gunicorn_config.py sensweb.wsgi:application
     hostname: api
     ports:
      - 7000:7000
     depends_on:
       - db
     env_file:
       - project.env
     volumes:
        - static:/app/static

  web:
      container_name: vuejs
      build: frontend/
      #command: http-server dist
      ports:
        - 7001:7001
      #depends_on:
      #  - db
      #  - web        

  nginx:
    container_name: nginx
    image: nginx:mainline-alpine
    restart: always
    ports:
      - 80:80
      - 81:81  
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - static:/app/static
    depends_on:
      - api
      - web

  adminer:
    container_name: adminer
    image: adminer
    restart: always
    ports:
      - 7080:8080

  # https://hub.docker.com/_/rabbitmq/
  # https://medium.com/@kailashyogeshwar/rabbitmq-cluster-using-docker-compose-7397ea378d73
  rabbit1:
    image: lucifer8591/rabbitmq-server:3.7.17
    hostname: rabbit1
    ports:
      - "5672:5672"
      - "15672:15672"
    env_file:
      - project.env
    #volumes:
    #  - ./rabbitmq/etc:/rabbitmq/rabbitmq_server-3.7.17/etc/rabbitmq

  rabbit2:
    image: lucifer8591/rabbitmq-server:3.7.17
    hostname: rabbit2
    links:
      - rabbit1
    environment:
      - CLUSTERED=true
      - CLUSTER_WITH=rabbit1
      - RAM_NODE=true
      #- RABBITMQ_NODENAME=rabbit@rabbit2
    ports:
      - "5673:5672"
      - "15673:15672"

  rabbit3:
      image: lucifer8591/rabbitmq-server:3.7.17
      hostname: rabbit3
      links:
        - rabbit1
        - rabbit2
      environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit1
        #- RABBITMQ_NODENAME=rabbit@rabbit3
      ports:
        - "5674:5672"

  logstash:
    #image: docker.elastic.co/logstash/logstash:7.10.0
    build: logstash/
    container_name: logstash

volumes:
  mysql_data:
  static:

Empezamos pues analizando cada uno de los contenedores que desplegamos en el fichero docker-compose.yml

DB (mariadb)

Éste contenedor está usando la imagen de dockerhum interlegis/mariadb-slim

La opción restart: always indica que éste contenedor si se reinicia el servidor Docker la máquina siempre esté encendida

La opción env_file sirve para definir unas variables de entorno para nuestro contenedor. He creado un fichero project.env con todas las variables de entorno necesarias para el despliegue de todos los contenedores y en el caso de las variables necesarias para mariadb serían:

# MySQL
MYSQL_ROOT_PASSWORD=*****
MYSQL_DATABASE=senscloud
MYSQL_USER=sensweb
MYSQL_PASSWORD=*****

Y finalmente definimos un volumen llamado mysql_data donde almacenaremos el contenido de /var/lib/mysql, con ésto lo que estamos haciendo es persistir en el host Docker los datos del contenedor, de ésta forma si volvemos a crear un contenedor nuevo, lo actualizamos o lo que sea, los datos de las bases de datos van a quedar fuera del contenedor en si.

api (backend)

Aquí empezamos con el juego y la diversión, para éste contenedor vamos a usar un fichero Dockerfile para desplegar el contenedor a nuestro gusto. Si nos fijamos, en lugar de usar la opción image: ahora usamos build: backend/. Con ésto le estamos diciendo a docker-compose que vaya a buscar un fichero Dockerfile que se encuentra dentro del directorio backend (docker-compose.yml se encuentra en ., el directorio actual y backend es un directorio que está en el directorio actual).
El Dockerfile nos va a crear una imagen en nuestro host de Docker, en lugar de ir a dockerhub, vamos a usar ésta máquina que hemos creado y se encuentra en local. Podríamos subir ésta imagen en dockerhub, pero esto ya es un post para otro día :)

El command: es el comando que vamos a ejecutar cada vez que se levante el contenedor. En éste caso: gunicorn –config gunicorn_config.py sensweb.wsgi:application

ports: indica que exponemos el puerto 7000 interno al 7000 externo en nuestro servidor Docker (el primero es el externo y el segundo el interno)

depends_on: no va a levantar éste contenedor si antes no está levantado en éste caso el contenedor db

env_file: ya lo he explicado y el contenido correspondiente para éste contenedor en el fichero proyect.env sería

# Django
DATABASE_URL=mysql://sensweb:****@db/senscloud
SECRET_KEY="************************************"
DJANGO_MANAGEPY_MIGRATE="on"

y finalmente volumes: lo vamos a usar para exponer directamente el directorio static de la aplicación django al contenedor nginx (lo vamos a ver mas adelante).

Si vemos el fichero Dockerfile del contenedor de backend tenemos ésto

FROM python:3.8-slim
RUN mkdir /app
WORKDIR /app

RUN set -ex \
    && RUN_DEPS=" \
    libpcre3 \
    mime-support \
    mariadb-client \
    libmariadb-dev \
    " \
    && seq 1 8 | xargs -I{} mkdir -p /usr/share/man/man{} \
    && apt-get update && apt-get install -y --no-install-recommends $RUN_DEPS \
    && rm -rf /var/lib/apt/lists/*

ADD requirements.txt /app
RUN pip install --upgrade pip

RUN set -ex \
    && BUILD_DEPS=" \
    build-essential \
    libpcre3-dev \
    libpq-dev \
    " \
    && apt-get update && apt-get install -y --no-install-recommends $BUILD_DEPS \
    && pip install --no-cache-dir -r /app/requirements.txt \
    \
    && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $BUILD_DEPS \
    && rm -rf /var/lib/apt/lists/*

RUN pip3 install gunicorn
ADD . /app
EXPOSE 7000
ENTRYPOINT ["/app/entrypoint.sh"]

Se basa en un contendor python, versión 3.8 slim. A él le instalamos las dependencias necesarias tanto del sistema como de python (requirements.txt). Para exponer la aplicación vamos a usar gunicorn (en lugar de uwsgi). Me estuve peleando con la instalación de ambos directamente en el sistema pero es mucho mas fácil instalar las dependencias de python y ejecutar gunicorn directamente desde python, de ésta forma ya no hay problemas con las variables de entorno que hacerlo de la otra forma era para cortarte las venas!

Finalmente ejecutamos el script entrypoint.sh para ejecutar un conjunto de comandos a ejecutar cuando la base de datos esté disponible

backend# cat entrypoint.sh 
#!/bin/sh
# created on 2020-11-08 by Laura Mora
set -e

echo "Check DB!"
while [ `python test_db.py` != "OK" ]; do echo "Wait ..."; sleep 2; done
echo "DB is UP! Continuing!"

# Make migrations and migrate the database.
echo "Making migrations and migrating the database. "
python manage.py makemigrations --noinput
python manage.py migrate --noinput
python manage.py collectstatic --noinput 
#chown www-data:www-data * -R

exec "$@"



backend# cat test_db.py 
#!/usr/bin/python

import pymysql, os

try:
    conn = pymysql.connect(
       host='db',
       user=os.environ.get("MYSQL_USER"),
       password =os.environ.get("MYSQL_PASSWORD"),
       db=os.environ.get("MYSQL_DATABASE"),
       )
    c = conn.cursor()
    print("OK")
except Exception:
    print("KO")

El contenido del directorio backend es éste:

root@docker-master:~/marck/senswebapp.rabbitMQ_cluster/backend# ls
Dockerfile     gunicorn_config.py  manage.py   README.md             requirements.txt  sens_units  sensweb
entrypoint.sh  __init__.py         old_delete  requirements-dev.txt  sens_data         sens_users  test_db.py

web (frontend)

El frontend es similar al backend, otro Dockerfile dentro del directorio frontend, no usamos command: y exponemos el puerto 7001. Tampoco definimos que depende ni de db ni de web, ya que técnicamente no requiere ni una y otra y básicamente funciona haciendo peticiones a la api de backend.

El contenido del Dockerfile es

FROM node:latest as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm update && npm install
COPY ./ .
RUN npm run build

FROM nginx:mainline-alpine as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 7001
CMD ["nginx", "-g", "daemon off;"]

Y creo que no hace falta explicar mucho mas :)

nginx

Aquí el chulo chulísimo, no veas para hacer encajar todo ésto con el backend y el frontend!!! te ahorro el sufrimiento y aquí te dejo mi “receta” :)

Backend: Ya te habrás encontrado que publicar una app con django algunas cosas como el static son un quebradero de cabeza, tal como se ha visto antes he puesto el static del backend como un volumen y ahí se almacenan todos los ficheros “static” de django (el secretito es ejecutar “python manage.py collectstatic –noinput” en el entrypoint.sh del Dockerfile del contenedor de backend). En éste contenedor en el docker-compose.yml añadimos también otro volumen en el que le pasamos el fichero de configuración que tenemos almacenado en el directorio nginx que cuelga del directorio donde tenemos el docker-compose.yml

# cat nginx/nginx.conf 
upstream backend {
    server api:7000;
}

upstream frontend {
    server vuejs:7001;
}

server {
    listen 81;
    listen [::]:81;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
    	alias /app/static/;
    }

}

server {
    listen 80;
    listen [::]:80;

    location / {
        proxy_pass http://frontend;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}
# create here 443 + certbot configuration

Queda pendiente hacer la configuración para exponer nginx bajo https y con el certificado de letsencrypt. Antes tengo que terminar de ver la parte de network de docker que lo dejaré para un post en los próximos días en otro despliegue que tengo pendiente con mailcow, onlyoffice y jitsi.

De la configuración del docker-compose.yml para éste contenedor no detallo nada mas porqué ya lo he ido explicando :) Sólo destacar que en el fichero de configuración de nginx me refiero a las máquinas por su nombre. Docker internamente lleva un servidor de DNS para resolver los hosts con su ip interna. Si usamos el hostname que le definimos al momento de crear el contenedor con la opción container_name: podremos escalar un contenedor a varios contenedores (pendiente para otro post!).

adminer

Éste contenedor lo desplegamos usando una imagen de dockerhub que se llama adminer (image: adminer). Si ya se ha ido entendiendo el resto no requiere mas explicación.

cluster rabbitMQ

Una de las ventajas de usar Docker es que puedes desplegar servicios sin la necesidad de liarte con la instalación ni saber como funciona la aplicación. En mi caso he tirado de google y he terminado en éste post con el trozo de docker-compose.yml que necesito para desplegar el cluster. En cada imagen en dockerhub se detallan las variables (environment) que se pueden pasar a un contenedor y en el caso de la imagen de rabbitMQ que estamos usando trae variables para montar un cluster de rabbitMQ.

Así que pego el contenido de project.env referente a rabbitMQ

# RabbitMQ
RABBITMQ_DEFAULT_VHOST=sens
RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER:-admin}
RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS:-admin}
RABBITMQ_NODENAME=rabbit@rabbit1

Y para conectar rabbit2 y rabbit3 a rabbit1 para construir el cluster (links y environment a pelo):

  rabbit2:
    image: lucifer8591/rabbitmq-server:3.7.17
    hostname: rabbit2
    links:
      - rabbit1
    environment:
      - CLUSTERED=true
      - CLUSTER_WITH=rabbit1
      - RAM_NODE=true
    ports:
      - "5673:5672"
      - "15673:15672"

  rabbit3:
      image: lucifer8591/rabbitmq-server:3.7.17
      hostname: rabbit3
      links:
        - rabbit1
        - rabbit2
      environment:
        - CLUSTERED=true
        - CLUSTER_WITH=rabbit1
      ports:
        - "5674:5672"

Logstash

Y finalmente logstash que lo desplegamos usando un Dockerfile

# cat logstash/Dockerfile 
# https://www.elastic.co/guide/en/logstash/current/docker-config.html
FROM docker.elastic.co/logstash/logstash:7.10.0
RUN rm -f /usr/share/logstash/pipeline/logstash.conf
ADD pipeline/ /usr/share/logstash/pipeline/
ADD config/ /usr/share/logstash/config/

Con mas detallitos en el link que pongo en el comentario.

Construir el docker-compose.yml

Ya con el docker-compose.yml construido y comprobado con Fromlatest.io, para ejecutarlo vamos a usar docker-compose up –build para comprobar que todo levanta correctamente (cuando vayas a construir tu docker-compose.yml te vas a hartar de éste comando hehe). Cuando vayas a subirlo en producción, es posible que interese usar la opción -d detach para que te quede libre la consola que estás usando, sino en su defecto te va a escupir los logs de los contenedores en pantalla y cuando pares el comando con control+c se van a parar las contenedores que acababas de construir con el docker-compose.yml.

Otros comandos que te puedan interesar para hacer éste trabajo que he explicado en éste post son:

Listar todos los contenedores:

# docker ps -a
CONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS                     PORTS                NAMES
65d17a275e58        nginx:mainline-alpine           "/docker-entrypoint.…"   7 days ago          Exited (0) 7 days ago                           nginx
917e2172d96d        senswebappdevelop_api           "/app/entrypoint.sh …"   7 days ago          Exited (0) 7 days ago                           api
f12927ef3652        senswebappdevelop_web           "/docker-entrypoint.…"   7 days ago          Exited (0) 7 days ago                           vuejs
169661df5878        adminer                         "entrypoint.sh docke…"   7 days ago          Exited (0) 7 days ago                           adminer
36287b10b939        senswebappdevelop_logstash      "/usr/local/bin/dock…"   7 days ago          Exited (137) 7 days ago                         logstash
fe17f29395a7        interlegis/mariadb-slim         "docker-entrypoint.s…"   7 days ago          Exited (0) 7 days ago                           db
7bef7e8038fd        portainer/agent:latest          "./agent"                8 days ago          Up 8 days   

Borrar los contenedores creados:

# docker rm nginx api vuejs adminer logstash db

usar la opción -f para forzar el previo parado de los contenedores antes de borrarlos

Listar las imagenes creadas:

# docker images
REPOSITORY                            TAG                 IMAGE ID            CREATED             SIZE
senswebappdevelop_web                 latest              13a291125356        7 days ago          31.9MB
                                              924c4ae4caba        7 days ago          1.21GB
senswebappdevelop_logstash            latest              132219cef10e        7 days ago          843MB
node                                  latest              2d840844f8e7        9 days ago          935MB
                                              b7658893056b        12 days ago         1.26GB
senswebappdevelop_api                 latest              7cf3b22f3713        2 weeks ago         323MB
docker.elastic.co/logstash/logstash   7.10.0              bc71baf6997e        3 weeks ago         843MB
adminer                               latest              476c78d02a95        4 weeks ago         89.9MB
nginx                                 mainline-alpine     e5dcd7aa4b5e        4 weeks ago         21.8MB
nginx                                 latest              c39a868aad02        4 weeks ago         133MB
python                                3.8-slim            0f59d947500d        4 weeks ago         113MB
python                                3-alpine            77a605933afb        4 weeks ago         44.3MB
node                                  lts-alpine          9db54a688554        5 weeks ago         117MB
mariadb                               10.5.6              c4655f911514        5 weeks ago         407MB
lucifer8591/rabbitmq-server           3.7.17              92ac7cb6573f        15 months ago       245MB
interlegis/mariadb-slim               latest              0fa119a420c3        4 years ago         390MB

Eliminar las imagenes creadas

# docker rmi senswebappdevelop_web senswebappdevelop_logstash senswebappdevelop_logstash senswebappdevelop_api adminer nginx python mariadb

Entrar en un contenedor cuando se esté ejecutando

# docker exec -ti senswebapprabbitmq_cluster_rabbit1_1 /bin/bash

Y creo que no me dejo nada mas importante. Espero que éste post te haya sido de ayuda.

Muchas gracias de tener la paciencia y dedicación de llegar hasta aquí :)

PS: si no tienes docker-compose instalado, sigue las instrucciones de éste link para instalarlo.

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.