Apuntes de Docker

Advertencia
Este artículo se actualizó por última vez el 2023-03-09, es posible que el contenido no esté actualizado.
Work in progress
Estos apuntes no están completos, (ni de lejos)

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.

Seguridad e Imágenes

Mucho ojo con esto, por que en el Docker Store 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.

Las imágenes oficiales suelen residir en un repo de primer nivel dentro de dockerhub y tendrán una url como esta, por ejemplo: https://hub.docker.com/_/nginx/

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:

    1
    
    docker run -it --rm alpine /bin/ash
  • Abrir un terminal contra un contenedor que ya está corriendo:

    1
    
    docker exec -it <containerId> 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

1
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

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 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

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:

    1
    2
    3
    
    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)

    1
    2
    
    volumes:
        - web_data:/usr/share/nginx/html:ro

    O también:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    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)

1
2
3
4
#/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

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
  2. 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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)

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:

1
2
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

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:
1
docker-compose up -d --no-deps --build <service>

Tenemos una opción más moderna que sería el comando:

1
docker-compose build --no-cache <service>

O bien:

1
docker-compose up -d --no-cache --build <service>

Mosquitto + Influxdb + Graphana con docker y docker-compose

referencia

Creamos un directorio para nuestro proyecto que llamamos homesensor (por ejemplo)

1
2
mkdir homesensor
cd homesensor

Mosquitto

Creamos un directorio para nuestro container de Mosquitto

1
2
mkdir 01_mosquitto
cd 01_mosquitto

Configuración de mosquitto

Creamos un fichero de configuración de Mosquitto mosquitto.conf con el siguiente contenido:

1
2
3
4
5
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:

1
mqttuser:53cret0

Y cifrarlo con el comando mosquitto_passwd -U <filename>

Si solo tenemos un usuario lo podemos crear con el comando mosquitto_passwd -c <filename> <username>


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:

1
2
3
4
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.

1
2
3
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:

1
mosquitto_sub -h localhost -t test -u mqttuser -P 53cret0

Y en otro terminal publicamos un mensaje en el topic test:

1
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

1
2
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

1
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:

1
2
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:

1
2
3
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 para llegar a lo mismo más rápido)

La primera forma de lanzar el contenedor es la que hemos usado hasta ahora:

1
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:

1
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 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:

1
2
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:

1
2
3
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

1
2
3
4
5
6
7
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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):

1
2
3
[http]
- auth-enabled = false
+ auth-enabled = true

Y por fin, para arrancar el contenedor influxdb que se quedará dando servicio, el comando es:

1
2
3
4
5
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:

1
2
3
4
> 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 <database>".

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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:

1
2
3
4
5
6
7
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:

1
2
3
4
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
1
2
3
4
5
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
  1. Modificamos el fichero de configuración de Telegraf para poner usuario y password.
1
2
3
## HTTP Basic Auth
username = "telegraf"
password = "secretpass"
  1. Lanzamos el contenedor telegraf
1
2
3
4
5
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, 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:

1
2
3
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 http://localhost:3000

  • 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 http://ipaddres:8086
    • 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

Solución completa con docker-compose

mosquitto en docker-compose

Definiremos el servicio mosquitto en nuestro fichero homesensor/docker-compose.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

1
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
2
3
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.

Otras vias
Aquí describen como lanzar aplicaciones via el X11 socket o utilizando VNC. Pero yo creo que las recetas que apunto son más fáciles.

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

    1
    2
    3
    4
    5
    
    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:

    1
    
    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

    1
    
    sudo apt install xserver-xephyr
  2. Lanzar una ventana de Xephyr

    1
    2
    3
    4
    
    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

    1
    
    docker run --rm --it --net=host -e DISPLAY=":1"

Referencias

influxdb

uwsgi and traefik

nginx and traefik

0%