Jose Hernández

Docker

Docker es una herramienta con la cual podemos crear contenedores con todo lo necesario para que nuestra aplicación se ejecute en cualquier entorno. Se puede decir que un contenedor es como una pequeña máquina virtual que contiene un sistema operativo, nuestra aplicación y el software que necesitamos para que funcione. Todo ello junto lo podemos desplegar en cualquier entorno gracias a docker.

Una de las ventajas que tiene esto, es que podemos tener en nuestra máquina de desarrollo contenedores exactamente iguales a los que se ejecutan en producción, por lo tanto podemos desarrollar sobre la misma plataforma en la que más tarde se ejecutará, evitando así los tipicos errores que no ocurren cuando estamo desarrollando, pero aparecen en producción.

Instalar Docker

Para instalar Docker tanto en OSX como en Windows instalaremos DockerToolbox. La instalación de DockerToolbox es mediante un asistente así que no tendría que haber ningún problema, pero por si acaso, aquí está la documentación para la instalación en OSX y la instalación en Windows.

Por su parte la instalación en Linux es también muy sencilla gracias a los gestores de paquetes con los que cuentan cada una de las distintas distribuciones, pero es posible que haya que realizar algunas configuraciones previas. Por ellos aquí dejo la instalación en Ubuntu, Red Hat, CentOS, Fedora y Debian. Si tú distribución no está entre estas, accede a la documentación de Docker y mira si está en la sección de instalación

Arrancar y parar un contenedor Docker

Una vez instalado Docker vamos a crear nuestro primer contenedor, para ello tenemos que tener arrancado el demonio de Docker. En sistemas Linux después de la instalación debería estar arrancado, de lo contrario ejecutaremos:

> docker daemon

Para OSX y Windows ejecutaremos Docker QuickStart Terminal, aplicación que se instala junto con DockerToolbox y nos arrancará el demonio de Docker y un terminal. Ahora escribimos lo siguiente en el terminal:

> docker run ubuntu /bin/echo 'Hello world'

Este comando se descargará la imagen de ubuntu (si previamente no la hemos descargado ya) arrancará el contenedor con la imagen, ejecutará el comando echo mostrando por pantalla Hello world y finalmente parará el contenedor.

Creemos ahora un contenedor con el que podamos interactuar, para ello escribiremos en el terminal el siguiente comando:

> docker run -ti ubuntu /bin/bash

Esta vez se arrancará el contenedor y se quedará esperando en un terminal para que podamos interactuar con él. Aquí podemos ejecutar los comandos que queramos para probar el contenedor y para salir escribiremos exit.

Hasta el momento hemos visto como poder poner en marcha un contenedor e interactuar con él. Pero lo que realmente es interesante es poder arrancar un contenedor y que una aplicación se quede ejecutandose en él. Para ello tenemos que indicarle al contenedor que se ejecute como un demonio. Vamos a ejecutar un script que escriba por pantalla la frase Hello world cada segundo mientras el contenedor esté en marcha. Para ello ejecutaremos el siguiente comando:

> docker run -d ubuntu /bin/sh -c "while true; do echo Hello world; sleep 1; done"

Como el contenedor se ejecuta en segundo plano el comando nos devuelve el identificador del contenedor, pero no nos muestra ninguna otra salida por pantalla. Para poder ver lo que está ejecutando el contenedor escribiremos el siguiente comando usando el identificador del contenedor:

> docker logs 1cfaa25a7a0eb254eca824c2e6c5e00a0c56d331087b7e240491c02857f9cc47

Este comando nos mostrará todos los mensajes que se envíen dentro del contenedor tanto a la salida estándar como a la salida de error. Para detener el contenedor podemos ejecutar los comando stop o kill:

> docker stop 1cfaa25a7a0eb254eca824c2e6c5e00a0c56d331087b7e240491c02857f9cc47

> docker kill 1cfaa25a7a0eb254eca824c2e6c5e00a0c56d331087b7e240491c02857f9cc47

La diferencia entre estos comando es que con stop primeramente se le envía la señal SIGTERM al proceso que se está ejecutando y a continuación la señal SIGKILL. Mientras que con el comando kill únicamente se envía la señal SIGKILL.

Crear un contenedor Docker

Hasta ahora simplemente hemos creado y arrancado contenedores básicos con únicamente un SO instalado, ubuntu. Pero lo normal es que nuestros contenedores tengan nuestras aplicaciones y todas las dependencias necesarias para que funcionen. Por ello tenemos que crear nuestro propio contenedor y adaptarlo a lo que nos interese.

Para el ejemplo vamos a crear un programa en Java que esté escuchando en el puerto 8081 y cuando reciba una petición conteste con la frase Hello World!. Además, escribirá en un fichero la dirección desde la que viene esa petición de forma que se quede como un registro y pueda ser consultada. El programa es bastante tonto pero nos ayudará a ver distintas opciones que podemos utilizar con docker.

Creamos un fichero al que llamamos HelloWorldServer.java y en él escribimos el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;



public class HelloWorldServer {


    public static void main(String args[]) {
        try {
            ServerSocket ss = new ServerSocket(8081);
            while (true) {
                Socket socket = ss.accept();
                socket.getOutputStream().write("Hello World!".getBytes());
                String message = "Connection from " + socket.getInetAddress()
                    .getHostAddress() + "\n";
                socket.close();
                
                FileOutputStream fos = new FileOutputStream("data/egistry.txt", true);
                fos.write(message.getBytes());
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Compilamos el fichero y obtenemos el fichero .class que es el que utilizaremos. Para compilarlo usaremos cualquier IDE que tengamos o directamente desde un terminal tecleando:

> javac HelloWorldServer.java

Con nuestro programa ya listo vamos a empezar a crear un contenedor. Para ello creamos un directorio nuevo en cualquier ubicación y en él copiamos el fichero HelloWorldServer.class que acabamos de generar. Además, crearemos un nuevo fichero al que llamaremos Dockerfile. En este último fichero es donde definiremos como va a ser nuestro contenedor y que contenido va a tener.

Comenzamos indicando a partir de que imagen queremos crearlo, es decir, cual es el contenido inicial que va a tener. En ocasiones utilizaremos una imagen simple que únicamente contenga el SO, pero en otras ocasiones nos interesará crear nuestro contenedor a partir de algún otro para añadirle las cosas que nos interese o para personalizarlo a nuestra medida. Para este ejemplo crearemos nuestro contenedor a partir de la imagen de ubuntu y para ello escribiremos en el fichero Dockerfile lo siguiente:

FROM ubuntu

Con la instrucción FROM indicamos que nuestra imagen base a partir de la que vamos a crear el contenedor es la viene indicada a continuación. El siguiente paso suele ser indicar quien es el encargado de mantener o el creador de este contenedor.

MAINTAINER Jose Hernandez

La instrucción MAINTAINER es opcional y después de ella podemos poner nuestro nombre e incluso nuestra dirección de email. Lo siguiente que vamos a hacer es actualizar el sistema para que tenga las últimas versiones de los programas base con lo que viene. Además, instalaremos el openjdk para poder ejecutar la aplicación que hemos creado.

RUN apt-get update -y && apt-get install openjdk-8-jdk -y

La instrucción RUN se encarga de ejecutar los comandos que indiquemos a continuación de él como si estuviéramos en un terminal. El siguiente paso será indicar que nuestro contenedor tiene un socker escuchando en el puerto 8081. Para ello lo indicaremos con la instrucción EXPOSE de la siguiente forma:

EXPOSE 8081

Gracias a esto el contenedor dejará ese puerto abierto para escuchar conexiones entrantes. Con estos pasos hemos terminado la configuración del sistema, ahora pasaremos a configurar nuestra aplicación para que funcione correctamente en el contenedor. Lo primero será añadir el fichero o ficheros de nuestra aplicación, en nuestro caso tendremos que añadir el fichero HelloWorldServer.class dentro del contenedor con la instrucción ADD

ADD HelloWorldServer.class /home/ubuntu/HelloWorldServer.class

Como anteriormente copiamos el fichero en el mismo directorio en el que tenemos nuestro fichero Dockerfile simplemente tenemos que indicar el fichero que queremos copiar y la ruta donde queremos dejarlo dentro del contenedor, en el ejemplo en /home/ubuntu

A continuación nos movemos al directorio de trabajo desde donde vamos a ejecutar la aplicación, como hemos mencionado antes /home/ubuntu con el comando WORKER:

WORKER /home/ubuntu

Nuestro programa guardaba un registro con las peticiones que recibía y la IP desde la que llegaban. Este registro estaba en un fichero que se genera dentro del directorio data. Por ello tenemos que crear dicho directorio y lo haremos utilizando de nuevo la instrucción RUN

RUN mkdir /home/ubuntu/data

Como al registro queremos poder acceder sin la necesidad de tener que conectarnos al contenedor, vamos a indicar que en la ruta /home/ubuntu/data se puede montar un volumen (un directorio en la máquina host que está ejecutando Docker) desde el cual tanto el contenedor, como un usuario de la máquina host pueda acceder a él y ver su contenido. Esto lo realizaremos con la instrucción VOLUME

VOLUME /home/ubuntu/data

Finalmente solo nos quedaría indicar que cuando arranque el contenedor se ejecute nuestra aplicación. Para ello utilizaremos la instrucción CMD.

CMD java HelloWordServer

Una vez visto paso a paso como hemos construido el fichero Dockerfile el resultado final es el siguiente:

FROM ubuntu
MAINTAINER Jose Hernandez

RUN apt-get update -y && apt-get install openjdk-8-jdk -y

EXPOSE 8081

ADD HelloWorldServer.class /home/ubuntu/HelloWorldServer.class

WORKDIR /home/ubuntu

RUN mkdir /home/ubuntu/data

VOLUME /home/ubuntu/data

CMD java HelloWordServer

Para poder ampliar la información sobre los ficheros Dockerfile o para ver todas las instrucciones que podemos usar a la hora de generar nuestro propio contenedor podemos darle un vistazo a la documentación.

Una vez terminado el fichero Dockerfile y con el fichero HelloWorldServer.class en el mismo directorio vamos a construir nuestro contenedor. Para ello desde un terminal nos movemos hasta dicho directorio y escribimos:

> docker build -t josehernandez/sample:0.1 .

Con la opción -t estamos indicando que queremos que el nombre del contenedor sea el que indicamos a continuación, en el ejemplo josehernandez/sample. Es recomendable poner el nombre de usuario o de empresa delante del nombre del contenedor para identificarlo correctamente así como si queremos publicarlo en Hub Docker. Además, también es recomendable ponerle un número de versión para poder evolucionar nuestro contenedor y siempre saber cual es el que tenemos arrancado.

Ejecutar nuestro contenedor Docker

Con el contenedor ya creado el siguiente paso es arrancarlo. Para ello ejecutaremos la siguiente instrucción en el terminal:

> docker run -d -p 8081:8081 -v /Users/Jose/docker-sample/data:/home/ubuntu/data  
    --name docker_sample josehernandez/sample:0.1

Una instrucción bastante larga, pero sencilla de entender. Empezamos utilizando la instrucción docker run, con el parámetro -d para indicar que el contenedor se ejecute en segundo plano y se mantenga arrancado. El parámetro -p 8081:8081 indica que queremos mapear en nuestro host el puerto 8081 con el puerto 8081 del contenedor. El primer puerto es el de nuestro host y el segundo puerto es el del contenedor. A continuación, el parámetro -v /Users/Jose/docker-sample/data:/home/ubuntu/data indica que queremos que el volumen del contenedor /home/ubuntu/data esté montado en nuestro host en la ruta /Users/Jose/docker-sample/data. El parámetro —name docker_sample indica que el contenedor se llamará docker_sample, si no pasamos este parámetro, Docker dará un nombre aleatorio al contenedor. Finalmente indicamos la imagen que queremos que se arranque, josehernandez/sample:0.1

Si ahora ejecutamos en un terminal la instrucción docker ps podremos ver que el contenedor está arrancado. Para comprobar que nuestra aplicación funciona correctamente dentro del contenedor, podemos abrir un navegador y en la dirección navegar hasta http://localhost:8081. Podremos ver como la respuesta es la frase Hello World!. Además si vamos hasta la ruta donde hemos montado el volumen veremos como se ha generado el fichero registry.txt y podemos ver su contenido.

Como podemos ver no ha sido complicado crear nuestro propio contenedor y poner nuestra aplicación a funcionar en él. Una recomendación de Docker es que cada contenedor únicamente tenga un proceso, es decir, que si nuestra aplicación hubiese usado una base de datos, la recomendación es crear un nuevo contenedor que solo tenga la base de datos y otro con la aplicación en vez de en un único contenedor tener las dos cosas.

Comandos Docker

Para finalizar dejo aquí la lista de comandos que se han utilizado y una breve explicación de lo que hacen.

run [opciones] nombre_imagen [comandos] [argumentos]
    opciones:
        -ti   -> Deja abierta la entrada STDIN para aceptar entrada desde el teclado.
        -d    -> Ejecuta el contenedor en segundo plano como un demonio.
        -p    -> Asociamos el puerto indicado de la máquina local donde se está 
                ejecutando docker, con el puerto indicado dentro del contenedor.
        -v    -> Indicamos como se van a montar los volúmenes que el contenedor pone 
                a nuestra disposición
        —name -> Le damos un nombre al contenedor para poder identificarlo

    comandos
        /bin/bash
        /bin/sh
        
stop [opciones] contenedor
    opciones:
        -t xx -> Tiempo en segundos entre que se envía SIGTERM y SIGKILL

kill [opciones] contenedor
    opciones:
        -s xx -> Señal que se enviará al contenedor, por defecto “KILL”

logs [opciones] contenedor
    opciones:
        -f           -> Van apareciendo sin parar los mensajes cada vez que ocurren
        --since=""   -> Se muestran logs desde el timestamp indicado
        -t           -> Muestra el timestamp en cada linea
        --tail="all" -> Muestra las lineas indicadas como parámetro desde el final 
            del log

build [opciones] path
    opciones:
        -t -> Le da un nombre al contenedor