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.9MB924c4ae4caba 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.