Cómo y por qué utilizar Docker

¿Cómo se crea la infraestructura de proyectos, se compilan y ejecutan servicios? Hablamos sobre el aislamiento de procesos en un sistema operativo sin virtualización pesada.
Docker es un programa que permite a un sistema operativo ejecutar procesos en un entorno aislado basado en imágenes especialmente creadas. A pesar de que las tecnologías en las que se basa Docker existían antes que él, Docker revolucionó la forma en que se crea la infraestructura de proyectos, se compilan y ejecutan servicios en la actualidad.

(En este artículo se omiten muchos detalles a propósito para no sobrecargar con información que no es necesaria para la comprensión).
Instalación
Para comenzar a utilizar Docker, es necesario instalar el motor: Docker Engine. En la página https://docs.docker.com/engine/install/ se encuentran disponibles enlaces de descarga para todas las plataformas populares. Elige la tuya e instala Docker.

Al trabajar con Docker, hay un detalle importante que debes conocer al instalarlo en Mac y Linux. Por defecto, Docker funciona a través de un socket Unix. Por motivos de seguridad, el socket está cerrado para los usuarios que no pertenecen al grupo docker. Aunque el instalador agrega automáticamente al usuario actual a este grupo, Docker no funcionará de inmediato. Esto se debe a que si el usuario cambia el grupo por sí mismo, nada cambiará hasta que el usuario vuelva a iniciar sesión. Esta es una característica del kernel. Para verificar a qué grupos pertenece tu usuario, puedes ejecutar el comando id.

Puedes verificar el éxito de la instalación con el comando docker info:

$ docker info
Containers: 22
 Running: 2
 Paused: 0
 Stopped: 20
Images: 72
Server Version: 17.12.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
...
Ejecución
En esta etapa, los comandos se ejecutan "tal cual" sin explicar los detalles. Más adelante se explicará cómo se forman y qué incluyen.

Comencemos con la opción más simple:

$ docker run -it nginx bash
root@a6c26812d23b:/#
En la primera ejecución, este comando comenzará a descargar la imagen nginx, por lo que tendrás que esperar un poco. Después de que se descargue la imagen, se ejecutará bash y te encontrarás dentro del contenedor (container).

Explora el sistema de archivos, echa un vistazo al directorio /etc/nginx. Como puedes ver, su contenido no coincide con el que tienes en tu computadora. Este sistema de archivos proviene de la imagen nginx. Cualquier cosa que hagas aquí dentro no afectará tu sistema de archivos principal. Puedes salir del contenedor con el comando exit.

Ahora veamos un ejemplo de ejecución del comando cat en otro contenedor, pero también iniciado desde la imagen nginx:

$ docker run nginx cat /etc/nginx/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
...
$
El comando se ejecuta casi instantáneamente, ya que la imagen ya está descargada. A diferencia del inicio anterior, donde se ejecuta bash y se inicia una sesión interactiva dentro del contenedor, la ejecución del comando cat /etc/nginx/nginx.conf para la imagen nginx mostrará el contenido del archivo especificado (tomado del sistema de archivos del contenedor en ejecución) y devolverá el control al lugar donde estaba. No estará dentro del contenedor.

El último método de inicio será el siguiente:

# Ten en cuenta que después del nombre de la imagen no se especifica ningún comando.
# Este enfoque funciona si el comando de inicio está especificado en la propia imagen
$ docker run -p 8080:80 nginx
Este comando no devuelve el control, ya que inicia nginx. Abre tu navegador y escribe localhost:8080. Verás cómo se carga la página Welcome to nginx!. Si en este momento vuelves a mirar la consola donde se inició el contenedor, verás que se muestra el registro de solicitudes a localhost:8080. Puedes detener nginx con el comando Ctrl + C.

A pesar de que todas las ejecuciones se realizaron de manera diferente y dieron diferentes resultados, el proceso general es el mismo. Docker descarga automáticamente la imagen (el primer argumento después de docker run) y, en base a ella, inicia un contenedor con el comando especificado.

Una imagen es un sistema de archivos independiente. Por ahora, estamos utilizando imágenes predefinidas, pero luego aprenderemos a crear las nuestras propias.

Un contenedor es un proceso en ejecución del sistema operativo en un entorno aislado con un sistema de archivos conectado desde la imagen.

Reitero que un contenedor es simplemente un proceso normal de tu sistema operativo. La diferencia es que, gracias a las capacidades del kernel (que se explicarán al final), Docker inicia el proceso en un entorno aislado. El contenedor ve su propia lista de procesos, su propia red, su propio sistema de archivos, etc. A menos que se le indique explícitamente, no puede interactuar con tu sistema operativo principal y todo lo que contiene o se ejecuta en él.

Intenta ejecutar el comando docker run -it ubuntu bash y escribe ps auxf dentro del contenedor en ejecución. La salida será la siguiente:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.1  18240  3300 pts/0    Ss   15:39   0:00 /bin/bash
root        12  0.0  0.1  34424  2808 pts/0    R+   15:40   0:00 ps aux
Como puedes ver, solo hay dos procesos y el PID de Bash es 1. También puedes ver el directorio /home con el comando ls /home y asegurarte de que está vacío. También ten en cuenta que el usuario predeterminado dentro del contenedor es root.
¿Para qué es todo esto?
Docker es una forma universal de entregar aplicaciones a máquinas (computadoras locales o servidores remotos) y ejecutarlas en un entorno aislado.

Recuerdas cuando tuviste que compilar programas a partir de código fuente. Este proceso incluye los siguientes pasos:
  • Instalar todas las dependencias necesarias para tu sistema operativo (debes encontrar la lista de ellas primero).
  • Descargar el archivo comprimido y descomprimirlo.
  • Ejecutar la configuración make configure.
  • Ejecutar la compilación make compile.
  • Instalar make install.
Como puedes ver, el proceso no es trivial y a menudo no es rápido. A veces incluso es imposible debido a errores inexplicables. Y eso sin mencionar la contaminación del sistema operativo.

Docker simplifica este procedimiento a la ejecución de un solo comando con casi un 100% de garantía de éxito. Echa un vistazo al ejemplo ficticio en el que se instala el programa Tunnel en tu computadora local en el directorio /usr/local/bin utilizando la imagen tunnel:

docker run -v /usr/local/bin:/out tunnel
Ejecutar este comando hará que el archivo ejecutable del programa, que se encuentra dentro de la imagen tunnel, aparezca en el sistema de archivos principal en el directorio /usr/local/bin. Ahora puedes iniciar el programa simplemente escribiendo tunnel en la terminal.

¿Y si el programa que instalamos de esta manera tiene dependencias? El truco está en que la imagen en la que se ejecuta ya tiene todas las dependencias instaladas, y su inicio garantiza casi un 100% de funcionalidad independientemente del estado del sistema operativo principal.

A menudo, ni siquiera es necesario copiar el programa del contenedor a tu sistema operativo principal. Es suficiente con ejecutar el propio contenedor cuando sea necesario. Supongamos que decidimos desarrollar un sitio estático basado en Jekyll. Jekyll es un generador de sitios estáticos popular escrito en Ruby. Por ejemplo, el tutorial que estás leyendo ahora mismo se encuentra en un sitio estático generado con Jekyll. Y al generarlo, se utilizó Docker (puedes leer más sobre esto en el tutorial: cómo crear un blog con Jekyll).

El antiguo método de uso de Jekyll requería instalar Ruby y Jekyll como una gema (gem es el nombre de los paquetes en Ruby) en tu sistema operativo principal. Y, como siempre en tales casos, Jekyll solo funciona con versiones específicas de Ruby, lo que complica la configuración.

Con Docker, el inicio de Jekyll se reduce a un solo comando que se ejecuta en el directorio del blog (puedes encontrar más detalles en el repositorio de nuestros tutoriales):

docker run --rm --volume="$PWD:/srv/jekyll" -it jekyll/jekyll jekyll server
Así es como se inicia Jekyll en la práctica. Cuanto más lejos, más se utiliza este enfoque. En este punto, puedes sumergirte un poco en el origen del nombre Docker.
Como sabes, el principal medio de transporte de mercancías en todo el mundo son los barcos. Antes, el costo del transporte era bastante alto debido a que cada carga tenía su propia forma y tipo de material.
Cargar un saco de pescado o un automóvil en un barco son tareas diferentes que requieren diferentes procesos e instrumentos. Había problemas con los métodos de carga, se requerían diferentes grúas e instrumentos. Y empaquetar eficientemente la carga en el barco, teniendo en cuenta su fragilidad, era una tarea no trivial.

Pero en algún momento todo cambió. Una imagen vale más que mil palabras:
Los contenedores igualaron todos los tipos de carga y estandarizaron los instrumentos de carga y descarga en todo el mundo. Lo que a su vez llevó a la simplificación de los procesos, la aceleración y, por lo tanto, la reducción de los costos de transporte.

Lo mismo sucedió en el desarrollo de software. Docker se convirtió en un medio universal de entrega de software independientemente de su estructura, dependencias y método de instalación. Todo lo que necesitan los programas distribuidos a través de Docker se encuentra dentro de la imagen y no se cruza con el sistema operativo principal y todo lo que contiene o se ejecuta en él.

Lo más impactante de Docker fue su influencia en la infraestructura del servidor. Antes de la era de Docker, la gestión de servidores era un proceso muy doloroso, incluso a pesar de contar con programas de gestión de configuraciones como Chef, Puppet y Ansible. La razón principal de todos los problemas era el estado mutable. Las aplicaciones se instalaban, actualizaban y eliminaban, y esto ocurría en diferentes momentos en diferentes servidores y de manera ligeramente diferente. Por ejemplo, actualizar la versión de lenguajes como PHP, Ruby o Python podría convertirse en toda una aventura con la posibilidad de perder la funcionalidad. Era más sencillo configurar un nuevo servidor y migrar hacia él. En esencia, Docker permite realizar este tipo de migración. Olvidar lo antiguo y establecer lo nuevo, ya que cada contenedor en ejecución opera en su propio entorno. Además, realizar un retroceso en tal sistema es trivial: todo lo que se necesita es detener el contenedor nuevo y levantar el antiguo, basado en la imagen anterior.
Aplicación en un contenedor
Ahora hablemos de cómo se representa una aplicación en contenedores. Hay dos enfoques posibles:
  • 1
    Toda la aplicación en un solo contenedor, dentro del cual se ejecuta un árbol de procesos: la aplicación, el servidor web, la base de datos y así sucesivamente.
  • 2
    Cada contenedor en ejecución es un servicio atómico. En otras palabras, cada contenedor representa exactamente un programa, ya sea un servidor web o una aplicación.
En la práctica, todas las ventajas de Docker se logran solo con el segundo enfoque. En primer lugar, los servicios suelen estar distribuidos en diferentes máquinas y a menudo se mueven entre ellas (por ejemplo, en caso de fallo de un servidor). En segundo lugar, la actualización de un servicio no debe resultar en la interrupción de los demás.

El primer enfoque rara vez es necesario. Por ejemplo, Hexlet opera en dos modos. El sitio en sí con sus servicios utiliza el segundo modelo, donde cada servicio es independiente. Sin embargo, la práctica que se realiza en el navegador se inicia siguiendo el principio de "un usuario, un contenedor". Dentro del contenedor, puede haber cualquier cosa dependiendo de la práctica. Como mínimo, siempre se inicia el entorno Hexlet IDE en ese contenedor, y a su vez, crea terminales (procesos). En el curso de bases de datos, se inicia una base de datos en el mismo contenedor, y en el curso relacionado con la web, se inicia un servidor web. Este enfoque permite crear la ilusión de trabajar en una máquina real y reduce drásticamente la complejidad en el soporte de ejercicios. Repito que esta forma de uso es muy específica y es probable que no la necesite.

Otro aspecto importante al trabajar con contenedores está relacionado con el estado. Por ejemplo, si una base de datos se inicia en un contenedor, sus datos nunca deben almacenarse en el mismo contenedor. Un contenedor es un proceso del sistema operativo que puede eliminarse fácilmente; su existencia es siempre temporal. Docker tiene mecanismos para almacenar y utilizar datos en el sistema de archivos principal. Hablaré de eso más adelante.
Trabajo con imágenes
Docker es más que una simple aplicación. Es todo un ecosistema con una variedad de proyectos y servicios. El servicio principal con el que tendrás que lidiar es el Registro. Es el repositorio de imágenes.

Conceptualmente, funciona de la misma manera que un repositorio de paquetes en cualquier gestor de paquetes. Puedes ver su contenido en el sitio web https://hub.docker.com/ haciendo clic en el enlace "Explorar".

Cuando ejecutas el comando docker run , Docker verifica si la imagen especificada está en la máquina local y la descarga si es necesario. Puedes ver la lista de imágenes que ya se han descargado en tu computadora con el comando 'docker images':

$ docker images
REPOSITORY                           TAG                 IMAGE ID            CREATED             SIZE
workshopdevops_web                   latest              cfd7771b4b3a        2 days ago          817MB
hexletbasics_app                     latest              8e34a5f631ea        2 days ago          1.3GB
mokevnin/rails                       latest              96487c602a9b        2 days ago          743MB
ubuntu                               latest              2a4cca5ac898        3 days ago          111MB
Ruby                                 2.4                 713da53688a6        3 weeks ago         687MB
Ruby                                 2.5                 4c7885e3f2bb        3 weeks ago         881MB
nginx                                latest              3f8a4339aadd        3 weeks ago         108MB
elixir                               latest              93617745963c        4 weeks ago         889MB
postgres                             latest              ec61d13c8566        5 weeks ago         287MB
Entenderemos cómo se forma el nombre de la imagen y qué incluye.

La segunda columna en la salida anterior se llama ETIQUETA (TAG). Cuando ejecutamos el comando docker run nginx, en realidad se ejecuta el comando docker run nginx:latest. Esto significa que no solo descargamos la imagen nginx, sino una versión específica de ella. "Latest" es la etiqueta por defecto. No es difícil deducir que significa la última versión de la imagen.

Es importante entender que esto es solo un acuerdo, no una regla. Una imagen específica puede no tener la etiqueta "latest", o incluso si la tiene, no contendrá los cambios más recientes simplemente porque nadie los ha publicado. Sin embargo, las imágenes populares siguen este acuerdo. Como se entiende en el contexto, las etiquetas en Docker son modificables, es decir, nadie garantiza que al descargar la imagen con la misma etiqueta en diferentes computadoras en momentos diferentes obtendrás lo mismo. Este enfoque puede parecer extraño y poco confiable, ya que no hay garantías, pero en la práctica, existen ciertos acuerdos que siguen todas las imágenes populares. La etiqueta "latest" realmente siempre contiene la última versión y se actualiza constantemente, pero además de esta etiqueta, se utiliza activamente la versión semántica. Echaremos un vistazo a https://hub.docker.com/_/nginx.

1.13.8, mainline, 1, 1.13, latest
1.13.8-perl, mainline-perl, 1-perl, 1.13-perl, perl
1.13.8-alpine, mainline-alpine, 1-alpine, 1.13-alpine, alpine
1.13.8-alpine-perl, mainline-alpine-perl, 1-alpine-perl, 1.13-alpine-perl, alpine-perl
1.12.2, stable, 1.12
1.12.2-perl, stable-perl, 1.12-perl
1.12.2-alpine, stable-alpine, 1.12-alpine
1.12.2-alpine-perl, stable-alpine-perl, 1.12-alpine-perl

Las etiquetas que contienen una versión semántica completa (x.x.x) son siempre inmutables, incluso si incluyen algo más, por ejemplo, _1.12.2-alphine_. Debes utilizar este tipo de versión con confianza en un entorno de producción. Las etiquetas similares a _1.12_ se actualizan cuando cambia la versión del parche. Esto significa que dentro de la imagen puede haber una versión _1.12.2_ en un momento y _1.12.8_ en el futuro. La misma lógica se aplica a las versiones que solo incluyen la versión principal, por ejemplo, _1_. En este caso, la actualización se produce no solo en la versión del parche, sino también en la versión menor.
Como recordarás, el comando docker run descarga una imagen si no está presente localmente, pero esta verificación no está relacionada con la actualización del contenido. En otras palabras, si nginx:latest se actualiza, docker run no lo descargará nuevamente; utilizará la versión latest que ya está descargada. Para garantizar la actualización segura de la imagen, existe otro comando: docker pull. Este comando siempre verifica si la imagen se ha actualizado para una etiqueta específica.

Además de las etiquetas, el nombre de la imagen puede contener un prefijo, por ejemplo, etsy/chef. Este prefijo es el nombre de la cuenta en el sitio a través del cual se crean las imágenes que se almacenan en el registro (Registry). La mayoría de las imágenes tienen este tipo de prefijo. Sin embargo, hay un pequeño conjunto, literalmente unas pocas docenas de imágenes, que no tienen un prefijo. La peculiaridad de estas imágenes es que Docker las respalda directamente. Por lo tanto, si ves que el nombre de la imagen no tiene un prefijo, significa que es una imagen oficial. Puedes ver una lista de tales imágenes aquí: https://github.com/docker-library/official-images/tree/master/library

Las imágenes se eliminan con el comando docker rmi <nombreimagen>.

$ docker rmi Ruby:2.4
Untagged: Ruby:2.4
Untagged: Ruby@sha256:d973c59b89f3c5c9bb330e3350ef8c529753ba9004dcd1bfbcaa4e9c0acb0c82
Si hay al menos un contenedor relacionado con la imagen que se va a eliminar en Docker, la plataforma no permitirá su eliminación por razones comprensibles. Si aún deseas eliminar tanto la imagen como todos los contenedores relacionados, utiliza la bandera -f.
Gestión de contenedores
La imagen describe el ciclo de vida (autómata finito) de un contenedor. Los círculos representan los estados, las comandos en negrita destacan las acciones de la consola, y los cuadrados muestran lo que se realiza en realidad.

Sigue el camino del comando docker run. A pesar de que es una sola línea de comando, desde la perspectiva de Docker, realiza dos acciones: crea un contenedor y lo inicia. Hay variaciones más complejas de ejecución, pero en esta sección solo consideraremos comandos básicos.

Ejecutemos Nginx de manera que funcione en segundo plano. Para lograr esto, se agrega la bandera -d después de la palabra "run":

$ docker run -d -p 8080:80 nginx
431a3b3fc24bf8440efe2bca5bbb837944d5ae5c3b23b9b33a5575cb3566444e
Después de ejecutar el comando, Docker mostrará el identificador del contenedor y devolverá el control. Para asegurarte de que Nginx está funcionando, abre el enlace localhost:8080 en tu navegador. A diferencia del lanzamiento anterior, nuestro Nginx se ejecuta en segundo plano, por lo que no verás su salida (registros). Puedes ver los registros utilizando el comando docker logs, al cual debes proporcionar el identificador del contenedor:

$ docker logs 431a3b3fc24bf8440efe2bca5bbb837944d5ae5c3b23b9b33a5575cb3566444e

172.17.0.1 - - [19/Jan/2018:07:38:55 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" "-"
También puedes conectarte al registro de salida en el estilo de tail -f. Para hacerlo, ejecuta docker logs -f 431a3b3fc24bf8440efe2bca5bbb837944d5ae5c3b23b9b33a5575cb3566444e. Ahora, el registro se actualizará cada vez que actualices la página en tu navegador. Para salir de este modo, puedes presionar Ctrl + C, y el contenedor en sí no se detendrá.

Ahora, mostraremos información sobre los contenedores en ejecución utilizando el comando docker ps:

CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS              PORTS                                          NAMES
431a3b3fc24b        nginx                            "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        80/tcp                                         wizardly_rosalind
Aquí tienes la explicación de las columnas:
  • CONTAINER_ID — el identificador del contenedor, similar a un hash abreviado, como en Git.
  • IMAGE — el nombre de la imagen a partir de la cual se creó el contenedor. Si no se especifica una etiqueta, se asume "latest".
  • COMMAND — el comando que se ejecutó en realidad al iniciar el contenedor.
  • CREATED — la hora de creación del contenedor.
  • STATUS — el estado actual.
  • PORTS — el reenvío de puertos.
  • NAMES — un alias. Docker permite asignar un nombre al contenedor además de su identificador. Esto facilita el manejo del contenedor. Si no se especifica un nombre al crear el contenedor, Docker lo crea automáticamente. En la salida de arriba, el contenedor de Nginx tiene uno de esos nombres generados automáticamente.

    (La orden docker stats muestra información sobre cuántos recursos consumen los contenedores en ejecución.)
Ahora, intentemos detener el contenedor. Ejecutaremos el comando:

# Вместо CONTAINER_ID можно указывать имя
$ docker kill 431a3b3fc24b # docker kill wizardly_rosalind
431a3b3fc24b
Si intentas ejecutar docker ps, notarás que el contenedor ya no está allí. Ha sido eliminado.

El comando docker ps muestra solo los contenedores en ejecución. Sin embargo, también puede haber contenedores detenidos. La detención de contenedores puede ocurrir tanto después de una finalización exitosa como en caso de errores. Intenta ejecutar docker run ubuntu ls, y luego docker run ubuntu bash -c "unknown". Estos comandos no ejecutan procesos de larga duración; terminan inmediatamente después de su ejecución, y el segundo comando termina con un error porque no existe tal comando.

Ahora, mostraremos todos los contenedores con el comando docker ps -a. Las tres primeras líneas de la salida incluirán:

CONTAINER ID        IMAGE                            COMMAND                  CREATED                  STATUS                       PORTS                                          NAMES
85fb81250406        ubuntu                           "bash -c unkown"         Less than a second ago   Exited (127) 3 seconds ago                                                  loving_bose
c379040bce42        ubuntu                           "ls"                     Less than a second ago   Exited (0) 9 seconds ago                                                    determined_tereshkova
Aquí tienes los dos últimos contenedores que ejecutamos. Si miras la columna STATUS, verás que ambos contenedores están en el estado "Exited". Esto significa que el comando que se ejecutó dentro de ellos se completó y se detuvieron. La diferencia está en que uno se detuvo con éxito (código de salida 0) y el otro con un error (código de salida 127). Después de detener un contenedor, incluso puedes reiniciarlo:

docker start determined_tereshkova # En tu caso habrá un nombre diferente
Sin embargo, esta vez no verás la salida. Para verla, utiliza el comando docker logs determined_tereshkova.
Interacción con otras partes del sistema
Ejecutar un contenedor aislado que opera completamente por sí solo es de poca utilidad. Por lo general, los contenedores necesitan interactuar con el mundo exterior, recibir solicitudes entrantes en un puerto específico, realizar solicitudes a otros servicios, leer y escribir archivos compartidos, entre otras cosas. Todas estas capacidades se pueden configurar al crear un contenedor.
Modo interactivo

El uso más simple de Docker, como ya hemos visto, es levantar un contenedor y ejecutar un comando en su interior:

$ docker run ubuntu ls /usr
bin
games
include
lib
local
sbin
share
src
$
Después de ejecutar el comando Docker, se te devuelve el control y vuelves a estar fuera del contenedor. Si intentas ejecutar Bash de la misma manera, no obtendrás el resultado deseado:

$ docker run ubuntu bash
$
El asunto es que bash inicia una sesión interactiva dentro del contenedor. Para interactuar con ella, es necesario dejar abiertos los flujos STDIN y lanzar un TTY (terminal seudográfico). Por lo tanto, para iniciar sesiones interactivas, no olvides agregar las opciones -i y -t. Por lo general, se agregan juntas como -it. Entonces, la forma correcta de iniciar bash se ve así: docker run -it ubuntu bash.

Puertos

Si ejecutas nginx con el comando docker run nginx, nginx no podrá recibir ninguna solicitud, a pesar de que dentro del contenedor esté escuchando en el puerto 80 (recuerda que cada contenedor por defecto reside en su propia red). Pero si lo ejecutas de esta manera: docker run -p 8080:80 nginx, nginx comenzará a responder en el puerto 8080.

La bandera -p te permite describir cómo y qué puerto exponer al exterior. El formato 8080:80 se descifra de la siguiente manera: redirecciona el puerto 8080 desde fuera del contenedor al puerto 80 dentro del contenedor. Por defecto, el puerto 8080 escucha en 0.0.0.0, es decir, en todas las interfaces disponibles. Por lo tanto, el contenedor iniciado de esta manera es accesible no solo a través de localhost:8080, sino también desde el exterior de la máquina (si no se prohíbe el acceso de alguna manera). Si necesitas redirigirlo solo al loopback, el comando se cambia a: docker run -p 127.0.0.1:8080:80 nginx.

Docker permite redirigir tantos puertos como necesites. Por ejemplo, en el caso de nginx, a menudo es necesario utilizar tanto el puerto 80 como el 443 para HTTPS. Puedes hacerlo de la siguiente manera: docker run -p 80:80 -p 443:443 nginx. Puedes leer más sobre otras formas de redirigir puertos en la documentación oficial.

Volúmenes

Otra tarea común está relacionada con el acceso al sistema de archivos principal. Por ejemplo, al iniciar un contenedor de nginx, puedes proporcionarle una configuración que se encuentra en el sistema de archivos principal. Docker lo copiará al sistema de archivos interno y nginx podrá leerlo y utilizarlo.

La redirección se realiza utilizando la opción -v. Así es como puedes iniciar una sesión de bash desde la imagen de Ubuntu, conectando el historial de comandos desde el sistema de archivos principal: docker run -it -v ~/.bash_history:/root/.bash_history ubuntu bash. Si presionas la flecha hacia arriba en la sesión de bash, verás el historial. Puedes redirigir tanto archivos como directorios. Cualquier cambio realizado dentro del volumen se reflejará tanto dentro del contenedor como en el exterior, y por defecto, se permiten todas las operaciones. Al igual que con los puertos, puedes redirigir cualquier cantidad de archivos y directorios.

Al trabajar con volúmenes, hay algunas reglas importantes que debes conocer:
  • La ruta al archivo en el sistema externo debe ser absoluta.
  • Si la ruta interna (lo que va después de :) no existe, Docker creará todos los directorios y archivos necesarios. Si existe, reemplazará lo antiguo con lo que se redirigió.
Además de redirigir parte del sistema de archivos desde el exterior, Docker proporciona varias formas más de crear y utilizar volúmenes. Puedes encontrar más información en la documentación oficial.

Variables de entorno

La configuración de una aplicación dentro de un contenedor generalmente se realiza a través de variables de entorno de acuerdo con los 12 factores. Hay dos formas de establecerlas:
  • La opción -e. Se utiliza de la siguiente manera: docker run -it -e "HOME=/tmp" ubuntu bash
  • Un archivo especial que contiene definiciones de variables de entorno, que se pasa al contenedor con la opción --env-file.
Preparación de tu propia imagen
La creación y publicación de tu propia imagen no es más complicada que su uso. El proceso se divide en tres pasos:
  • Se crea un archivo Dockerfile en la raíz del proyecto. En su interior, se describe el proceso de creación de la imagen.
  • Se realiza la construcción de la imagen con el comando docker build.
  • Se publica la imagen en un registro (Registry) con el comando docker push.
Vamos a analizar el proceso de creación de una imagen a través del ejemplo de empacar un linter como eslint (asegúrate de seguirlo por tu cuenta). Como resultado de la construcción, obtendremos una imagen que se puede utilizar de la siguiente manera:

$ docker run -it -v /path/to/js/files:/app my_account_name/eslint

/app/index.js
  3:6  error  Parsing error: Unexpected token

  1 | import path from 'path';
  2 |
> 3 | path(;)
    |      ^
  4 |

✖ 1 problem (1 error, 0 warnings)
Entonces, solo necesitas ejecutar un contenedor a partir de esta imagen, conectando un directorio con archivos JS para su revisión como un volumen en el directorio interno /app.
1. La estructura final del directorio, en base a la cual se construirá la imagen, se ve de la siguiente manera:

eslint-docker/
    Dockerfile
    eslintrc.yml
El archivo eslintrc.yml contiene la configuración del linter. Se leerá automáticamente si se encuentra en el directorio raíz con el nombre .eslintrc.yml. Es decir, este archivo debe tener ese nombre cuando se copie en el directorio /root dentro de la imagen.
2. Creación del Dockerfile

# Dockerfile
FROM node:9.3

WORKDIR /usr/src

RUN npm install -g eslint babel-eslint
RUN npm install -g eslint-config-airbnb-base eslint-plugin-import

COPY eslintrc.yml /root/.eslintrc.yml

CMD ["eslint", "/app"]
El Dockerfile tiene un formato bastante sencillo. En cada línea se especifica una instrucción (directiva) y su descripción.

FROM

La instrucción FROM se utiliza para especificar la imagen de la cual se hereda. Es importante tener en cuenta que las imágenes se construyen basándose unas en otras y, en conjunto, forman un gran árbol.
En la raíz de este árbol se encuentra la imagen busybox. En aplicaciones prácticas, generalmente no se utiliza directamente, ya que Docker proporciona imágenes predefinidas para cada ecosistema y pila.

RUN

Es la instrucción principal en un Dockerfile. En esencia, se especifica un comando sh que se ejecutará en el entorno establecido por la imagen base especificada en la instrucción FROM al construir la imagen. Dado que, por defecto, todas las operaciones se realizan como el usuario root, no es necesario utilizar sudo (y es probable que no esté presente en la imagen base). Además, ten en cuenta que la construcción de una imagen no es un proceso interactivo. En situaciones en las que se utiliza un comando que podría solicitar entrada del usuario, debes suprimir la salida. Por ejemplo, en el caso de los gestores de paquetes, puedes hacerlo de la siguiente manera: apt-get install -y curl. La bandera -y indica que la instalación debe realizarse sin hacer preguntas adicionales.

Técnicamente, una imagen de Docker no es un solo archivo, sino un conjunto de capas (layers). Cada ejecución de la instrucción RUN crea una nueva capa, que se puede ver como un conjunto de archivos creados y modificados (incluyendo eliminaciones) por el comando especificado en RUN. Este enfoque mejora significativamente el rendimiento del sistema al aprovechar la caché de capas que no han cambiado. Por otro lado, Docker reutiliza capas en diferentes imágenes si son idénticas, lo que reduce la velocidad de descarga y el espacio en disco utilizado. El tema de la caché de capas es importante cuando se utiliza activamente Docker. Para que funcione de manera eficiente, es necesario comprender cómo funciona y cómo escribir correctamente las instrucciones RUN para aprovechar al máximo la caché.

COPY

Como su nombre indica, la instrucción COPY toma un archivo o directorio del sistema de archivos principal y lo copia dentro de la imagen. Sin embargo, esta instrucción tiene una limitación: lo que se copia debe residir en el mismo directorio que el Dockerfile. Esta es la instrucción que se utiliza durante el desarrollo cuando es necesario empacar una aplicación dentro de la imagen.

WORKDIR

Esta instrucción establece el directorio de trabajo. Todas las instrucciones subsiguientes se ejecutarán en este directorio. La instrucción WORKDIR funciona de manera similar al comando cd. Además, cuando ejecutas un contenedor, también se inicia desde el directorio de trabajo. Por ejemplo, si ejecutas bash, estarás dentro de ese directorio.

CMD

Es la instrucción que define la acción predeterminada cuando se utiliza docker run. Se utiliza solo si el contenedor se inicia sin especificar una instrucción, de lo contrario, se ignora.

3. Construcción

Para construir la imagen, se utiliza el comando docker build. Se especifica el nombre de la imagen con la opción -t, incluyendo el nombre de la cuenta y la etiqueta. Si no se especifica una etiqueta, se utiliza la etiqueta latest de forma predeterminada.

$ docker build -t my_account_name/eslint .
Después de ejecutar este comando, puedes ver la imagen actual en la lista de docker images. Incluso puedes comenzar a usarla sin necesidad de publicarla en un Registry. Recuerda que el comando docker run no intenta buscar una versión actualizada de la imagen si ya existe una imagen local con ese nombre y etiqueta.

4. Publicación

$ docker push my_account_name/eslint
Para que la publicación sea exitosa, debes cumplir con dos requisitos:
  • Registrarte en Docker Cloud y crear un repositorio para la imagen allí.
  • Iniciar sesión en la interfaz de línea de comandos utilizando el comando docker login.
Docker Compose es una herramienta que permite desarrollar proyectos localmente utilizando Docker. En términos de los problemas que resuelve, se puede comparar con Vagrant.

Docker Compose te permite administrar un conjunto de contenedores, donde cada contenedor representa un servicio dentro de tu proyecto. La gestión incluye la construcción, ejecución teniendo en cuenta las dependencias y la configuración. La configuración de Docker Compose se describe en un archivo llamado docker-compose.yml, que se encuentra en la raíz del proyecto, y tiene una estructura similar a la que se muestra a continuación:

# https://github.com/hexlet-basics/hexlet_basics

version: '3.3'

services:
  db:
    image: postgres
  app:
    build:
      context: services/app
      dockerfile: Dockerfile
    command: mix phx.server
    ports:
      - "${PORT}:${PORT}"
    env_file: '.env'
    volumes:
      - "./services/app:/app:cached"
      - "~/.bash_history:/root/.bash_history:cached"
      - ".bashrc:/root/.bashrc:cached"
      - "/var/tmp:/var/tmp:cached"
      - "/tmp:/tmp:cached"
    depends_on:
      - db
En este archivo docker-compose.yml, defines los servicios que componen tu proyecto y su configuración. Cada servicio puede estar basado en una imagen existente (como image: postgres) o construirse a partir de un Dockerfile específico (como build: context: services/app, dockerfile: Dockerfile). Además, puedes especificar puertos expuestos, variables de entorno, volúmenes y dependencias entre servicios.

Docker Compose facilita la orquestación de múltiples contenedores para proyectos más complejos y proporciona un enfoque más estructurado y manejable para desarrollar aplicaciones que dependen de varios servicios.

En el uso de Docker, la configuración de las máquinas en un proyecto generalmente se reduce a la instalación de Docker. Luego, solo necesitas implementar el proyecto. El proceso más simple de implementación se ve de la siguiente manera:
  • 1
    Descargar una nueva imagen.
  • 2
    Detener el contenedor anterior.
  • 3
    Iniciar un nuevo contenedor.
Además, este procedimiento de implementación no depende de la pila de tecnologías utilizada. Puedes realizar la implementación (así como la configuración de las máquinas) utilizando Ansible.

Otra opción, adecuada para proyectos más complejos, implica el uso de sistemas de orquestación especiales como Kubernetes. Esta opción requiere una preparación significativa, que incluye la comprensión de los principios de funcionamiento de sistemas distribuidos.
Docker bajo el capó
El aislamiento proporcionado por Docker se logra gracias a las capacidades del kernel de Linux, como Cgroups y Namespaces. Estas capacidades permiten ejecutar procesos del sistema operativo en un entorno aislado y con restricciones en el uso de recursos físicos, como memoria o CPU.
Leer otros artículos de Guías
Lea otros artículos relevantes del mundo de la tecnología y el espíritu empresarial.