Arquitectura de Docker

Docker tiene una arquitectura cliente-servidor, donde el cliente es un programa que se ejecuta en nuestro ordenador, y el servidor es un daemon o un servicio que se encarga de crear y administrar los contenedores.

Arquitectura de Docker

Contenedores

Los contenedores son el concepto fundamental al hablar de docker.

Un contenedor es una entidad lógica, una agrupación de procesos que se ejecutan de forma nativa como cualquier otra aplicación en la máquina host. Los procesos no tienen acceso a nada que se encuentre fuera del contenedor, y no sólo eso, no tienen forma de consumir más recursos que los que el contenedor les permite.

El único software que se comparte entre un contenedor y la máquina host es el kernel del sistema operativo. En cuanto al sistema de archivos, a cada contenedor se le asigna una carpeta, la cual toma como raíz, y no puede acceder a nada que esté fuera de esa carpeta, porque "no va a saber que existe".

Ciclo de vida de un contenedor

Veamos un ejemplo. Ejecutamos por primera vez un contenedor ubuntu con el comando tail -f /dev/null:

docker run --name ubuntu ubuntu tail -f /dev/null

Ahora, podemos ejecutar otro proceso en ese mismo contenedor, por ejemplo, bash:

docker exec -it ubuntu bash

Esto nos ha abierto una terminal (porque es el proceso que acabamos de ejecutar). Si listamos todos los procesos de todas las sesiones, con ps -fea, vemos lo siguiente:

UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 12:18 ?        00:00:00 tail -f /dev/null
root         6     0  1 12:19 pts/0    00:00:00 bash
root        16     6  0 12:19 pts/0    00:00:00 ps -fea

Podemos observar que el PID=1 corresponde al root command, es decir, al proceso ejecutado al crear el contenedor. Eso significa que el contenedor se apagará cuando ese proceso termine.

Imágenes

Las imágenes son otro componente fundamental de Docker y sin ellas los contenedores no tendrían sentido.

Estas imágenes son fundamentalmente plantillas o templates. Es como si estuvieran formadas por un conjunto de capas, donde partiemos de una capa base y les vamos añadiendo más capas encima.

Algo que debemos tener en cuenta es que las imágenes no van a cambiar, es decir, una vez esté creada, no la podremos cambiar.

Cómo crear nuestra propia imagen

Para crear una imagen, tenemos que crear un archivo llamado Dockerfile, como puede ser este:

FROM ubuntu

RUN touch /usr/src/hola-platzi

Ahora, construimos la imagen a partir de ese archivo con el siguiente comando:

docker build -t ubuntu:platzi .

Y esto es lo que está pasando:

docker build -t ubuntu:platzi .

En resumen, el flujo de construcción en Docker sería el siguiente:

Flujo de construcción de imágenes y contenedores en Docker

Datos en Docker

Vamos a ver diferentes maneras que existen en Docker para almacenar datos fuera del contenedor.

Bind mount

A modo de ejemplo, vamos a crear una base de datos, y la vamos a guardar en un directorio de nuestro host, permitiéndole al contenedor acceder a ese directorio.

docker run --name db -d -v <host-path>:<container-path> mongo

Para comprobar que el contenedor tiene acceso al directorio que le hemos asignado, podemos utilizar docker inspect, y vemos que el mount que acabamos de crear existe. El otro mount es el que viene por defecto con Mongo.

"Mounts": [
    {
        "Type": "bind",
        "Source": "/Users/mgoigfer/Desktop/db",
        "Destination": "/data/db",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    },
    {
        "Type": "volume",
        "Name": "b4cfe69325381aebf27830ee69e18b3f8b6e29808c6afdb84d72cfedf4c10eb6",
        "Source": "/var/lib/docker/volumes/b4cfe69325381aebf27830ee69e18b3f8b6e29808c6afdb84d72cfedf4c10eb6/_data",
        "Destination": "/data/configdb",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

De esta manera, todo lo que guardemos en la base de datos se almacenerá en el volumen que lo hemos indicado. Y por tanto, si por algún motivo eliminamos el contenedor, esa carpeta seguirá existiendo en nuestro host y no perderemos la base de datos. Por otro lado, si queremos crear un nuevo contenedor para esa misma base de datos, lo haremos partiendo de ese directorio.

Volúmenes

El problema de bind mount es que le estamos dando acceso a Docker a una parte de nuestro sistema, esto no siempre es lo correcto. Por eso aparecieron los volúmenes en Docker, que funcionan de la misma manera que bind mount, pero la diferencia es que ese espacio donde se van a almacenar los archivos está gestionado por Docker. Por lo tanto, el resto de procesos del host no debería poder acceder.

Por ejemplo, si tenemos creado un volumen y se lo queremos asignar a una base de datos, lo haremos de la siguiente manera:

docker run -d --name db --mount src=<volume-name>,dst=<container-path> mongo

Servidores en Docker

Los contenedores están aislados de todo el sistema, y a nivel de red, cada contenedor tiene su propia stack de net y sus propios puertos. Para que los contenedores sean accesibles desde el exterior, debemos redirigir los puertos del contenedor a los de la computadora. Eso lo podemos hacer con este comando:

docker run --name server -d -p <host-port>:<container-port> nginx

Redes en Docker

Docker nos permite conectar contenedores mediante redes.

Por defecto, Docker crea tres redes:

NETWORK ID          NAME                DRIVER              SCOPE
6e763d91ff6d        bridge              bridge              local
3292e4aaf222        host                host                local
0fbe4b9074ce        none                null                local 

bridge es una red que está en desuso, así que no la vamos a utilizar.

host representa la red de la máquina host, con lo cual es peligroso utilizarla, ya que el contenedor queda expuesto a toda la red local.

none es como una red que hace que los contenedores conectados a ella estén totalmente aislados; se utiliza en contenedores que manejan procesos sensibles, para que no puedan ni comunicarse ni recibir datos con el exterior.