63 KiB
weight | title | date | draft | summary | categories | tags | |||
---|---|---|---|---|---|---|---|---|---|
4 | Apuntes de Docker | 2021-01-23T16:01:50+0100 | false | Apuntes de docker |
|
|
{{< 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 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: 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:
docker run -it --rm alpine /bin/ash
-
Abrir un terminal contra un contenedor que ya está corriendo:
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
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 >}}
{{< /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 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:
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:
- Un dockerfile es un fichero de texto que contien las instrucciones que se ejecutarán con
docker build
para construir una imagen Docker - Es un conjunto de instrucciones paso a paso
- Se compone de un conjunto de instrucciones estándar (como
FROM
,ADD
, etc. etc.) - 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
Simple
Un Dockerfile muy simple para un script de python
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
Este es un ejemplo de Dockerfile multi-stage
. En un Dockerfile de tipo multi-stage
cada FROM
(puede haber varios) usa la base que quieras para abrir una nueva etapa (stage) Se pueden copiar artefactos de una etapa anterior de forma selectiva.
- En la primera etapa, que llamaremos
builder
partimos de la imagenalpine:3.20.1
- Instalamos:
pcre
, una biblioteca de expresiones regulareslibxml2
libxslt
gcc
, el compilador gnumake
, la herramientalibc-dev
pcre-dev
zlib-dev
libxml2-dev
libxslt-dev
- Descargamos el código de nginx en el directorio
/tmp
y lo descomprimimos - Ejecutamos
autoconfigure
especificando que se instale en/opt
- Compilamos con
make
e instalamos conmake install
- Desinstalamos la bibliotecas y borramos la caché (aunque no creo que esto valga para nada, hasta donde yo entiendo la etapa
builder
se descarta)
FROM alpine:3.20.1 AS builder
RUN apk add --update \
--no-cache \
pcre~=8.45-r3 \
libxml2~=2.12.7-r0 \
libxslt~=1.1.39-r1 \
gcc~=13.2 \
make~=4.4 \
musl-dev~=1.2.5 \
pcre-dev~=8.45 \
zlib-dev~=1.3.1 \
libxml2-dev~=2.12.7 \
libxslt-dev~=1.1.39 && \
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.20.1
ARG UID=${UID:-1000}
ARG GID=${GID:-1000}
RUN apk add --update \
--no-cache \
pcre~=8.45 \
libxml2~=2.12.7 \
libxslt~=1.1.39 \
tini~=0.19 \
shadow~=4.15 &&\
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"]
La segunda etapa empieza en la linea 24 definiendo un par de variables para establecer en UID
y el GID
del usuario www-data
que usaremos dentro del contenedor nginx
para ejecutar el servidor.
- Instalamos las bibliotecas:
pcre
libxml2
libxslt
tini
, un microinit
ideal para implementar contenedores ligerosshadow
, el PAM de Alpine
- Limpiamos la caché de instalación de paquetes
- Cambiamos el
GID
para el grupowww-data
- Creamos el usuario
www-data
con elUID
especificado - Creamos el directorio
/html
- Copiamos los ejecutables de nginx de la etapa anterior
- Copiamos el
nginx.conf
y elentrypoint.sh
desde el directorio de trabajo del Dockerfile - Declaramos el puerto de acceso y el volumen correspondiente a
/html
- Ejecutamos como usuario
www-data
el entrypoint del contenedor.
Ficheros auxiliares
entrypoint
El fichero entrypoint
que hemos usado es muy simple:
#!/bin/bash
mediaowner=$(ls -ld /html | awk '{print $3}')
echo "Current /html owner is $mediaowner"
/opt/nginx/sbin/nginx -g "daemon off;"
El fichero nginx.conf
tampoco es muy complicado. Dejamos secciones comentadas para tener algunas referencias.
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8080;
server_name localhost;
access_log /dev/stdout;
error_log /dev/stdout info;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /html;
index index.html index.htm;
include mime.types;
try_files $uri $uri/ =404;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
Networks
Bridge
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):
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
- Stackoverflow: docker data volume vs mounted host directory
- Docker 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: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)
volumes: - web_data:/usr/share/nginx/html:ro
O también:
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)
#/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
- Definir los entornos de aplicación con un dockerfile
- Definir los servicios provistos mediante docker-compose. Esto permitirá ejecutarlos en un entorno aislado.
- 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:
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:
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:
- Compose file (en una sección enviroment)
- Variables de entorno del sistema (en nuestro intérprete)
- Enviroment file, es un fichero en el mismo directorio que el
fichero de compose con el nombre
.env
- En el dockerfile
- 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:
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:docker-compose up -d --no-deps --build <service>
Tenemos una opción más moderna que sería el comando:
docker-compose build --no-cache <service>
O bien:
docker-compose up -d --no-cache --build <service>
Mosquitto + Influxdb + Graphana con docker y docker-compose
Creamos un directorio para nuestro proyecto que llamamos homesensor
(por ejemplo)
mkdir homesensor
cd homesensor
Mosquitto
Creamos un directorio para nuestro container de Mosquitto
mkdir 01_mosquitto
cd 01_mosquitto
Configuración de mosquitto
Creamos un fichero de configuración de Mosquitto mosquitto.conf
con el siguiente
contenido:
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 <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:
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.
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ónt
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:
mosquitto_sub -h localhost -t test -u mqttuser -P 53cret0
Y en otro terminal publicamos un mensaje en el topic test:
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
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
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:
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:
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:
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
:
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:
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:
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
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.
- 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 llamahsuser
.
- 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
- Un par de políticas de retención para la base de datos
- Mediante variables de entorno (opciones
- 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:
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):
[http]
- auth-enabled = false
+ auth-enabled = true
Y por fin, para arrancar el contenedor influxdb
que se quedará dando
servicio, el comando es:
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:
> 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.
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:
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:
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.
- Lanzamos el container
influxdb
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
- Modificamos el fichero de configuración de Telegraf para poner usuario y password.
## HTTP Basic Auth
username = "telegraf"
password = "secretpass"
- Lanzamos el contenedor
telegraf
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 contenedortelegraf
, 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 datostelegraf
(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 contenedortelegraf
se cree en elnetwork stack
del contenedorinfluxdb
, 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:
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 estilodocker inspect influxdb
odocker 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
Solución completa con docker-compose
mosquitto en docker-compose
Definiremos el servicio mosquitto en nuestro fichero
homesensor/docker-compose.yml
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
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.
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
- Parar todos los contenedores que usen los volúmenes que queremos salvar
- 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)
- Hacer los backups
- Parar el contenedor de backups
- Arrancar los contenedores de producción
Un ejemplo:
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í 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).
-
Averiguar nuestro display con
echo "$DISPLAY"
-
Autorizar las conexiones a nuestro display, por seguridad están prohibidas
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
-
Lanzar nuestro contenedor estableciendo la variable de entorno
$DISPLAY
y fijando elnet
de tipohost
: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.
-
Instalar Xephyr
sudo apt install xserver-xephyr
-
Lanzar una ventana de Xephyr
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
-
-
Por último lanzamos nuestro contenedor igual que en la primera opción, pero con el Xdisplay correspondiente a Xephyr
docker run --rm --it --net=host -e DISPLAY=":1"
Referencias
-
Docker-compose tutorial <-- No está mal
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