9.2.2.2.2.1. Operativa básica

9.2.2.2.2.1.1. Introducción

Antes de entrar en harina, es preciso distinguir entre los conceptos de imagen y contenedor:

Imagen

Es una entidad inmutable constituida por una serie de archivos (que constituyen un sistema de archivos) y una metainformación que describe algunos aspectos de la virtualización.

Contenedor

Es la instanciación de una imagen para su uso. En el mundo de Docker una imagen es a un contenedor lo que una clase a un objeto en el mundo de la POO.

Así pues, una imagen es una plantilla con la que se crean uno o más contenedores, que serán aquellos dentro de los cuales correrá la aplicación que hayamos «dockerizado».

Además, es necesario entender que Docker dispone fundamentalmente de tres componentes:

Demonio

Que es la base de todo, ya que es el que se encarga de gestionar y construir imágenes y contenedores. Por tanto, es él el que sostiene toda la lógica del asunto. Por ejemplo, cuando se da la orden de crear y ejecutar un contenedor a partir de una imagen es este demonio el que toma la imagen de su repositorio local y genera el contenedor.

Cliente

Es la herramienta encargada de comunicarse con el demonio para transmitirle las órdenes que estimemos pertinentes. Puede encontrarse en la misma máquina que el demonio (y en ese caso se comunicará con éste a través del socket /var/run/docker.sock) o en una máquina remota para lo cual el demonio dispone una API REST a la cual se puede acceder por HTTP[1].

El cliente habitual es la órden CLI docker que usaremos en esta pequeña introducción, pero existen muchos otros como, por ejemplo, Portainer, que usa una interfaz web.

Registro

Es el servicio donde se almacenan imágenes que puede descargar el demonio en su repositorio local a fin de utilizarlas para generar contenedores. Por lo general, es Docker Hub, aunque puede establecerse otro distinto a través del uso de docker login.

../../../../../_images/componentes.png

Para disponer de Docker, podemos instalar la edición de la comunidad según las instrucciones oficiales o, simplemente, los paquetes disponibles en la propia Debian a partir de Buster:

# apt install docker.io
# docker version
Client:
 Version:           19.03.6
 API version:       1.40
 Go version:        go1.13.8
 Git commit:        369ce74
 Built:             Wed, 26 Feb 2020 11:20:11 +1100
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          19.03.6
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.8
  Git commit:       369ce74
  Built:            Wed Feb 26 00:20:11 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          19.03.6
  GitCommit:        7c1e88399ec0b0b077121d9d5ad97e647b11c870
 runc:
  Version:          1.0.0~rc10+dfsg1
  GitCommit:        1.0.0~rc10+dfsg1-1
 docker-init:
  Version:          0.18.0
  GitCommit:

El paquete instala en nuestro máquina tanto el demonio como el cliente oficial, de manera que éste último conecta con el primero a través del socket ya referido, y usará como registro Docker Hub. En este socket tiene permisos de escritura el grupo docker, por lo que cualquier usuario que pertenezca a este grupo podrá usar el cliente para manejar imágenes y contenedores. En consecuencia, si queremos que el usuario «pepe» sea capaz de manejar contenedores, podemos simplemento añadirlo a tal grupo (que la instalación de Debian crea automáticamente):

# adduser pepe docker

Nota

El demonio almacena imágenes y contenedores dentro de /var/lib/docker. Esto supone que dentro de ese directorio acabará habiendo una gran cantidad de datos, por lo que quizás puede que nos interese montar ese directorio en un sistema de archivos independiente.

9.2.2.2.2.1.2. Imágenes

Como ya se ha establecido, las imágenes son las plantillas a partir de las cuales se crean los contenedores. Hay tres métodos para obtenerlas:

  1. Construyéndolas nosotros mismos con docker build.

  2. Generando una a partir de un contenedor con docker commit.

  3. Obteniéndo las imágenes de un registro de imágenes. El registro existente más importante es Docker Hub.

Dejaremos el estudio de los dos primeros métodos al tratar la construcción de imáganes y bajo este epígrafe nos centraremos en cómo obtener imágenes de Docker Hub. Si nos registramos en el sitio tendremos la posibilidad de crear nuestros propios repositorios con imágenes creadas por nosotros mismos a través de las dos vías citadas anteriormente, pero sin necesidad de registro podemos usar los repositorios públicos que distintos usuarios y organizaciones mantienen en el sitio. Por ejemplo:

$ docker image pull debian

obtiene y almacena en nuestro repositorio local la imagen oficial de Debian:

$ docker image ls
REPOSITORY        TAG              IMAGE ID         CREATED       SIZE
debian            latest           971452c94376     4 weeks ago   114MB

Nota

Con el subcomando ls podemos usar un patron con comodines como los que se usan en la shell (p.e. docker image ls de*).

Como vemos ya tenemos descargada la imagen de Debian en nuestro repositorio local, lista para ser usada en la creación de contenedores. La imagen se ha obtenido de Docker Hub. Ahora bien, ¿cómo sabemos que existe esta imagen en el repositorio? La respuesta es obvia: buscando previamente en el registro. Y del mismo que, por ejemplo, existe apt search para buscar paquetes disponibles en Debian, existe docker search para buscar imágenes en Docker Hub:

aunque tenemos también la alternativa de visitar la web y hacer la búsqueda directamente en ella. Escogida cuál es la imagen que deseamos utilizar es importante tener presente qué significa la etiqueta (TAG) que acompaña al nombre. La etiqueta identifica distintas versiones del contenedor que pueden responder bien a distintas versiones del paquete que contienen, bien a qué es lo que realmente contienen. Por ejemplo, acabamos de instalar la imagen debian:latest (porque al no indicar etiqueta se sobrentiende que es la más reciente, o sea, la latest). Si investigamos la imagen avewriguaremos que esta imagen coincide con la que se hace para Buster que es la actual estable:

$ docker image pull debian:buster
$ docker image ls
REPOSITORY        TAG              IMAGE ID         CREATED       SIZE
debian            latest           971452c94376     4 weeks ago   114MB
debian            buster           971452c94376     4 weeks ago   114MB

Lo cual se hace evidente, porque no hemos tenido que esperar la descarga de la imagen y el identificador de la imagen es exactamente el mismo. Por supuesto también existe debian:bullseye o debian:stretch; pero también hay versiones slim que incluyen menos software y, por tanto, son más ligeras:

$ docker image pull debian:buster-slim
$ docker image ls *buster*
debian            buster-slim      2f14a0fb67b9     4 weeks ago   69.2MB
debian            buster           971452c94376     4 weeks ago   114MB

Como con las imágenes de Debian, sucede con otras muchas. Por ejemplo, PHP tiene sus propias imágenes, muchísimas en realidad porque varía en ellas desde la versión de PHP usada a cuál es la distribución base que han utilizado (Buster, Alpine) o qué software adicional se ha incluido dentro (p.e. el propio servidor Apache). Esta variadad de imágenes es bastante lógica si atendemos a que una de las principales funciones de Docker es proporcionar un método portable para distribuir aplicaciones y servicios.

Antes de continuar, es muy productivo parar un momento a analizar cómo funcionan las órdenes del cliente:

  • En cualquier punto podemos utilizar el argumento --help para conocer qué es lo que podemos añadir a continuación. Por ejemplo, para saber qué subcomandos podemos usar para manipular imágenes:

    $ docker image --help
    Usage:  docker image COMMAND
    
    Manage images
    
    Commands:
      build       Build an image from a Dockerfile
      history     Show the history of an image
      import      Import the contents from a tarball to create a filesystem image
      inspect     Display detailed information on one or more images
      load        Load an image from a tar archive or STDIN
      ls          List images
      prune       Remove unused images
      pull        Pull an image or a repository from a registry
      push        Push an image or a repository to a registry
      rm          Remove one or more images
      save        Save one or more images to a tar archive (streamed to STDOUT by default)
      tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
    

    De los expuestos hemos usado ya los subcomandos pull y ls, e incluso con estos hay distintas opciones que no hemos llegado a usar:

    $ docker image ls --help
    Usage:  docker image ls [OPTIONS] [REPOSITORY[:TAG]]
    
    List images
    
    Aliases:
      ls, images, list
    
    Options:
      -a, --all             Show all images (default hides intermediate images)
          --digests         Show digests
      -f, --filter filter   Filter output based on conditions provided
          --format string   Pretty-print images using a Go template
          --no-trunc        Don't truncate output
      -q, --quiet           Only show numeric IDs
    
  • El primer subcomando (o sea, el subcomando inmediatamente posterior a docker) indica sobre qué entidad se lleva a cabo la orden. En nuestro caso, estamos usando. image puesto que estamos actuando sobre imágenes. Sin embargo, esta no fue la filosofía inicial de la orden, por lo que se mantiene tambien una sintaxis antigua que no seguí esta filosofía. Por ese motivo:

    Sintaxis

    Sintaxis simplificada

    docker image pull

    docker pull

    docker image push

    docker push

    docker image rm

    docker rmi

    docker image build

    docker build

    El resto de entidades también tienen asociadas órdenes con sintaxis reducida.

Algunos otras órdenes sobre imágenes las trataremos al examinar cómo se construyen. Ahora, sin embargo, es interesante saber cómo eliminarlas:

$ docker image rm debian:buster-slim

para lo cual puede usarse el nombre (con la etiqueta) o el identificador. Es importante notar que para poder borrar una imagen no puede existir un contenedor que se haya generado con ella. Para fozar el borrado de la imagen y de todos los contenedores creados a partir de ella, puede usarse la opción -f. Relacionado con las opciones de borrado está:

$ docker image prune

que borra todas las imagenes antiguas de las que se ha descargado una versión más actualizada (son aquellas en cuya columna TAG aparece la etiqueta <Unused>) y que, además, no tienen contenedor asociado. Si lo que se quiere es borrar todas las imagenes sin contenedor asociado, aunque estén en su última versión, debe añadirse la opción -a:

$ docker image prune -a

Para consultar las características de la imagen podemos usar inspect:

$ docker image inspect debian:buster-slim

que las devuelve en formato JSON. Al inspeccionar puede interesarnos obtener sólo una parte de la información para lo cual puede usarse la opción -f:

$ docker image inspect -f '{{json .Config.Cmd}}' debian:buster-slim
['bash']
$ docker image inspect -f '{{json .RootFS}}' debian:buster-slim
{"Type":"layers","Layers":["sha256:f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da"]}

Nota

Al realizar una operación sobre una imagen (lo mismo ocurre con otras entidades) podemos referirnos a ella por su nombre o por su identificador único.

9.2.2.2.2.1.3. Contenedores

Una imagen es, simplemente, una plantilla para crear contenededores que ejecutan aplicaciones. Por ello, un contenedor se crea instanciando una imagen e indicando cuál es la aplicación que deseamos ejecutar. Por ejemplo:

$ docker run -ti debian:buster-slim bash
root@fd65c6309e43:/# echo "Estoy ejecutando bash en el contenedor"
Estoy ejecutando bash en el contenedor
root@fd65c6309e43:/# exit

Nota

No es necesario descargar la imagen previamente con docker image pull. Si la imagen que se invoca con docker run (o el docker create que veremos después) no existe en el repositorio local, se descargará del registro automáticamente.

docker run es el encargado de ello, aunque es la sintaxis simplificada de docker container run. En la consola de bash que se ejecuta dentro del contenedor, el sistema de archivos es una superposición de los archivos que proporciona la imagen y los archivos que puedan generarse durante la ejecución del contenedor[2].

En la orden hay tres argumentos posiciones:

  • -ti que en realidad son las opciones -t y -i y posibilta el uso interactivo, que es lo que realizamos a continuación. En contraposición -d se usa cuando el contenedor levanta un servicio y queremos que se libere la línea de órdenes del anfitrión.

  • La imagen de la que deseamos crear un contenedor.

  • Qué programa queremos arrancar con el contenedor. El que digamos que bash, posibilita que obtengamos una consola interactiva en la que podríamos haber llevado a cabo varias acciones, entre las cuales podría haberse encontrado instalar software adicional usando apt.

    Las imágenes, sin embargo, pueden predefinir un comando, de modo que si al crear un contenedor, no se especifica comando alguno, será el predefinido el que se ejecute. En este caso:

    $ docker image inspect -f '{{.Config.Cmd}}' debian:buster-slim
    [bash]
    

    Esa orden ya era bash, así que podríamos habernos ahorrado la expresión de la orden.

Es importante tener presente que el contenedor sólo tiene sentido como contenedor del programa que se pretende correr. Por ese motivo, el contenedor está en ejecución mientras dura nuestra sesión de bash y al salir de ella (como hemos hecho con exit), el contenedor se para. Por ese motivo, docker ps (o docker container ls):

$ docker ps

no devuelve contenedor alguno, a pesar de haberse creado. Esto es debido a que, en principio, sólo se muestran los contenedores en ejecución. En cambio, si añadimos la opción -a:

$ docker ps -a
CONTAINER ID   IMAGE               COMMAND  CREATED        STATUS                      PORTS  NAMES
fd65c6309e43   debian:buster-slim  "bash"   25 minutes ago Exited (0) 23 minutes ago          vigorous_greider

en donde podemos leer un resumen de las características del contenedor. También son interesantes las opciones:

  • -s, que muestra el tamaño del contenedor.

  • -l que se limita a mostrar el último contenedor arrancado con independencia de cuál sea su estado.

  • --filter que permite filtrar la salida del listado utilizando distintos criterios (consúltese la página del manual docker-container-ls).

Obsérvese que en el listado el identificador de la máquina coincide con el nombre de host y que aparece en el prompt de la sesión interactiva que abrimos anteriormente. Además, el demonio ha asignado un nombre generado aleatoriamente al contenedor (vigorous_greater). A partir de ahora, podremos referirnos al contenedor tanto usando su nombre como su identificador. Estas asignaciones de nombres automáticas podemos manipularlas, pero antes de intentar hacerlo, notemos que docker run no arranca sin más un contenedor, sino que lo crea y lo arranca. Si hubiéramos querido crearlo sin llegar a arrancarlo, podríamos haber usado docker container create o, simplemente, docker create:

$ docker create -ti debian:buster-slim
$ docker ps -a
CONTAINER ID   IMAGE               COMMAND  CREATED        STATUS                      PORTS  NAMES
c2e42d8b9b94   debian:buster-slim  "bash"   2 seconds ago  Created                            trusting_babbage
fd65c6309e43   debian:buster-slim  "bash"   25 minutes ago Exited (0) 23 minutes ago          vigorous_greider

que, como vemos, se ejecuta igual, aunque hemos obviado la orden de ejecución, porque ya sabemos que será bash.

Para arrancar un contenedor previamente parado, debemos usar docker start, (o docker container start) que retomará las opciones con las que creamos el contenedor:

$ docker start -i vigorous_greider
root@fd65c6309e43:/#

Ahora, en cambio, no acabamos la sesión de bash y consecuentemente el contenedor seguirá en ejecución. Con el contenedor en ejecución, podemos interactuar con el. Por ejemplo:

  • ejecutando algún comando adicional. Por ejemplo:

    $ docker exec fd65c6309e43 hostname
    fd65c6309e43
    
  • transfiriendo archivos:

    $ docker cp ~/texto.txt vigorous_greider:/tmp
    $ docker cp vigorous_greider:/tmp/texto.txt /tmp
    
  • parando el contenedor:

    $ docker stop vigorous_greider
    vigorous_greider
    

    que provocará la salida automática de la sesión interactiva de bash que dejamos pendiente.

  • parando y reiniciando el contenedor en la imisma operación:

    $ docker restart -t30 vigorous_greiter
    

    La opción -t pospone el número de segundos indicados la parada y reinicio. SI no la incluimos, la operación será inmediata.

Tanto para un contenedor parado como en funcionamiento, podemos revisar su registro cómodamente con docker-container-logs que utiliza una sintaxis semejante a la orden tail (podremos usar las opciones -n y -f)[3]:

$ docker logs -n5 portainer
2021/03/27 06:10:47 server: Reverse tunnelling enabled
2021/03/27 06:10:47 server: Fingerprint d7:44:c1:c2:08:59:1a:7e:e1:3c:f0:e1:33:8b:5a:8e
2021/03/27 06:10:47 server: Listening on 0.0.0.0:8000...
2021/03/27 06:10:47 Starting Portainer 1.23.2 on :9000
2021/03/27 06:10:47 [DEBUG] [chisel, monitoring] [check_interval_seconds: 10.000000] [message: starting tunnel management process]

Por otra parte, cuando se crea un contenedor podemos definir tanto el nombre del contenedor como el nombre de la máquina:

$ docker run --rm -ti --name=debiandock --hostname=debiandock debian:buster-slim
root@debiandock:/# echo "Este contenedor es efímero"
root@debiandock:/# exit

En este caso, además, hemos incluido la opción --rm que provoca que el contenedor se destruya en cuanto acabe la ejecución de la aplicaciói (bash en este caso).

Utilicemos una imagen distinta para ilustrar otras posibilidad al ejecutar un contenedor:

$ docker run --rm -d --name=nginx-test -p 8080:80 nginx:alpine
818ba206cb7158a9fe44c58649f9e47b39c11ede9a9fd9deb62174838f7c6420

En este caso, ejecutamos un contenedor oficial de nginx construido sobre Alpine (lo cual nos asegura que su tamaño es mínimo). Como el contenedor ejecuta un servidor web utilizamos -d para evitar que se quede ocupada la terminal del sistema anfitrión[4] y publicanmos el puerto 80 del contenedor en el 8080 del sistema anfitrión[5]. Si en estas condiciones, hacemos, por ejemplo:

$ wget -qS --spider http://localhost:8080
  HTTP/1.1 200 OK
  Server: nginx/1.17.9
  Date: Thu, 26 Mar 2020 18:42:48 GMT
  Content-Type: text/html
  Content-Length: 612
  Last-Modified: Tue, 03 Mar 2020 17:36:53 GMT
  Connection: keep-alive
  ETag: "5e5e95b5-264"
  Accept-Ranges: bytes

accederemos a la página que ofrezca el servidor web. Si nuestra intención es montar un sitio web con esta imagen tendremos, obviamente, que hacer algo más, pero ahora no es el momento. Podemos comprobar eso sí, el mapeo de puertos que hace este contenedor:

$ docker port nginx-test
80/tcp -> 0.0.0.0:8080

En realidad, la publicación del puerto 80 es posible porque al crear la imagen el autor expuso el puerto 80. No así el 443 (de hecho, la imagen no tiene ningún certificado), por lo que no puede publicarse tal puerto. Una alternativa es pedirle al demonio que publique todos los puertos que mandó exponer el autor en puertos libres escogidos aleatoriamente:

$ docker run --rm -d --name=nginx-test -P nginx:alpine
$ docker port nginx-test
80/tcp -> 0.0.0.0:32768

En este caso, es imperioso consultar el puerto para saber cómo conectar al servidor web. Finalmente, para eliminar un contenedor podemos simplemente usar docker rm o docker container rm:

$ docker rm nginx-test

Ahora bien, si el contenedor se encuentra arrancada, entonces será necesario o pararlo primero o forzar su borrado:

$ docker rm -f nginx-test

Nota

Un truco para eliminar todas las imágenes y contenedores es:

$ docker container rm -fv `docker container ls -aq`
$ docker image rm prune

donde además de -f hemos utilizado la opción -v para borrar también los volúmenes asociados a contenedores. Veremos este concepto en el próximo epígrafe.

Nota

Hay un aspecto muy importante de los contenedores que no hemos tratado: su política de ejecución. Dado que un contenedor de Docker se crea para la ejecución de una aplicación, hay que arrancar manualmente el contenedor para poner en ejecución la aplicación y, cuando la aplicación acaba, el contenedor se para. Esto hecho, sin embargo, puede ser que no nos interese, sobre todo si esa aplicación es un servidor. Por ejemplo, podría interesarnos que ante un falló que haga colapsar la máquina, el demonio de Docker reinicio el servidor, o bien, que ante un reinicio del propio demonio, también se reinicie el contenedor. Trataremos esto al ver los ejemplos finales.

Debe tenerse presente que al borrarse el contenedor, desaparecen todos los datos que pudiera contener.

9.2.2.2.2.1.4. Volúmenes

Un volumen es un directorio externo montado en el contenedor. Por ello, los datos que se almacenan en un volumen, aunque accesibles, no forman parte del contenedor y ni desaparecen al borrarse el contenedor ni están sujetos al control de almacenamiento que lleva a cabo el demonio. Son útiles para:

  • Hacer la información persistente más allá de la vida del contenedor.

  • Servir de almacenamiento cuando se está constantemente escribiendo, ya que los contenedores están pensados para que se escriban en ellos pocos datos. Por ejemplo, un volumen sería muy adecuado si quisiéramos almacenar los logs de un servicio.

  • Compartir datos entre el anfitrión y los contenedores.

Hay tres tipos de volúmenes

Volumen temporal

que es un espacio de memoria RAM que se monta como directorio para datos no persistentes en el contenedor (véase tmpfs).

Volumen de host (o bind mount)

es, simplemente, un directorio ya existente en el anfitrión que se monta sobre un contenedor para que ambos puedan compartir la información.

Volumen de datos (o, simplemente, volumen)

es un directorio creado expresamente para ser montado en los contenedores. Se gestionan mediante docker-volume, como ya veremos; y, obviamente, acaban resultando también directorios del anfitrión creados dentro de /var/lib/docker. La diferencia es que los volúmenes de host, aunque directorios del anfitrión, tienen existencia justificada al margen de Docker.

Para ilustrar el uso de los dos últimos podemos usar Portainer que es un contenedor que contiene un cliente web para el demonio de Docker. El cliente necesita dos cosas:

  • Por un lado, debe comunicarse con el demonio o lo que es lo mismo, acceder al socket /var/run/docker.sock del anfitrión. Esto podemos resolverlo con un volumen de host.

  • Por otro, la aplicación necesita manejar una base de datos y ello implica escrituras. Para hacerlas, se usa, en este caso, un volumen de datos creado para ese efecto.

Así pues, el uso de tal aplicación puede hacerse así:

$ docker volume create portainer_data
$ docker run -d -P --name=portainer --restart=unless-stopped \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v portainer_data:/data \
      portainer/portainer
5bab0fd1699832eb30310e4a5fb6ccd19b7e3171ead2df84e3426b67ef10b6cb
$ docker port portainer
9000/tcp -> 0.0.0.0:32768

donde primero se crea el volumen de datos y después se crea y ejecuta el contenedor utilizan este volumen y haciendo que el socket se comparta como volumen de host. La aplicación escucha en el puerto 9000 del container que se ha mapeado al 32768 del anfitrión. Listo. Desde el navegador, conectándonos a http://localhost:32768 podremos gestionar el demonio mediante una interfaz web.

Hay, no obstante, una precisión que hacer respecto a cómo se declaran los volúmenes. Ambos tipos se declaran mediante la opción -v y la sintaxis para establecer el volumen y el punto de montaje es la misma. La forma que tiene el demonio de distinguir un tipo de volumen de otro es la forma en la que expresamos el origen:

  • Si se declara una ruta obsoluta, se trata de un volumen de host.

  • Si se trata de un nombre. debe ser el nombre de un volumen de datos que, en caso de no existir, se creará. De lo que se deduce que podríamos habernos ahorrado la primera orden de creación.

  • Si sólo se expresa el punto de montaje (sin siquiera los dos puntos), se sobreentiende un volumen de datos al que se le asigna un nombre aleatorio. A estos volúmenes se les denomina volúmenes de datos anónimos.

Nota

Para declarar un volumen temporal, debe recurrirse a la opción --tmpfs.

Al crear volúmenes puede ser interesante usar la opción --label para añadir parejas clave-valor a los metadatos del volumen. que pueden ayudar luego como filtro al listarlos o borrarlos:

$ docker volume create --label service=http --label server=nginx htdocs
$ docker volume ls --filter label=server=nginx
DRIVER    VOLUME NAME
local     htdocs

Además de crear volúmenes, podemos llevar a cabo otras acciones básicas con ellos:

$ docker volume --help

Usage:  docker volume COMMAND

Manage volumes

Commands:
  create      Create a volume
  inspect     Display detailed information on one or more volumes
  ls          List volumes
  prune       Remove all unused local volumes
  rm          Remove one or more volumes

que no requieren demasiada explicación. Si es interesante, tener claro que al borrar un contenedor no se borran los volúmenes que se hayan podido crear como consecuencia de su creación. Existe, sin embargo, la opción -v para que al borrarse un contenedor se borren todos los volúmenes de datos anónimos asociados a él:

$ docker rm -v contenedor_con_volumenes_anonimos

Nota

La opción -m (o --mount) permite definir los tres tipos de volúmen (véase docker-run).

9.2.2.2.2.1.5. Redes

Docker dispone tres tipos fundamentales de controlador de red:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
201d5e901e39        bridge              bridge              local
377c54f52aa3        host                host                local
bed15cc78e24        none                null                local

para cada uno de los cuales hay ya creado un nombre de red. El significado de cada driver es el siguiente:

9.2.2.2.2.1.5.1. Tipos

bridge

Crea los contenedores dentro una red interna que se conecta con el exterior a través de una interfaz bridge en el anfitrión. Para la red homónima bridge ya definida esta interfaz es docker0:

$ ip link show dev docker0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:7f:40:be:40 brd ff:ff:ff:ff:ff:ff

Este tipo de red (que se corresponde con el tipo «Red NAT» de Virtualbox) es la predeterminada para los contenedores que se crean, por lo que todos los ejemplos que hemos estado mostrando la usaban:

$ docker network inspect -f '{{json .IPAM.Config}}' bridge
[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]
$ docker run --rm -ti alpine
/ # hostname -i
172.17.0.2
/ # ip route show
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link  src 172.17.0.2

Si crearamos otro contenedor, estaría también en la red bridge y recibiría una dirección IP de la misma red 172.17.0.0/16. Al estar todas estos contenedores en una red interna, la forma de hacer accesibles sus servicios es mediante la publicación de los puertos expuestos al crear el contenedor, como ya se ilustró al tratar Portainer. En aquella ocasión se usó la opción -P que escoge un puerto cualquiera del anfitrión. Puede también usarse la opción -p que permite indicar cuál será el puerto en concreto:

$ docker run --rm -d --name=nginx-test -p 8080:80 nginx:alpine

En este caso, el puerto expuesto 80 se publica en el 8080 de la máquina anfitrión.

Es posible crear otra red distinta con este mismo controlador de manera que todos los contenedoes asociados a esa red estarán dentro de ella y conectados entre sí, pero aislados de los contenedores asociados a la red predefinida. Lo trataremos más adelante.

host

Cuando se utiliza este contralador, el contenedor usa la misma red que el administrador. Por tanto, el contendor:

$ docker run --rm -ti --network host alpine

compartirá las interfaces con el anfitrión, por lo que cualquier servicio será directamente accesible. Así pues, se levantamos un servidor web en el contenedor, éste ocupará directamente el puerto 80 del anfitrión.

null

Simplemente, aisla al contenedor de la red. En este caso el contenedor sólo dispondrá de la interfaz de loopback.

Además de estos tres tipos de redes, existen otras. Una interesante en sistemas locales es la asociada al driver macvlan que permite incluir el contenedor en la misma red a la que pertenece el anfitrión, por lo que sería lo más aproximado al tipo «Adaptador puente» de Virtualbox. Ilustraremos su uso bajo el próximo epígrafe,

9.2.2.2.2.1.5.2. Gestión de redes

Las redes se pueden gestionar a través de docker network. Por ejemplo, podemos crear una nueva red de tipo bridge:

$ docker network create -d bridge bridge1
$ docker network inspect -f '{{json .IPAM.Config}}' bridge1
[{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]

Gracias a ello y al uso de --network al crear el contenedor, podremos incluir contenedores dentro de esta otra red llamada bridge1 que se comporta como la predefinida:

$ docker run --rm -ti --network bridge1 alpine
/ # hostname -i
172.18.0.2

Al crear la red no hemos especificado cuál es la red ni qué dirección hará de puerta de enlace. Estos datos, sin embargo, pueden especificarse a través de --subnet (la red), --ip-range (el rango de IPs dinámicas) o --gateway (la dirección de la puerta de enlace). Por ejemplo, si la red del anfitrión es 192.168.0.0/24. la puerta de enlace 192.168.0.1 y la interfaz física eth0, podemos usar el driver macvlan del siguiente modo:

$ docker network create -d macvlan -o parent=eth0
   --subnet 192.168.0.0/24 --ip-range=192.168.0.128/25 --gateway=192.168.0.1
   redreal
$ docker run --rm -ti --network redreal alpine
$ docker run --rm -ti --network redreal --ip 192.168.0.25 alpine

De asignar direcciones dinámicas a las interfaces de los contenedores se encarga el demonio, por lo que es conveniente escoger un rango que estimemos que no será concendido por el servidor DHCP de la red.

Nota

El driver macvlan se caracteriza por no poder comunicarse con la interfaz física a la que está asociada, por lo que en este caso el contenedor podrá comunicarse con el resto de la red, pero no con el anfitrión. Para evitar esta circunstancia, podría configurarse la dirección del anfitrión en otra interfaz macvlan, en vez de sobre êth0:

# The primary network interface
allow-hotplug enp1s0
iface enp1s0 inet manual
   up   ip link set dev $IFACE up
   down ip link set dev $IFACE down

auto host
iface host inet dhcp
   pre-up    ip link add link enp1s0 $IFACE type macvtap mode bridge
   post-down ip link del dev $IFACE

Es posible también conectar contenedores a dos o más redes bridge utilizando los subcomandos connect y disconnect:

$ docker create -ti alpine
$ docker network connect bridge1

De esta manera el contenedor tendrá una interfaz en la red bridge y otra en la red bridge1. Podríamos haber añadido la opción --ip tal como se hace al crear un contenedor con docker run, para especificar cuál es la IP fija que deseamos que se asigna al contenedor.

Como pueden crearse, pueden borrarse redes también:

$ docker network rm bridge1

Nota

Las redes definidas por el usuario, no tienen exactamente el mismo comportamiento que las redes preconstruidas. Por ejemplo:

  • Se puede desconectar en caliente una máquina de ellas.

  • Tienes integrado un servidor DNS, que permite la comunicación entre máquinas a través de sus respectivos nombres de máquina (el proporcionado mediante --hostname).

9.2.2.2.2.1.6. Limitaciones

Al crear un contenedor también pueden establecerse limitaciones en el uso de los recursos, en particular, al uso de la memoria y el procesador.

Nota

Es probable que al hacer docker info obtengamos el mensaje:

WARNING: Noswaplimitsupport

Si ese es el mensaje, es necesario añadir dos parámetros al arranque del núcleo, para lo cual debemos editar el archivo /etc/default/grub:

GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"

9.2.2.2.2.1.6.1. Memoria

Hay varias opciones que limitan la memoria:

Parámetro

Descripción

-m, –memory=numero[bkmg]

Establece un límite estricto al uso de memoria.

–memory-reservation=numero[bkmg]

Esteblece un límite suave al uso de memoria.

–memory-swap=numero[bkmg]

Establece un límite al uso de memoria swap.

Por ejemplo:

$ docker run --rm -d -m 256m --name=nginx nginx:alpine
$ docker container stats --no-stream --format '{{.MemUsage}}' nginx
4.945MiB / 256MiB

9.2.2.2.2.1.6.2. Procesador

Existen varias opciones relativas al uso del procesador. Por ejemplo:

$ docker run --rm -d --cpus=1 --name=nginx nginx:alpine

limitará el uso del contenedor a una CPU. Puede también especificarse qué CPU exactamente:

$  docker run --rm -d --cpuset-cpus=0,2 --name=nginx nginx:alpine

que limitará el uso al primer y tercer procesador.

Si se tienen varios contenedores simultáneamente, puede también repartirse el consumo máximo de CPU entre todos ellos mediante la opción --cpu-shares cuyo valor predeterminado es 1024:

$ cat /sys/fs/cgroup/cpu/docker/cpu.shares
1024

De esto modo, si arrancamos estos tres contenedores:

$  docker run --rm -d --cpuset-cpus=0 --name=nginx nginx:alpine
$  docker run --rm -d --cpuset-cpus=0 --cpu-share=512 --name=php php:alpine
$  docker run --rm -d --cpuset-cpus=0 --cpu-share=512 --name=mysql mysql:alpine

El primer contenedor podrá consumir hasta el 50% de la CPU y los otros dos hasta el 25%.

Notas al pie