Introducción a Docker

30 enero 2016 at 18:34 by Adrián Pérez

aaaaasdddd

He aprovechado las navidades para verme los dos primeros vídeos super didácticos que Docker tiene en su web. Como es un tema interesante, he decidido a hacerme un pequeño resumen.

1. Introducción a Docker

1.1. ¿Qué es docker?

"Docker es una plataforma para desarrollar, enviar y correr aplicaciones usando tecnología de virtualización de containers".

1.2. Ok... ¿Qué es un container?

"La virtualización basada en containers usa el kernel del sistema operativo del host para correr múltiples instancias guest" (llamadas containers). Cada container tiene su propio:

  • root filesystem
  • procesos
  • memoria
  • devices
  • puertos de red

Si estás pensando en que suena a una máquina virtual, en efecto, los containers serían la evolución de las máquinas virtuales, pues a diferencia de éstas, los containers no necesitan un sistema operativo guest instalado en cada máquina virtual, ni un hypervisor, con el consiguiente ahorro descomunal de recursos. Aquí una comparativa entre un servidor físico que aloja máquinas virtuales (izquierda) y otro que aloja containers (derecha) sacada directamente de los vídeos de Docker:

Containers_VS_VM

Así pues, los containers usan el kernel del sistema operativo para crear entornos aislados en los que correr una aplicación y todas las librerías de las que depende.

1.3. Conceptos y terminología Docker

La plataforma de Docker la conforman varias herramientas/componentes:

  • Docker engine: El demonio de Docker, el cual permite crear, enviar y correr containers. Usa varios de los namespaces del Kernel de Linux para crear los entornos aislados.
  • Docker client: al tratarse de una arquitectura cliente/servidor, el cliente se encarga de recoger los inputs del usuario y pasárselos al daemon, para que éste construya, corra o distribuya los containers. El cliente tiene una CLI (Command Line Interface) y una GUI (llamada "kitematic")
  • Imágenes (Docker images): templates de sólo lectura usados para crear containers. Puedes crear tus templates (y guardarlos en tu registro local) o usar los oficiales de Docker Hub.
  • Containers (Docker containers): plataforma que corre aplicaciones aisladas, conteniendo todo lo que ésta necesita (binarios, librerías, etc.). Creados a partir de una o más imágenes.
  • Registro: lugar donde almacenar tus imágenes (ej: Docker Hub)
  • Repositorio: dentro de un registro, podemos tener varios repositorios, cada uno alojando sus propias imágenes. Por ejemplo, un repo para cada SO.

2. Instalación de Docker

Una forma sencilla de instalar docker (y sus dependencias) en nuestra máquina, es la que nos propone el primer vídeo, que básicamente es hacer un wget de get.docker.com, el cual en realidad es un script que le pasamos vía pipe a "sh" para que lo ejecute. Así pues, símplemente con el siguiente comando haríamos toda la instalación:

[adri@localhost ~]$ wget -qO- https://get.docker.com/ | sh

Durante el proceso de instalación, se nos recomendará añadir nuestro usuario al grupo "docker" para usar docker con nuestro usuario no-root. Ésto lo haríamos así:

[adri@localhost ~]$ sudo usermod -aG docker adri

Necesitaremos cerrar sesión y volver a iniciarla para que este cambio tenga efecto. Tras ello, podremos iniciar docker de la forma habitual y pasar a testear la instalación con un "hello world".

[adri@localhost ~]$ service docker start
[adri@localhost ~]$ docker run hello-world

3. Trabajando con Docker. Containers e imágenes.

3.1. Docker images

Docker buscará en el host local por una imagen antes de ir a buscarla en un registro externo. Sólo descargará la imagen del registro externo si no ha podido encontrar una copia en local.

Podemos ver las imágenes que tenemos en local con el siguiente comando:

[adri@localhost ~]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
hello-world         latest              0a6ba66e537a        11 weeks ago        960 B

Las imágenes tienen un ID único, pero lo habitual es usar el repositorio y el tag (el tag por defecto es "latest") para identificar a una imagen.

docker_image

Una imagen de docker está formada por varias capas, donde cada capa es en realidad una imagen. La capa de nivel inferior, es la imagen base (en el ejemplo la del SO). Todas las imagenes, como ya hemos visto, son de sólo lectura. Cuando lanzas un container a partir de una imagen, docker añade una nueva capa encima del todo, con un sistema de ficheros de r/w. Es en esta capa r/w que añade el container donde corre el proceso que hemos indicado como "command" en el "docker run" (mira la sección "docker run" un poco más abajo), así como donde se realiza cualquier cambio que hagas. Así si modificas el fichero de configuración de Apache, lo que habrá pasado es que se habrá copiado este fichero de la capa (imagen) de Apache a la capa de r/w del container, y estarás modificando esta copia en la capa de r/w, quedando escondida la original de la capa de Apache.

Podemos hacer los cambios permanentes con el comando "docker commit" o mediante las "dockerfiles", ambas explicadas más adelante.

Por otro lado, tal y como explican aquí, podemos eliminar una imagen con el comando "docker rmi", aunque las imágenes funcionan un poco como los inodos en Linux, es decir, vas eliminando referencias hasta que eliminas la imagen cuando ya no hay más referencias a ella.

 

3.2. Crear un container ("docker run")

El comando "docker run" creará el container usando la imagen que le especifiquemos, y seguidamente iniciará el container. La sintaxis es la siguiente:

docker run [options] [image] [command] [args]

Podemos probar a ejecutar un "echo" en un container que usará el tag "latest" de la imagen oficial de Fedora, ejecutando:

[adri@localhost ~]$ docker run fedora:latest echo "hello world"
Unable to find image 'fedora:latest' locally
latest: Pulling from library/fedora
369aca82a5c0: Pull complete 
3fc68076e184: Pull complete 
Digest: sha256:7d916d5d3ab2d92147e515d3c09cb47a0431e2fff9d550fd9bcc4fed379af9ea
Status: Downloaded newer image for fedora:latest
hello world

Como vemos, lo primero que prueba es a buscar la imagen "fedora:latest" en el registro local. Tras comprobar que no la tenemos, se la descarga y finalmente ejecuta el comando "echo" con el parámetro "hello world", lo cual devuelve... sí, un "hello world".

Dos anotaciones importantes:

El container se ejecutará mientras el proceso especificado como "command" al hacer el "docker run" esté corriendo. Si el proceso se para o acaba, el container se parará.

El PID de este proceso (pasado como "command" en el "docker run") siempre es el 1 dentro del container.

3.2.1. Opción "-it": Usando el terminal del container
Podemos lanzar un container con las opciones -i (para conectar con el STDIN del container) y -t (para tener un pseudo-terminal), para especificar el shell que usaremos en la pseudo-terminal. De esta manera, abriremos una sesión en la terminal con el container, y podremos ejecutar comandos en el propio container de la forma habitual (instalar paquetes adicionales, moverse por el sistema de ficheros, etc.).

[adri@localhost ~]$ docker run -it fedora:latest /bin/bash
[root@6548a8a5e169 /]# cat /etc/fedora-release
Fedora release 23 (Twenty Three)
[root@6548a8a5e169 /]# exit
exit
[adri@localhost ~]$

Cuando salimos del pseudo-terminal vía "exit", también salimos del container, y al parar el proceso "/bin/bash" que hemos pasado como comando, también se parará el propio container. Esto significa que cualquier cambio que hayamos hecho en el container, no estará si volvemos a lanzar el container, pues el "docker run" nos creará un nuevo container. Podemos salir del pseudo-terminal sin parar el container, pulsando "Ctrl+P+Q".

Si salimos de un container con Ctrl+P+Q, podremos ver el container con el comando "docker ps" (con la opción -a para listar también los containers ya parados). Con "docker stop <container-short-id>" pararemos ese container. "docker start <container-short-id>" hará lo propio.

[adri@localhost ~]$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
787c6040f4ef        fedora:latest       "/bin/bash"         21 minutes ago      Up 21 minutes                           fervent_goldstine
[adri@localhost ~]$ docker stop 787c6040f4ef
787c6040f4ef
[adri@localhost ~]$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[adri@localhost ~]$

3.2.2. Opciones "-d" y  "-P": Docker en background y mapeo de puertos
La forma habitual de lanzar un docker container, es con el parámetro "-d" (dettached) que ejecutará el container en background, mientras el proceso especificado como "command" esté corriendo. Durante este tiempo, podremos ejecutar "docker logs <container-short-id>" (opción "-f" para ir actualizando) para ver la salida que se esté produciendo por la STDOUT del container.

La opción "-P" hará el mapeo de puertos entre el host y el container, para poder publicar y hacer accesibles los servicios del container.

[adri@localhost ~]$ docker run -d -P tomcat:7

El comando anterior, lanzará el container que usa la imagen de Tomcat 7 en background, haciendo el mapeo de puertos. Al no especificar ni comando ni argumentos, el container lanzará el comando que la imagen tiene por defecto (tomcat en el ejemplo). Podemos ver los detalles, incluyendo el mapeo de puertos, con "docker ps":

[adri@localhost ~]$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                     NAMES
85bc98e8ec94        tomcat:7            "catalina.sh run"   23 seconds ago      Up 18 seconds       0.0.0.0:32768-&gt;8080/tcp   adoring_austin

En el ejemplo, el puerto 32768 del host redirigirá al 8080 del container.

Más sobre networking en el apartado 5 de este post.

3.3. Guardar los cambios ("docker commit")

Este comando permite salvar los cambios realizados en un container como una nueva imagen. La sintaxis es:

docker commit [options] [container ID] [repository:tag]

La idea es ejecutar un container con la pseudo-terminal, realizar cambios, salir (con lo que se parará el container) y hacer el commit.

[adri@localhost ~]$ docker run -it fedora bash
[root@20609c501150 /]# man
bash: man: command not found
[root@20609c501150 /]# dnf install man
...
[root@20609c501150 /]# man man
[root@20609c501150 /]# exit
[adri@localhost ~]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                     PORTS               NAMES
20609c501150        fedora              "bash"                 8 minutes ago       Exited (0) 5 seconds ago                       furious_brattain
...
[adri@localhost ~]$ docker commit 20609c501150 adri/man:1.0.0
2a98a1fe4b9570c84ea4d93e5fc962c598c2e5a74977059ff9cc8f2694235934
[adri@localhost ~]$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
adri/man            1.0.0               2a98a1fe4b95        20 seconds ago      357.6 MB
3.4. Dockerfile

"Dockerfile es un fichero de configuración que contiene las instrucciones necesarias para crear una Docker image." Cuenta con varios tipos de instrucciones pero las básicas son:

  • FROM: especifica la imagen base
  • RUN: define el comando a ejecutar sobre la imagen base. Este comando, que puede ser cualquier comando que usariamos en nuestro terminal, se ejecuta en la capa r/w de nivel superior que añade el container, seguido de un commit en la imagen. Puedes ejecutar dos o más comandos en un único RUN separándolos con "&&".
  • CMD: sólo puede especificarse una única vez en el Dockerfile, y sirve para definir el comando por defecto (es decir, sólo si no se especifica ningún comando) a ejecutar si se ejecuta (docker run) un container a partir de la imagen que se ha creado con esta Dockerfile.
  • ENTRYPOINT: igual que CMD, sólo puede especificarse una única vez en el Dockerfile. Define el comando que se ejecutará al hacer el "docker run" (por ejemplo, "ping"). En este caso, el comando y argumentos del docker run, serán los argumentos del comando definido en el entrypoint (por ejemplo, "127.0.0.1"), pues al contrario que CMD, el ENTRYPOINT se ejecutará siempre.
  • VOLUME: permite especificar uno o más directorios que se crearán en el container como "volúmenes" (hablamos de ellos más adelante, en el punto 4.
  • EXPOSE: define los puertos que el container expondrá, para poderse mapear desde el host con tal de publicar los servicios del container al exterior.
3.4.1. Generar una docker image ("docker build")

El comando "docker build" genera una "Docker Image" a partir de un "dockerfile". La sintaxis es la siguiente:

docker build [options] [path]

El "path" conocido como "Build Context", no es más que el directorio donde se encuentra cualquier fichero al que se hace referencia en la Dockerfile, incluido el propio fichero de dockerfile. Al hacer el build, se genera un tar con el contenido del directorio indicado en "path" que se envía al demonio de docker. Ejemplo:

docker build -t adri/testimage:1.0 /path/to/context/

En /path/to/context tendremos nuestro fichero dockerfile, al que llamaremos por defecto "Dockerfile". Es interesante saber que cada "RUN" crea una nueva "capa de imagen" que ejecuta un container intermedio y temporal. Al acabar el RUN, commitea los cambios, y el siguiente RUN se ejecutará en un nuevo container usando la imagen creada con el RUN previo. Recordemos que podemos usar "&&" para ejecutar varios comandos en el mismo RUN, evitándo así la creación de los containers intermedios.

NOTA: Si volvemos a hacer un build, los comandos RUN que no hayan cambiado, se ejecutarán en un santiamén, pues tendrá cacheada la "capa de imagen" para ese RUN.

3.5. Resumen de comandos para gestionar imágenes y containers

Lista todos los containers

docker ps -a

Inicia/para ese container

docker start/stop [container ID]

Elimina un container. Sólo se pueden eliminar containers parados.

docker rm [container ID]

Lista las imágenes de nuestro registro local

docker images

Elimina una imagen. Si una imagen se ha tageado varias veces, hemos de eliminar los tags uno a uno

docker rmi [repo:tag]

Sube las imágenes de nuestro repo:tag local, al repo:tag que hemos creado en Docker Hub.

docker push [repo:tag]

Crea en local, una copia de la imagen renombrando el repo:tag.

docker tag [repoA:tag1] [repoB:tag2]

3.5.1. Usando "docker exec"

Docker exec inicia otro proceso (además del proceso principal) dentro del container. Este comando es útil para tener acceso vía terminal a un container que está corriendo. Al hacer exit, no se para el container, pues el /bin/bash no es el proceso con ID 1.

docker exec -i -t [container ID] /bin/bash

4. Trabajando con Docker. Volúmenes

Un volúmen en Docker es un directorio del container donde almacenar datos que queremos que persistan, incluso si el container se para o se borra. Este directorio nos permitirá compartir ficheros entre containers, además de permitir mapearse en un directorio del host.

Indicaremos el volumen a crear y/o el directorio host a mapear con la opción "-v".

docker run -v /host/src:/myvolume nginx:1.7
docker run -v /myvolume nginx:1.7

Es importante anotar que los cambios en los volúmenes no se incluyen al crear una imagen a partir del container que tiene el volumen.

5. Trabajando con Docker. Networking

5.1. Mapeo de puertos

Con la opción "-p" (o "-P") mapearemos un puerto del host a un puerto del container, para poder hacerlo accesible desde el exterior.

docker run -d -p 8080:80 nginx:1.7

Si no especificamos ningún puerto a la opción "-p", se auto-mapearán los puertos definidos en la instrucción "EXPOSE" de la dockerfile.

5.2. Enlazar containers

Con Docker, contamos con la opción de enlazar dos containers, para que puedan comunicarse entre sí, sin necesidad de publicar o exponer al exterior ningún puerto.

Una vez tengamos corriendo un container, podremos lanzar otro container con la opción "--link", para indicar el nombre:tag del primer container, el cual será el origen de los datos para este segundo.

// Lanzamos el primer container que será el origen de los datos
docker run -d --name database postgres
// Lanzamos el segundo container, receptor de los datos del primero
docker run -d -P --name website --link database:db nginx

En el ejemplo, "--link database:db", indica que "database" es el nombre del primer container, mientras que "db" es un alias que usará el segundo container para referirse al primero. Al usar alias, se habrá creado una entrada en el /etc/hosts del container, con la referencia al primer container.

6. Integración Continua con Docker

El segundo vídeo, habla de varias estrategias para usar Docker en nuestro proceso de Integración Continua.

6.1. Docker image

La primera propuesta, no es más que crear una docker image tras compilar y testear nuestro código, en el servidor de Integración Continua, para posteriormente publicarla en nuestro registro de Docker Hub.

docker

6.2. Docker Hub auto build

En esta segunda propuesta, es el propio Docker Hub que detecta un cambio en el repositorio de código (github  o bitbucket en el diagrama). Tras ello, monta la imagen y el container a partir de ella. La compilación del código y los testeos se han de ejecutar una vez el container está creado.

docker

Fuentes:

https://training.docker.com/self-paced-training

http://prakhar.me/docker-curriculum/