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