--- weight: 4 title: "Apuntes de Docker" date: 2021-01-23T16:01:50+0100 draft: false summary: "Apuntes de docker" categories: - notes tags: - docker - traefik --- {{< image src="/images/docker_logo_wide.jpg" >}} {{< admonition type=warning title="Work in progress" open=true >}} Estos apuntes no están completos, (ni de lejos) {{< /admonition >}} ## Conceptos ### El software El software de Docker implementa el estándar OCI (_Open Container Initiative_) tan fielmente como les es posible. La OCI especifica: * Imágenes * _Runtime containers_ runc : Es la implementación para _Docker_ de la especificación de _runtime container_ de la OCI. La misión de _runc_ es crear contenedores, puede funcionar por si solo via CLI (es un binario) pero funciona a muy bajo nivel. containerd : Es un demonio. Se encarga del ciclo de vida de los contenedores: arrancarlos, pararlos, pausarlos, borrarlos. Se encarga también de otras cosas como _pulls_, _volumes_ y _networks_. Esta funcionalidad adicional es modular y opcional y se introdujo para facilitar el uso de _containerd_ en otros proyectos como por ejemplo _Kubernetes_. shim : _shim_ se encarga de dejar conectados los containers con _containerd_ una vez que _runc_ termina su misión y muere. ### _images_ Puede pensarse que una imagen es como un contenedor parado. De hecho puedes parar un contenedor y construir una imagen a partir de el. Se dice que las imágenes son objetos de _build-time_ y los contenedores son objetos de _run-time_ Las imágenes se pueden almacenar centralizadas en los _image registries_. Por ejemplo _dockerhub_ es un _image registry_, seguramente el más popular, pero tu podrías tener tu propio _image registry_. {{< admonition type=warning title="Seguridad e Imágenes" state=open >}} Mucho ojo con esto, por que en el [Docker Store](https://hub.docker.com/) nos podemos encontrar todo tipo de imágenes, al fin y al cabo cualquiera puede publicar sus imagenes. No debemos confiar automáticamente en cualquier imagen. Ten cuidado con tus fuentes. __¡No te fies de las etiquetas!__ Son solo eso, etiquetas. Por ejemplo _latest_ se supone que apunta a la última versión de un contenedor pero no hay ningún mecanismo que lo asegure, _alguien_ tiene que ocuparse de que la etiqueta apunte al contenedor correcto. {{< /admonition >}} Las __imágenes oficiales__ suelen residir en un repo de primer nivel dentro de _dockerhub_ y tendrán una url como esta, por ejemplo: ### _containers_ Como dijimos arriba, un _container_ es una instancia _runtime_ de una imagen. A partir de una imagen podemos crear varios _containers_. Un contenedor es muy parecido a una máquina virtual clásica (como las de _Virtual Box_ por ejemplo). La principal diferencia es que son mucho más ligeros y rápidos, ya que comparten _kernel_ con la máquina _host_. Además suelen basarse en imágenes lo más ligeras posible que contienen solo lo imprescindible para que el contenedor cumpla su función. ### _volumes_ ### _networks_ ## cheatsheet ### Imágenes | Comando | Efectos | |:--------------------|:---------------------------------------| | docker images | Lista las imágenes disponibles | | docker ps | Lista los container en ejecución | | docker ps -a | Lista todos los container | | docker build | construye imagen desde dockerfile | | docker history | Muestra la hist. es decir como se hizo | | docker inspect | Detalles de la imagen (p.ej. ip) | | docker images | Lista todas las imágenes | | docker images -a | lista tb las imágenes intermedias | | docker images -q | lista solo identificativos | | docker system prune | Borra todas las imágenes no usadas | | docker pull | descarga una imagen | | docker push | sube una imagen a la nube | | docker image rm | Borra imágenes, alias `docker rmi` | ### Contenedores | Comando | Efectos | |:------------------------|:--------------------------------------| | docker run | Ejecuta un contenedor | | docker run -d | detached | | docker run -name | para pasarlo por nombre | | docker container rename | para renombrar un contenedor | | docker ps | Para ver cont. ejecutandose | | docker ps -a | Para verlos todos | | docker stop | Para parar uno, `SIGTERM` + `SIGKILL` | | docker start | reiniciar un cont. | | docker restart | stop + start | | docker kill | `SIGKILL` | | docker pause | lo pone en pausa | | docker unpause | lo saca de la pausa | | docker cp | copiar ficheros a y desde cont. | | docker exec -it | ejecutar y abrir terminal | | docker top | ver estadísticas del cont. | | docker stats | para ver más stats. `--no-stream` | | docker --rm | borrar cont. al sali | | docker container prune | borrar todos los cont. parados | __Ejemplos__: - Lanzar un alpine y abrir un terminal contra el: ```bash docker run -it --rm alpine /bin/ash ``` - Abrir un terminal contra un contenedor que ya está corriendo: ```bash docker exec -it bash ``` ### Volumes | Comando | Efectos | |:------------------------|:--------------------------------------| | docker volume prune | Borrar todos los `volume` no usados | ### Dockerhub | Comando | Efectos | |:---------------------|:-------------------------| | docker commit | crea imagen desde cont. | | docker login | | | docker push | | | docker search ubuntu | busca imágenes en el hub | ### docker inspect ### ```shell docker inspect --format '{{ .NetworkSettings.IPAddress }}' CONT_ID ``` ** TODO ** Investigar mas el `format` ### docker build ### Como construir una imagen `docker build https://github.com/salvari/repo.git#rama:directorio` Tambien se le puede pasar una url o un directorio en nuestro pc. ## Dockerfile {{< admonition type=info title="Referencias" open=true >}} - [Dockerfile Best Practices with Examples](https://takacsmark.com/dockerfile-tutorial-by-example-dockerfile-best-practices-2018/#overview) {{< /admonition >}} Un _dockerfile_ es un fichero de texto que define una imagen de Docker. Con esto podemos crear nuestras imágenes a medida, ya sea para "dockerizar" una aplicación, para construir un entorno a medida, para implementar un servicio etc. etc. En el [dockerhub](https://hub.docker.com/) tenemos imágenes para hacer prácticamente cualquier cosa, pero en la práctica habrá muchas ocasiones donde queramos cambiar algún detalle o ajustar la imagen a nuestro gusto y necesidades (o simplemente aprender los detalles exactos de la imagen), para todo ello necesitamos saber como funcionan los _dockerfiles_. Un _dockerfile_ de ejemplo: ```dockerfile FROM python:3 WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [ "python", "./your-daemon-or-script.py" ] ``` Un _dockerfile_ funciona por capas (_layers_) que son los bloques de construcción de _docker_. La primera _layer_ siempre es `FROM image_name` que define en que imagen pre-construida se basará nuestra imagen. Podemos definir muchas cosas en un _dockerfile_ desde permisos de usuario hasta _scripts_ de arranque. Asi que: 1. Un _dockerfile_ es un fichero de texto que contien las instrucciones que se ejecutarán con `docker build` para construir una imagen Docker 2. Es un conjunto de instrucciones paso a paso 3. Se compone de un conjunto de instrucciones estándar (como `FROM`, `ADD`, etc. etc.) 4. Docker contruirá automáticamente una imagen a partir de esas instrucciones Nota: En _docker_ un contenedor es una imagen con una capa de lectura-escritura en la cima de muchas capas de solo-lectura. Esas capas inferiores se denominan "imágenes intermedias" y se generan cuando se ejecuta el _dockerfile_ durante la etapa de construcción de la imagen. Evidentemente para construir un _dockerfile_ no basta con conocer Docker. Si quiero construir una imagen para proveer un servicio de base de datos, necesito conocer como funciona ese servicio y el proceso de instalación del mismo para "dockerizarlo" `ADD` : Copia un fichero del host al contenedor `CMD` : el argumento que pasas por defecto `ENTRYPOINT` : el comando que se ejecuta por defecto al arrancar el contenedor `ENV` : permite declarar una variable de entorno en el contenedor `EXPOSE` : abre un puerto del contenedor `FROM` : indica la imagen base que utilizarás para construir tu imagen personalizada. Esta opción __es obligatoria__, y además __debe ser la primera instrucción__ del Dockerfile. `MAINTAINER` : es una valor opcional que te permite indicar quien es el que se encarga de mantener el Dockerfile `ONBUILD` : te permite indicar un comando que se ejecutará cuando tu imagen sea utilizada para crear otra imagen. `RUN` : ejecuta un comando y guarda el resultado como una nueva capa. `USER` : define el usuario por defecto del contenedor `VOLUME` : crea un volumen que es compartido por los diferentes contenedores o con el host `WORKDIR define el directorio de trabajo para el contenedor. ### Ejemplos de dockerfiles Primero ```dockerfile FROM python:3 WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [ "python", "./your-daemon-or-script.py" ] ``` #### nginx ``` FROM alpine:3.15 AS builder RUN apk add --update \ --no-cache \ pcre~=8.45 \ libxml2~=2.9 \ libxslt~=1.1 \ gcc~=10.3 \ make~=4.3 \ libc-dev~=0.7 \ pcre-dev~=8.45 \ zlib-dev~=1.2 \ libxml2-dev~=2.9 \ libxslt-dev~=1.1 && \ cd /tmp && \ wget -q https://github.com/nginx/nginx/archive/master.zip -O nginx.zip && \ unzip nginx.zip && \ cd nginx-master && \ ./auto/configure --prefix=/opt/nginx && \ make && \ make install && \ apk del gcc make libc-dev pcre-dev zlib-dev libxml2-dev libxslt-dev && \ rm -rf /var/cache/apk FROM alpine:3.15 ARG UID=${UID:-1000} ARG GID=${GID:-1000} RUN apk add --update \ --no-cache \ pcre~=8.45 \ libxml2~=2.9 \ libxslt~=1.1 \ tini~=0.19 \ shadow~=4.8 &&\ rm -rf /var/cache/apk && \ groupmod -g $GID www-data && \ adduser -u $UID -S www-data -G www-data && \ mkdir /html COPY --from=builder /opt /opt COPY nginx.conf /opt/nginx/conf/nginx.conf COPY entrypoint.sh / EXPOSE 8080 VOLUME /html RUN chown -R www-data:www-data /html && \ chown -R www-data:www-data /opt/nginx USER www-data ENTRYPOINT ["tini", "--"] CMD ["/bin/sh", "/entrypoint.sh"] ``` ## Networks ### Bridge ```bash docker network ls # Vemos las redes creadas docker run -dit --name alpine1 alpine ash docker run -dit --name alpine2 alpine ash docker container ls # Vemos los dos alpine docker network inspect bridge # Vemos que se han conectado al bridge docker attach alpine1 # nos conectamos al contenedor ping alpine2 # NO FUNCIONA por nombre ping 172.17.0.4 # FUNCIONA por ip # Para hacer un detach ordenado del alpine1 C-p C-q ``` Vamos a crear una red bridge propia (_user defined bridge_): ```bash docker network create --driver bridge alpine-net # Creamos la red docker network ls # Vemos redes existentes docker network inspect alpine-net docker run -dit --name alpine1 --network alpine-net alpine ash # Creamos tres alpines en la red bridge alpine-net docker run -dit --name alpine2 --network alpine-net alpine ash docker run -dit --name alpine3 --network alpine-net alpine ash docker run -dit --name alpine4 alpine ash # este alpine4 está en la bridge default docker network connect bridge alpine3 # alpine3 está conectado a dos redes: default y alpine-net docker container ls docker inspect alpine-net docker inspect bridge docker attach alpine3 # nos conectamos a alpine3 ping alpine1 # FUNCIONA, en redes definidas funciona # por nombre y por IP ping alpine4 # NO FUNCIONA alpine3 y alpine4 se conectan # por la default bridge que no resuelve nombres # detach C-p C-q ``` Si probamos desde `alpine1` podremos hacer ping a los `alpine2` y `alpine3`, por nombre o dirección ip, pero no podremos llegar a `alpine4` de inguna manera, ya que está en otra red. ---- __IMPORTANTE__ Un _docker-compose_ siempre crea una _user defined bridge_, aunque no se lo indiquemos. ---- ## _Volumes_ y _Bind Mounts_ ### Referencias * [Understanding Docker Volumes: Persisting a Grafana Configuration File](https://www.datamachines.io/blog/understanding-docker-volume-persisting-a-grafana-configuration-file) * [Stackoverflow: docker data volume vs mounted host directory](https://stackoverflow.com/questions/34357252/docker-data-volume-vs-mounted-host-directory) * [Docker Storage](https://docs.docker.com/storage/) Ojito con esto por que, para mi, son muy diferentes y no encontré ningún sitio que lo dejara claro desde el principio. Se crean con la misma opción `-v` o `--volume` en el comando pero __hay diferencias__. ### _Volumes_ En principio los contenedores son volátiles y eso está bien para ciertas cosas se supone que tienes que diseñarlos para que sean muy volátiles y que se puedan crear y matar fácilmente, pero en la práctica no. Lo normal es que necesites tener datos persistentes, o incluso datos compartidos entre varios contenedores. * Un _volume_ es un espacio de almacenamiento que crea _Docker_ internamente, le puedes dar nombre y _docker_ lo va a almacenar en algún lugar del disco duro del _host_ (en linux tipicamente `/var/lib/docker/volumes`) * Se pueden crear previamente dándoles un nombre y separarlos de la creación del contenedor: `docker volume create grafana-storage`. En _docker-compose_ esto supone declararlos como _external_: ```docker-compose volumes: grafana-storage: external: true ``` Eso hace que el _docker-compose file_ no sea independiente (no parece lo más elegante). * Si los declaramos en el fichero _docker-compose.yml_ se crearán en el caso de que no existan (__parece la mejor manera__) ```docker-compose volumes: - web_data:/usr/share/nginx/html:ro ``` O también: ```docker-compose volumes: web_data: name: ${VOLUME_ID} services: app: image: nginx:alpine ports: - 80:80 volumes: - web_data:/usr/share/nginx/html:ro ``` ### _Bind Mounts_ En este caso montamos un fichero o directory __del sistema de ficheros del host__ en el contenedor. ### Diferencias entre _volumes_ y _bind mounts_ * Los _bind mounts_ son más simples a la hora de hacer copias de seguridad (aunque en linux no veo grandes diferencias) * Los _volumes_ se pueden gestionar desde el CLI del _Docker_ * Los _volumes_ siempre funcionan, en cualquier S.O. así que tus _dockerfiles_ o tus _docker compose files_ serán multiplataforma. * Los _volumes_ se pueden compartir entre contenedores fácilmente. * El almacenamiento de los _volumes_ es teoricamente más facil de implementar en plataformas remotas (la nube) * Los _bind mounts_ pueden dar problemas con los permisos de usuarios (el usuario que se usa en el _container_ quizás no exista en el _host_) * Los _bind mounts_ son muy dependientes con el S.O. se suelen declarar en variables de entorno para intentar desacoplar los _dockerfiles_ y los _docker-compose files_ del S.O. que use cada uno, por que las rutas de ficheros evidentemente dependen del S.O. ### Renombrar un volumen (receta) ```bash #/bin/bash docker volume create --name $2 docker run --rm -it -v $1:/from -v $2:/to alpine ash -c "cd /from ; cp -av . /to" [ $? -eq 0 ] && docker volume rm $1 ``` ## _docker-compose_ * [doc oficial](https://docs.docker.com/compose/compose-file) * [edututorial](https://www.educative.io/blog/docker-compose-tutorial) Es el siguiente paso en abstracción. Con _docker-compose_ podemos definir y ejecutar aplicaciones compuestas de múltiples contenedores. La definición se especifica mediante un fichero _YAML_ que permite configurar las aplicaciones y también crearlas. Las principales ventajas de _docker-compose_ son: * Múltiples entornos aislados en un unico _host_ * Preservar los volúmenes de datos cuando se crean los contenedores * Se recrean únicamente los contenedores que cambian * Orquestar múltiples contenedores que trabajan juntos * Permite definir variable y mover orquestaciones entre entornos ### Flujo de trabajo 1. Definir los entornos de aplicación con un _dockerfile_ 1. Definir los servicios provistos mediante _docker-compose_. Esto permitirá ejecutarlos en un entorno aislado. 3. Lanzar los servicios ejecutando el _docker-compose_ ### _docker-compose_ fichero de configuración La estructura básica de un fichero de configuración para _docker-compose_ tiene esta pinta: ```dockercompose version: 'X' services: web: build: . ports: - "5000:5000" # host:container mejor siempre como string volumes: - .:/code redis: image: redis ``` Se puede ver más claro con un fichero real: ```docker-compose version: '3' services: web: # Path to dockerfile. # '.' represents the current directory in which # docker-compose.yml is present. build: . # Mapping of container port to host ports: - "5000:5000" # Mount volume volumes: - "/usercode/:/code" # Link database container to app container # for reachability. links: - "database:backenddb" database: # image to fetch from docker hub image: mysql/mysql-server:5.7 # Environment variables for startup script # container will use these variables # to start the container with these define variables. environment: - "MYSQL_ROOT_PASSWORD=root" - "MYSQL_USER=testuser" - "MYSQL_PASSWORD=admin123" - "MYSQL_DATABASE=backend" # Mount init.sql file to automatically run # and create tables for us. # everything in docker-entrypoint-initdb.d folder # is executed as soon as container is up nd running. volumes: - "/usercode/db/init.sql:/docker-entrypoint-initdb.d/init.sql" ``` `version: '3'` : Indica la versión del compose-file que estamos usando ([lista de versiones y manual de referencia]( https://docs.docker.com/compose/compose-file/)) `services` : Esta sección define los diferentes containers que queremos crear, en nuestro ejemplo solo habrá dos "web" y "database" `web` : Marca el comienzo de la definición del servicio "web", en este ejemplo sería un servicio implementado con _Flask_ `build` : Indica el _path_ al _dockerfile_ que define el servicio, en este caso es relativo y el "." significa que el _dockerfile_ está en el mismo directorio que el fichero _yaml_ del _compose_ `ports` : Mapea puertos desde el container a puertos del _host_ `volumes` : mapea sistemas de ficheros del host en el contenedor (igual que la opción `-v` en _docker_) `links` : Indica "enlaces" entre contenedores, para la _bridge network_ debemos indicar que servicios pueden acceder a otros `image` : alternativamente al _dockerfile_ podemos especificar que nuestro servicio se basa en una imagen pre-construida `enviroment` : Permite especificar una variable de entorno en el contenedor (igual que la opción `-e` en _docker_) ### Gestión de variables en _docker-compose_ Las variables pueden definirse en varios sitios, por orden de precedencia serían: 1. Compose file (en una sección _enviroment_) 2. Variables de entorno del sistema (en nuestro intérprete) 3. _Enviroment file_, es un fichero en el mismo directorio que el fichero de _compose_ con el nombre `.env` 4. En el _dockerfile_ 5. La variable no está definida La opción _Enviroment file_ es un fichero que se llama `.env`, podemos usar otro nombre cualquiera incluyendo la directiva `env_file` en el fichero _compose_. Por ejemplo: ```docker env_file: - ./secret-stuff.env ``` Se supone que __no__ vas a subir tus secretos a un repo en la nube. El fichero `.env` probablemente no deba estar incluido en _git_ en un entorno de producción. [Variables, documentación oficial](https://docs.docker.com/compose/environment-variables/) ### docker-compose comandos útiles Reconstruir un contenedor : Cuando cambiamos la definición de un servicio dentro del fichero `docker-compose.yml`, podemos reconstruir ese servicio con el comando: ```bash docker-compose up -d --no-deps --build ``` Tenemos una opción más moderna que sería el comando: ```bash docker-compose build --no-cache ``` O bien: ```bash docker-compose up -d --no-cache --build ``` ## Mosquitto + Influxdb + Graphana con _docker_ y _docker-compose_ [referencia](https://github.com/Nilhcem/home-monitoring-grafana) Creamos un directorio para nuestro proyecto que llamamos `homesensor` (por ejemplo) ```bash mkdir homesensor cd homesensor ``` ### Mosquitto Creamos un directorio para nuestro container de _Mosquitto_ ```bash mkdir 01_mosquitto cd 01_mosquitto ``` #### Configuración de _mosquitto_ Creamos un fichero de configuración de *Mosquitto* `mosquitto.conf` con el siguiente contenido: ```conf persistence true persistence_location /mosquitto/data/ log_dest file /mosquitto/log/mosquitto.log allow_anonymous false password_file /mosquitto/config/users ``` Con este fichero le indicamos a _Mosquitto_: * Qué queremos que tenga persistencia de datos. Así salvará su estado a disco cada 30 minutos (configurable), de lo contrario usaría exclusivamente la memoria. * le indicamos en que _path_ debe guardar los datos persistentes (`/mosquitto/data/`) * configuramos el fichero de log (`/mosquitto/log/mosquitto.log`) * prohibimos usuarios anónimos * establecemos el listado de usuarios con su password ---- __Nota__: Puedes echar un ojo al fichero de ejemplo de configuración de mosquitto arrancando un contenedor sin ninguna opción de mapeado de volúmenes. O consultando la documentación de _mosquitto_. Hay muchísimas más opciones de las que proponemos. ---- Tendremos que preparar también un fichero `users` de usuarios para establecer los usuarios y sus password. Podemos crear un fichero de usuarios y passwords en claro con el formato: ``` mqttuser:53cret0 ``` Y cifrarlo con el comando `mosquitto_passwd -U ` Si solo tenemos un usuario lo podemos crear con el comando `mosquitto_passwd -c ` ---- __Nota__: Nos adelantamos a los acontecimientos, pero si no tienes _mosquitto_ instalado en tu máquina puedes cifrar el fichero de usuarios con un contenedor de _mosquitto_ transitorio, que se encargue del cifrado: ```bash docker run --rm \ -v `pwd`/users:/mosquitto/config/users \ eclipse-mosquitto \ mosquitto_passwd -U /mosquitto/config/users ``` ---- #### _mosquitto_ en _docker_ desde linea de comandos Una vez creados estos dos ficheros podemos crear un contenedor _mosquitto_ con _ podemos hacerlo desde linea de comandos. ```bash docker run -it -p 1883:1883 \ -v `pwd`/mosquitto.conf:/mosquitto/config/mosquitto.conf \ -v `pwd`/users:/mosquitto/config/users eclipse-mosquitto ``` `run` : Ejecutará el contenedor, si la imagen especificada no existe la bajará del _dockerhub_ `-it` : La opción `i` ejecuta el contenedor en modo interactivo, la opción `t` abrira un terminal contra el contenedor. `-p 1883:1883` : mapea el puerto 1883 del contendor al puerto 1883 del host ``-v `pwd`/mosquitto.conf:/mosquitto/config/mosquitto.conf`` : mapea el fichero `mosquitto.conf` del directorio actual (`pwd`) en el volumen del contenedor: `/mosquitto/config/mosquitto.conf` ``-v `pwd`/users:/mosquitto/config/users`` : Igual que el anterior pero con el fichero `users` `eclipse-mosquitto` : La imagen que vamos a usar para crear nuestro contenedor, si no especificamos nada se usará `:latest`, es decir la imagen más reciente. Podemos probar esta configuración con los clientes de mqtt (tenemos que tener instalado el paquete `mosquitto-clients` en nuestra máquina) En un terminal lanzamos el _subscriber_: ```bash mosquitto_sub -h localhost -t test -u mqttuser -P 53cret0 ``` Y en otro terminal publicamos un mensaje en el _topic_ test: ```bash mosquitto_pub -h localhost -t test -u mqttuser -P 53cret0 -m "Hola mundo" ``` Comprobaremos que los mensajes se reciben en el primer terminal. ### InfluxDB Vamos a instalar _InfluxDB_ en _docker_. Igual que con _Mosquitto_ vamos a mapear los ficheros de configuración y los ficheros donde realmente guarda los datos la base de datos en nuestro sistema de ficheros local (o el del host) Estamos en el directorio `homesensor` y creamos el directorio `02_influxdb` ```bash mkdir 02_influxdb cd 02_influxdb ``` Nos bajamos la imagen oficial de _InfluxDB_ con `docker pull influxdb` Podemos ejecutar un contenedor para probar la imagen con ```bash docker run -d --name influxdbtest -p 8086:8086 influxdb ``` La opción `-d` (_detached_) es opcional, si no la pones verás el log de influxdb, pero no libera el terminal. Ahora desde otro terminal (o desde el mismo si usaste `-d`) podemos conectarnos al contenedor con uno de los dos comandos siguientes: ```bash docker exec -it influxdbtest /bin/bash # con un terminal docker exec -it influxdbtest /usr/bin/influx # con el cliente de base de datos ``` Dentro del cliente de base de datos podríamos hacer lo que quisieramos, por ejemplo: ```sql show databases create database mydb use mydb ``` Como hemos mapeado el puerto 8086 del contenedor al host (nuestro pc) también podemos conectarnos a la base de datos con un cliente en nuestro pc, con scripts de python, etc. etc. #### Dos maneras de lanzar el contenedor Podemos examinar la historia de la imagen docker `influxdb` con `docker history influxdb`(desde un terminal en nuestro pc). Si sale muy apelotonada podemos indicarle que no recorte la salida con `docker history --no-trunc influxdb` Veremos que se define `ENTRYPOINT ["/entrypoint.sh"]` Podemos investigar el contenido del fichero `entrypoint.sh` abriendo un terminal contra el contenedor `influxdbtest` Después de investigar el contenido del fichero `influxdbtest.sh` y el contenido del fichero `init-influxdb.sh` veremos que hay dos formas de lanzar un contenedor a partir de la imagen `influxdb` (También nos podíamos haber leído [esto](https://hub.docker.com/_/influxdb) para llegar a lo mismo más rápido) La primera forma de lanzar el contenedor es la que hemos usado hasta ahora: ```bash docker run -d --name influxdbtest -p 8086:8086 influxdb ``` La segunda forma (más interesante para ciertas cosas) sería invocando el script `init-influxdb.sh`: ```bash docker run --name influxdbtest influxdb /init-influxdb.sh ``` Con esta segunda forma de lanzar el contenedor tenemos dos ventajas: * podemos usar variables de entorno y scripts de inicio, como nos explican en [la pagina de dockerhub](https://hub.docker.com/_/influxdb) que citamos antes. * podemos tener un directorio `/docker-entrypoint-initdb.d` en la raiz de nuestro contenedor y cualquier script con extensión `.sh` (shell) o `.iql` (Influx QL) se ejecutará al arrancar el contenedor. __Pero__ la segunda forma de lanzar el contenedor sólo es práctica para crear y configurar una base de datos, no está pensada para lanzar un contenedor "de producción" Cuando acabemos de jugar, paramos el contenedor y lo borramos: ```bash docker stop influxdbtest docker rm influxdbtest ``` #### Configuración de _InfluxDB_ Vamos a preparar las cosas para lanzar un contenedor transitorio de _InfluxDB_ que nos configure la base de datos que queremos usar. Después lanzaremos el contenedor definitivo que usará la base de datos configurada por el primero para dejar el servicio _InfluxDB_ operativo. Lo primero es hacernos un fichero de configuración para _InfluxDB_. Hay un truco para que el propio _InfluxDB_ nos escriba el fichero de configuración por defecto: ```bash cd homesensor/02_influxdb docker run --rm influxdb influxd config |tee influxdb.conf ``` Este comando ejecuta un contenedor transitorio basado en la imagen `influxdb`, es un contenedor transitorio por que con la opción `--rm` le decimos a _docker_ que lo elimine al terminar la ejecución. Este contenedor lo unico que hace es devolvernos el contenido del fichero de configuración y desaparecer de la existencia. Ahora tendremos un fichero de configuración `influxdb.conf` en nuestro directorio `02_influxdb`. Este fichero lo tenemos que mapear o copiar al fichero `/etc/influxdb/influxdb.conf` del contenedor (lo vamos a mapear) Crearemos también un volumen en nuestro contenedor que asocie el directorio `/var/lib/influxdb` del contenedor al directorio en el sistema de ficheros local: `homesensor/data/influxdb` De esta forma tendremos los ficheros que contienen la base de datos en el sistema de ficheros del host. También vamos a crear un directorio `homesensor/02_influxdb/init-scripts` que mapearemos al directorio `/docker-entrypoint-initdb.d` del contenedor. En este directorio crearemos un script en _Influx QL_ para crear usuarios y políticas de retención para nuestra base de datos. En el directorio `homesensor/02_influxdb/init-scripts`, creamos el fichero: `influxdb_init.iql` ```sql CREATE RETENTION POLICY one_week ON homesensor DURATION 168h REPLICATION 1 DEFAULT; CREATE RETENTION POLICY one_year ON homesensor DURATION 52w REPLICATION 1; CREATE USER telegraf WITH PASSWORD 'secretpass' WITH ALL PRIVILEGES; CREATE USER nodered WITH PASSWORD 'secretpass'; CREATE USER pybridge WITH PASSWORD 'secretpass'; GRANT ALL ON homesensor TO nodered; GRANT ALL ON homesensor TO pybridge; ``` --- __NOTA___: Creamos el usuario telegraf con todos los privilegios, eso lo convierte en administrador de _InfluxDB_. De momento lo vamos a dejar así para facilitar las pruebas del contenedor _Telegraf_. --- ¡Vale! Ya tenemos todo listo. Ahora tenemos que hacer dos cosas. 1. Ejecutar un contenedor transitorio que va a crear * Mediante variables de entorno (opciones `-e`): * Un usario administrador de _InfluxDB_ * Una base de datos `homesensor` * Un usuario genérico de la base de datos `homesensor` que se llama `hsuser`. * Mediante el script en _InfluxQL_: * Un par de políticas de retención para la base de datos `homesensor` * Tres usuarios de bases de datos, a los que damos privilegios sobre la base de datos `homesensor`: * `telegraf` * `noderedu` * `pybridge` 2. Una vez configurada toda la base de datos: activar el acceso seguro en la configuración y lanzar el contenedor `influxdb` que se va a quedar trabajando. Para el primer paso __configurar la base de datos__ el comando es: ```bash cd homesensor # ASEGURATE de estar en el directorio homesensor sudo rm -rf data/influxdb docker run --rm \ -e INFLUXDB_HTTP_AUTH_ENABLED=true \ -e INFLUXDB_ADMIN_USER=admin -e INFLUXDB_ADMIN_PASSWORD=s3cr3t0 \ -e INFLUXDB_DB=homesensor \ -e INFLUXDB_USER=hsuser -e INFLUXDB_USER_PASSWORD=pr1vad0 \ -v $PWD/data/influxdb:/var/lib/influxdb \ -v $PWD/02_influxdb/influxdb.conf:/etc/influxdb/influxdb.conf \ -v $PWD/02_influxdb/init-scripts:/docker-entrypoint-initdb.d \ influxdb /init-influxdb.sh ``` ---- __IMPORTANTE__ * Si no pones la opción `-e INFLUXDB_HTTP_AUTH_ENABLED=true` los comandos de creación de usuarios fallan sin dar error. * Cada vez que lances el contenedor de iniciación __ASEGURATE__ de borrar previamente el directorio `homesensor/data/influxdb` o fallará todo sin dar errores. ---- Para el segundo paso, es __necesario__ habilitar la seguridad en el fichero de configuración antes de arrancar el contenedor. En el fichero `influxdb.conf` asegurate de cambiar la linea (de _false_ a _true_): ```ini [http] - auth-enabled = false + auth-enabled = true ``` Y por fin, para arrancar el contenedor `influxdb` que se quedará dando servicio, el comando es: ```bash cd homesensor # asegurate de estar en el directorio homesensor docker run -d --name influxdb -p 8086:8086 \ -v $PWD/data/influxdb:/var/lib/influxdb \ -v $PWD/02_influxdb/influxdb.conf:/etc/influxdb/influxdb.conf \ influxdb ``` Con el contenedor _influxdb_ disponible podemos conectarnos a la base de datos, ya sea desde nuestra máquina (si tenemos el cliente instalado en local) o abriendo un terminal al contenedor. Si ejecutamos: ```bash > show databases ERR: unable to parse authentication credentials Warning: It is possible this error is due to not setting a database. Please set a database with the command "use ". ``` Nos da un error de autenticación. Si añadimos usuario y password entramos sin problemas y podemos vere que la base de datos `homesensor` y todos los usuarios se han creado correctamente. ```bash influx --username admin --password s3cr3t0 Connected to http://localhost:8086 version 1.8.3 InfluxDB shell version: 1.8.3 > show databases name: databases name ---- homesensor _internal > show users user admin ---- ----- admin true idbuser false telegrafuser false nodereduser false pybridgeuser false ``` Un ejemplo de continous query para el dia que haga falta: ```sql CREATE CONTINUOUS QUERY "cq_30m" ON "homesensor" BEGIN SELECT mean("temperature") AS "mean_temperature",mean("humidity") AS "mean_humidity" \ INTO "a_year"."acum_ambient" FROM "ambient" GROUP BY time(30m) END; ``` ### Telegraf Preparamos un fichero de configuración de _Telegraf_. _Telegraf_ es de la familia _InfluxDB_ así que soporta el mismo truco, para volcar una configuración por defecto: ```bash cd homesensor mkdir 03_telegraf cd 03_telegraf docker run --rm telegraf telegraf config |tee telegraf.conf ``` En _Telegraf_ todo se hace en el fichero de configuración, en el se programan las entradas y las salidas. #### Usando `container:network` Este escenario lo pongo por que me pareció un caso interesante. Pero no veo que sea posible implementarlo en _docker-compose_ así que no será el definitivo. 1. Lanzamos el container `influxdb` ```bash docker run -d --name influxdb -p 8086:8086 \ -v $PWD/data/influxdb:/var/lib/influxdb \ -v $PWD/02_influxdb/influxdb.conf:/etc/influxdb/influxdb.conf \ -v $PWD/02_influxdb/init-scripts:/docker-entrypoint-initdb.d \ influxdb ``` 2. Modificamos el fichero de configuración de _Telegraf_ para poner usuario y password. ```init ## HTTP Basic Auth username = "telegraf" password = "secretpass" ``` 3. Lanzamos el contenedor `telegraf` ```bash docker run -d --name telegraf \ --network=container:influxdb \ -v /proc:/host/proc:ro \ -v $PWD/03_telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro \ telegraf ``` Si nos conectamos con el cliente `influx` al servidor de bases de datos (o nos conectamos al contenedor y ejecutamos el cliente) veremos que _Telegraf_ está mandando todas las estadísticas que tiene definidas por defecto de nuestro host. Para que todo esto funcione: * Hemos mapeado el directorio `/proc` del linux de nuestro host (nuestro pc o servidor) en el directorio `/host/proc` del contenedor `telegraf`, así que _Telegraf_ puede leer varias estadísticas de nuestro _host_ (es decir nuestro pc) y escribir los distintos valores como _measurements_ en la base de datos `telegraf` (es el nombre de la base de datos que usa por defecto). * El contenedor `telegraf` ha creado esta base de datos que no existía gracias a que el usario que le hemos configurado para acceder a la base de datos tiene permisos de administración, de lo contrario tendríamos que haber creado nosotros la base de datos antes de que se conectara. * Hemos usado la opción `--network=container:influxdb`, esta opción hace que el contenedor `telegraf` se cree en el `network stack` del contenedor `influxdb`, en la práctica el _Telegraf_ se cree que está en la misma máquina que _InfluxDB_ y se comunica via el interfaz _loopback_ Esta opción tan curiosa creo que no se puede implementar en _docker-compose_ (al menos yo no he encontrado nada parecido) ### Grafana Grafana se puede configurar a través de su propia interfaz web. ---- __NOTA__: No me aclaro con [la documentación de _Grafana_ para _Docker_](https://grafana.com/docs/grafana/latest/installation/docker/), la única forma en que he conseguido mapear la base de datos en el directorio local `data/grafana` ha sido lanzar el contenedor como _root_. No se que riesgos implica eso. ---- Lanzamos el contenedor: ```bash docker run --user root -d --name=grafana -p 3000:3000 \ -v $PWD/data/grafana:/var/lib/grafana \ grafana/grafana ``` La primera vez que nos conectamos a grafana tenemos que entrar con el usuario `admin` con contraseña `admin`. Nos pedirá cambiar la contraseña. * Nos conectamos a * Configuramos la contraseña del administrador * Añadimos InfluxDB como fuente de datos. En este escenario tenemos que configurar la dirección IP del contenedor `influxdb` así que tendremos que usar algún comando del estilo `docker inspect influxdb` o `docker inspect bridge |grep influxdb -A 5` para averiguar la dirección IP. * __name__ influxdb * __query language__ InfluxQL * __http url__ * __Access__ server * __Auth__ Basic auth with credentials * __user__ telegraf (con su contraseña) * __database__ telegraf, configurar usuario y contraseña * __http method__ GET * Por ultimo el report de grafana lo importamos de uno que hay pre-construido para este escenario con el id 1443 [Grafana Data Sources tutorial](https://grafana.com/tutorials/grafana-fundamentals/?utm_source=grafana_gettingstarted#1) ### Solución completa con _docker-compose_ #### _mosquitto_ en _docker-compose_ Definiremos el servicio _mosquitto_ en nuestro fichero `homesensor/docker-compose.yml` ```docker version: '3' services: mosquitto: image: eclipse-mosquitto container_name: mosquitto ports: - 1883:1883 volumes: - ./01_mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf - ./01_mosquitto/users:/mosquitto/config/users - ${DATA_DIR}/mosquitto/data:/mosquitto/data - ${DATA_DIR}/mosquitto/log:/mosquitto/log restart: always ``` Como se puede ver en el fichero `homesensor/docker-compose.yml` hemos definido dos volúmenes adicionales para tener accesibles desde el _host_ el directorio de datos: `/mosquitto/data` y el directorio de logs `/mosquitto/log` El mapeado de los nuevos volúmenes está controlado por una variable `DATA_DIR` que podemos pasar desde el entorno de sistema o con un fichero `.env` para que apunte, por ejemplo, al directorio `homesensor/data` en el host. `.env` ```bash DATA_DIR=./data ``` Ahora podemos lanzar el contenedor con `docker-compose up` (asegúrate de borrar el contenedor que creamos a mano, y de establecer la variable `DATA_DIR` de alguna manera) Una vez lanzado el contenedor podemos hacer pruebas con los clientes mqtt instalados y comprobaremos que los datos y el log de _mosquitto_ se guardan en el host. Otra cosa que podemos comprobar al usar _docker-compose_ es que se crea una red específica y segregada para nuestros contenedores. ```bash docker network ls NETWORK ID NAME DRIVER SCOPE d48756f76467 bridge bridge local 315e4570dff1 homesensor_default bridge local 9b579976258b host host local 51c50bab6f35 none null local docker inspect homesensor_default . . . "Containers": { "39a45dde629a0ab6f834e7c59f0d9785c64ba31b0d3d93c6c8d2622d31607c28": { "Name": "cmosquitto", "EndpointID": "504ae74b9109e6d87cd2359f7d6b17352388a5a9a48f7bba5aa7ad29fa43977e", "MacAddress": "02:42:ac:1b:00:02", "IPv4Address": "172.27.0.2/16", "IPv6Address": "" } }, . . . ``` Podemos ver que ahora tenemos la red `homesensor_default` y que dentro de esa red solo tenemos el contenedor `mosquito`. #### _InfluxDB_ en _docker-compose_ Añadimos la configuración de _InfluxDB_ a nuestro fichero `dockercompose.yml` #### Grafana en _docker-compose_ Añadimos la configuración del servicio ## Recetas ### Backup de volúmenes Los pasos típicos 1. Parar todos los contenedores que usen los volúmenes que queremos salvar 2. Arrancar un contenedor dedicado para hacer los backups. Tiene que montar los volúmenes que queremos salvaguardar y montar también un volumen (probablemente un _bind-mount_, donde salvaremos los backups) 3. Hacer los backups 4. Parar el contenedor de backups 5. Arrancar los contenedores de producción Un ejemplo: ```bash docker stop targetContainer mkdir ./backup docker run --rm --volumes-from targetContainer -v ~/backup:/backup ubuntu bash -c “cd /var/lib/targetCont/content && tar cvf /backup/ghost-site.tar .” ``` ### Ejecutar aplicaciones X11 en un contenedor Docker Supongamos que queremos ejecutar una aplicación X11 en un contenedor y verla en el escritorio del Host. Tenemos dos alternativas, lanzar la aplicación contra el Xdisplay de nuestro ordenador (`echo "$DISPLAY"`) o preparar otro _display_ dedicado a nuestro contenedor. {{< admonition type=tip title="Otras vias" open=false >}} [Aquí](https://www.howtogeek.com/devops/how-to-run-gui-applications-in-a-docker-container/) describen como lanzar aplicaciones via el _X11 socket_ o utilizando VNC. Pero yo creo que las recetas que apunto son más fáciles. {{< /admonition >}} #### Usar el Xdisplay de nuestro escritorio Las ventajas de esta opción es que la aplicación del contenedor aparecerá simplemente como una ventana más en nuestro escritorio, no deberíamos tener ningún problema a la hora de usar nuestros dispositivos de entrada (ratón, teclado, etc.) y de salida (pantalla básicamente). 1. Averiguar nuestro display con `echo "$DISPLAY"` 2. Autorizar las conexiones a nuestro display, por seguridad están prohibidas ```bash xhost + # Esto equivale a autorizar todas las conexiones # Es mejor NO USAR esta opción xhost +"local:docker@" # Esta opción es mucho más segura # solo permite conexiones desde nuestros contenedores ``` 3. Lanzar nuestro contenedor estableciendo la variable de entorno `$DISPLAY` y fijando el `net` de tipo `host`: ```bash docker run --rm --it --net=host -e DISPLAY=$DISPLAY ``` El detalle importante aquí es limitar lo más posible las conexiones autorizadas a nuestro Xdisplay #### Lanzar otro display dedicado Yo suelo usar _Xephyr_ para hacer estos experimentos. _Xephyr_ es un servidor de display anidado. _Xephyr_ nos da un Xdisplay al que podemos conectar aplicaciones X11 pero que funciona como una ventana dentro de nuestro entorno gráfico (es decir está anidado en el Xdisplay de nuestro escritorio). La ventaja de esta opción es que no abrimos autorizaciones al Xdisplay de nuestro sistema. A cambio tendremos que ver como usa Xephyr nuestro teclado y ratón. 1. Instalar _Xephyr_ ```bash sudo apt install xserver-xephyr ``` 2. Lanzar una ventana de _Xephyr_ ```bash echo "$DISPLAY" # Averiguamos nuestro DISPLAY, que suele ser el 0 o el 1 # HAY QUE USAR UNO DISTINTO PARA Xephyr Xephyr -ac -screen 800x600 -br -reset -terminate 2> /dev/null :1 & # Arrancamos el Xserver ``` Opciones de Xephyr utilizadas: - __-ac__ Autorizar conexiones de clientes indiscriminadamente (disable access restrictions) - __-screen__ Especificar la geometría de la pantalla - __-br__ La ventana raiz tendrá fondo negro - __-reset__ Reset al terminar el último cliente - __-terminate__ Finalizar cuando se resetee el servidor - __2> /dev/null__ Mandar los mensajes de error al limbo (alias NE en nuestro pc) - __:1__ Arrancar el server en el DISPLAY=1 __TIENE QUE SER DISTINTO DEL DISPLAY DEL SISTEMA__ 3. Por último lanzamos nuestro contenedor igual que en la primera opción, pero con el Xdisplay correspondiente a _Xephyr_ ```bash docker run --rm --it --net=host -e DISPLAY=":1" ``` ## Referencias * [Getting Started (oficial)](https://docs.docker.com/get-started/) * [El tutorial de atareao](https://www.atareao.es/tutorial/docker/) * [The smart person guide to Docker](https://www.techrepublic.com/article/docker-the-smart-persons-guide/) * [Lista de versiones de compose-file y manual de referencia]( https://docs.docker.com/compose/compose-file/) * [Kitematic: un interfaz gráfico para docker](https://kitematic.com/) * [Podman, un docker alternativo](https://www.atareao.es/podcast/es-podman-la-alternativa-a-docker/) * [Docker-compose tutorial](https://www.educative.io/blog/docker-compose-tutorial) <-- No está mal * [Advanced Docker-compose configuration](https://runnable.com/docker/advanced-docker-compose-configuration) * [Docker Compose Tutorial](https://vegibit.com/docker-compose-tutorial/) * [Awesome Compose](https://github.com/docker/awesome-compose) ### influxdb * https://thenewstack.io/how-to-setup-influxdb-telegraf-and-grafana-on-docker-part-1/ * https://lazyadmin.nl/it/installing-grafana-influxdb-and-telegraf-using-docker/ * https://dev.to/project42/install-grafana-influxdb-telegraf-using-docker-compose-56e9 ### uwsgi and traefik * https://blog.miguelgrinberg.com/post/running-your-flask-application-over-https * https://beenje.github.io/blog/posts/running-your-application-over-https-with-traefik/ * https://dockerquestions.com/2020/03/23/route-to-flask-and-vue-containers-with-traefik/ * https://www.fullstackpython.com/wsgi-servers.html * https://gist.github.com/nknapp/20c7cd89f1f128b8425dd89cbad0b802 * https://medium.com/@tiangolo/full-stack-modern-web-applications-using-python-flask-docker-swagger-and-more-b6609dedb747 * https://stackoverflow.com/questions/44639958/nginx-behind-traefik-docker-swarm-mode-real-ip ### nginx and traefik * [Static files with nginx and traefik](https://www.simplecto.com/use-traefik-with-nginx-apache-caddyserver-serve-static-files/) * [Install traefik](https://www.howtoforge.com/tutorial/ubuntu-docker-traefik-proxy/) * [Traefik and worpress, as usual](https://www.digitalocean.com/community/tutorials/how-to-use-traefik-as-a-reverse-proxy-for-docker-containers-on-ubuntu-16-04) * [nginx behind traefik question](https://stackoverflow.com/questions/44639958/nginx-behind-traefik-docker-swarm-mode-real-ip) * https://docs.ovh.com/gb/en/domains/create_a_dns_zone_for_a_domain_which_is_not_registered_at_ovh/ ### Kibana and Elastic Search * [elastic kibana and docker-compose](https://codingfundas.com/how-to-install-elasticsearch-7-with-kibana-using-docker-compose/index.html) * [quick start](https://www.devopsroles.com/quick-start-install-elasticsearch-and-kibana-with-docker/) * [docker logs elastic and kibana](https://www.sarulabs.com/post/5/2019-08-12/sending-docker-logs-to-elasticsearch-and-kibana-with-filebeat.html)