Docker

La revolución de los contenedores y las aplicaciones en la nube
© 2020 by César Arcila

Cuando se habla de contenedores se puede ver como sinónimo de empaquetamiento (sin comprimir, pero con un entorno de ejecución interno y aislado, generalmente basado en Linux). Se puede asociar perfectamente por analogía con los contenedores de las embarcaciones, que actualmente llegan a usarse como bodegas, oficinas pequeñas y hasta algunas viviendas.

Si se ha escuchado sobre máquinas virtuales, Docker puede verse como una virtualización ligera dado que posibilita independencia del entorno y asociación de los recursos. Sin embargo, esto lo hace de modo que no ocupa los mismos recursos que una máquina virtual, mas bien dispone de los recursos de una manera más eficiente, acercándose a la velocidad del sistema anfitrión (Windows, macOS, Linux). Además permite mayor número de instancias que cuando se usan máquinas virtuales en un servidor, incluso se usan contendores Docker dentro de máquinas virtuales.

Podemos asociar mejor a Docker con el concepto de contenedores y su gestión, siendo posible proporcionar un entorno de desarrollo independiente colocándolo dentro de un contenedor con la posibilidad de portarlo a otro equipo de computo o guardar todo tu ambiente en un disco externo, además de pasar de un modo más fluido a producción, es decir, una gestión de configuración más confiable al lograr un entorno semejante, mitigando inconsistencias.

Sin embargo, esto requiere algunos conceptos fundamentales para utilizar y operar sobre contenedores de este tipo, es decir, se requieren hacer “scripts” y mantenerlos, siendo razonablemente comprensibles. Dependiendo del perfil del recurso humano o de la aplicación, la operación con contenedores puede ir de menos a más, y en definitiva es la práctica la que aporta el aprendizaje cuando te enfrentas a este tipo de herramientas en mayor medida. Hoy día, quién se dedica profesionalmente al desarrollo de software está llamado, como mínimo, a tener un concepto sobre estas tecnologías, en caso de involucrarse más se requieren conocimientos en Linux y conviene haber usado alguna máquina virtual previamente (ej. VirtualBox).

Este documento tiene un alcance sencillo y ágil a modo de abre bocas. Superado este alcance, para conocer más de este tema deberá investigarse.
Referencia sobre Linux

Conceptos esenciales

  1. Docker. Herramienta que gestiona imágenes de repositorio de un sistema (o aplicación) y contenedores que operan como un entorno virtual aislado y ligero. Se instala frecuentemente en sistemas operativos Linux, con infraestructura orientada a la nube (incluso máquinas virtuales).

  2. Docker Desktop. Versión de Docker en sistemas operativos de escritorio como Windows 10 y macOS. Principalmente es usado para entornos de desarrollo o pruebas, punto de partida para lograr un entorno de producción semejante, o integración continua, en dónde se mitigan inconsistencias con esta tecnología.

  3. Docker Hub. Servicio en la nube que dispone de repositorios de imágenes (de sistemas operativos o aplicaciones) expuestas por compañías de tecnologías o desarrolladores.

  4. Dockerfile. Archivo de “script” para producir nuevas imágenes personalizadas basadas en un repositorio (Docker Hub u otro). Se usa un archivo con este nombre por cada proyecto (de código) definido.

  5. Imágen. Corresponde a un repositorio de la imágen de un sistema operativo o aplicación preparada para su reproducción, o bien, construida de modo personalizado usando un archivo Dockerfile. Una analogía que puede servir, si alcanzó a conocer el DVD (o los arhivos ISO), es que la imágen es un medio de instalación original que se usa para hacer copias, solo que en este caso consiste en un archivo descriptivo que define su contenido (Dockerfile).

  6. Contenedor. Empaquetamiento de un entorno virtual aislado y ligero que se reproduce a partir de una imágen. Primero se debe obtener o construir una imágen, y a partir de ésta se ejecuta el contenedor. Puede verse como la instancia en ejecución del entorno que se obtiene basado en una imagen, aunque el contenedor puede haberse establecido sin estar ejecutándose. Si la analogía del DVD es útil, sería entonces la copia obtenida (un DVD) que se puede reproducir para ver su contenido (video, music, aplicación informática), por tanto Docker sería en ese caso el reproductor.

  7. Docker Compose. Propuesta avanzada para simplificar el uso de multiples contenedores que se enlazan o comunican, con el fin de gestionar contenedores por cada servicio o cada aplicación y con una configuración declarativa más sencilla (usando archivo de tipo YAML), en lugar de elaborar “scripts” complejos con Dockerfile. Con esta herramienta es posible obtener una composición de contendores para servicios como Servidor Web, microservicios de aplicaciones (API), Email (ej. poste.io), Proxy (ej. Traefik), Chat (ej. Rocket Chat), Video chat (ej. jitsi.org). Esto nos llevaría a distinguir otros conceptos como servicios, volúmenes compartidos, mapeo de red, que pueden ser investigados en la documentación oficial de Docker Compose en tanto se requiera.

Docker Desktop para Windows 10 Pro (y mayor)

Para usar Docker en Windows debemos contar al menos con la versión de Windows 10 Pro a 64bits. En versiones anteriores se puede investigar sobre el uso de la herramienta Docker Toolbox, pero en este documento se usará Docker Desktop que es la herramienta actual, así que se debe descargar desde su sitio, dónde también debes crear una cuenta en el servicio en la nube para contenedores denominado Docker Hub.

Dado que Docker Desktop utiliza Hyper-V de Windows para las características de virtualización, se requiere habilitar privilegio con el siguiente comando desde PowerShell como administrador:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All

Puede comprobarse que la instalación ha dejado a Docker configurado usando el comando:

docker --version

Si no se ha iniciado aún el servicio puede esperarse o ejecutar el icono respectivo

Usaremos el ejemplo disponible en la documentación oficial de Docker para ilustrar la ejecución de un contenedor. Para ello se ejecuta:

docker run hello-world

Suponiendo que deseamos instalar Linux Ubuntu, usamos el comando docker pull para descargar una imágen de repositorio y luego el comando docker run para lanzarla como contenedor. Por ejemplo:

docker pull ubuntu
docker run -it ubuntu

El parámetro -it se usa para entrar a modo interactivo de comandos. En otras palabras, la última línea es la manera como inicias un servidor en Linux Ubuntu. Para salir de esa sentencia se usan las teclas CTRL + D (o el comando exit). Si se ha entendido bien, con esta simple línea tienes operando otro sistema operativo sin instalación ni configuración de una máquina virtual convencional y en pocos minutos, así que es razonablemente sencillo hasta que se requieren cosas bien específicas.

En el caso de requerir Node.js se podría ejecutar:

docker run -it node:lts-buster-slim

Dados los dos ejemplos anteriores, nótese que docker pull puede ser omitido al usar el comando docker run, debido a que automaticamente hace la descarga cuando no se encuentra la imágen en la máquina local.
En este último ejemplo, la imágen de repositorio con nombre node:lts-buster-slim utiliza el sistema operativo Debian (distribución sobre la que se basa Ubuntu). También podría usarse docker pull debian:buster-slim y agregar las herramientas de desarrollo deseadas con un archivo Dockerfile (como se verá avanzando el documento).

Docker en macOS

De manera semejante se puede instalar Docker Desktop para macOS. Sin embargo, conviene conocer como instalar el cliente de Docker en el sistema macOS. Actualmente, puede ustilizarse la herramienta brew con colima. Para ello ejecutamos los siguientes comandos:

brew install docker
brew install docker-compose
brew install colima
colima start --cpu 2 --memory 2 --disk 20
limactl list

colima start es el comando que inicia el servicio reservando una máquina virtual, en cuyo caso se puede especificar procesadores memoria y disco (que por defecto es de 60GB), incluso dns (--dns 1.1.1.1). Para volverlo a iniciar podemos usar el comando corto (sin especificaciones).

Podemos verficar si existe un proceso de contenedores docker ejecutándose mediante el comando: docker ps -a

Comandos esenciales de Docker

Podemos citar los siguientes comandos esenciales:

Comando Descripción
docker --version Muestra la versión de Docker (también puede usarse -v)
docker run Arranca un contenedor basado en una imágen. -it indica modo interactivo, sino se usa -t para salida tty (sin interactuar), o -d para lanzarlo en “background” (segundo plano).
docker ps Lista procesos asociados a contenedores en ejecución. Suele usarse con el parámetro -a
docker image ls Para listar las imagenes disponibles localmente
docker pull Descarga una imagen de contenedor disponible en docker-hub
docker push Publica una imagen de contenedor en docker-hub
docker tag Etiqueta una imagen de contenedor para publicar en docker-hub
docker container ls Lista los contenedores en ejecución. --all lista todos
docker container stop Detiene la ejecución del contenedor indicado. Puede abreviarse como docker stop
docker container start Inicia la ejecución del contenedor indicado. Puede abreviarse como docker start
docker volume ls Para listar volumenes (discos de docker)
docker network ls Para listar recursos de red (permitiendo comunicar otros contenedores)
docker build Crea una imágen a partir de un archivo de script con nombre Dockerfile. Ej. docker build -t image .
docker logs Muestra “logs” de un contenedor
docker exec Puede usarse para acceder al contenedor cuando se está ejecutando en “background” (-d). De esta manera se puede interacturar con comandos del sistema interno. Por ejemplo, podemos verificar la version de linux así: docker exec mycontainer uname -a

Ejemplo con repositorio MariaDB

Los siguientees comandos representan los pasos para establecer un contenedor con MariaDB

docker volume create mariadb
docker pull mariadb:10.9
docker run --name mariadb -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -v mariadb:/var/lib/mysql -d mariadb:10.9
docker exec -it mariadb bash
mysql -ppassword

docker volume create... establece un volumen externo para Docker. Se puede limpiar o remover los volúmenes sin uso ejecuando docker volume prune.

En una siguiente ocasión, podemos iniciar el contenedor e ingresar de nuevo directamente así:

docker start mariadb
docker exec -it mariadb mysql -p

Una vez se ingresa al contenedor se puede crear la base de datos con: create database ...;

Ejemplo con repositorio PostgreSQL

Los siguientees comandos representan los pasos para establecer un contenedor con PostgreSQL

docker volume create postgres
docker pull postgres:14.5
docker run --name postgres -e POSTGRES_PASSWORD=password -e PGDATA=/var/lib/postgresql/data/pgdata -p 5432:5432 -v postgres:/var/lib/postgresql/data -d postgres:14.5
docker exec -it postgres bash
psql -U postgres

En el comando docker run.., podría usarse -e POSTGRES_USER=myuser para indicar un usuario. En este caso asumiría el usuario postgres.

En una siguiente ocasión, podemos iniciar el contenedor e ingresar de nuevo directamente así:

docker start postgres
docker exec -it postgres psql -U postgres

Una vez se ingresa al contenedor se puede crear la base de datos con: create database ...; (iniciando psql -U postgres)

Ejemplo con repositorio Redis

Los siguientees comandos representan los pasos para establecer un contenedor con Redis

docker volume create redis
docker pull redis:latest
docker run --name redis -p 6379:6379 -v redis:/data -d redis:latest
docker exec -it redis redis-cli

Ejemplo con repositorio MongoDB

Los siguientees comandos representan los pasos para establecer un contenedor con MongoDB

docker volume create mongodb
docker pull mongo:latest
docker run --name mongodb -p 27017:27017 -v mongodb:/data/db -d mongo:latest
docker exec -it mongodb bash
mongod

Una vez se ingresa al contenedor se puede crear la base de datos con: use ....

Un mejor ejemplo puede encontrarse avanzando el documento con Docker Compose

Ejemplo con BitWarden (para guardar contraseñas)

Si quieres guardar contraseñas podemos citar BitWarden. Dado que ya conocemos un poco sobre Docker, esta herramienta se puede usar con contenedores y simplifica su configuración con un “script” de instalación ejecutando lo siguiente:

mkdir btwrdn
cd btwrdn
curl -Lso bitwarden.sh https://go.btwrdn.co/bw/sh \
  && chmod +x bitwarden.sh
./bitwarden.sh install

El instalador crea un boveda y solicita nuestro email. Se requiere un cliente para gestionar la boveda.

Dockerfile - El script de Docker para crear Imágenes

📂 Dockerfile ~> build (image) ~> run (container)

La dinámica sugiere que a partir del archivo Dockerfile se construye (build) una imágen que a su vez posibilita la ejecución (run) de un contendor, es decir, que los contenedores son generados a partir de una imágen, la cual puede definirse con un archivo Dockerfile. Por tanto, se puede empezar con una imágen ya creada o con tu propio archivo Dockerfile.

Con el servicio de Docker Hub se pueden descargar imagenes de repositorio usando el comando docker pull. Sin embargo, cuando se desea modificar o implementar tu propia imágen basada en un repositorio se hace uso de un archivo de script con nombre Dockerfile (tal como se ha escrito). Con este archivo se busca automatizar el proceso de creación de imágenes para contenedores y normalmente las sentencias son cercanas o equivalentes a comandos Linux. Veamos un ejemplo:

FROM nginx:alpine
COPY index.html /usr/share/nginx/html
COPY assets/ /usr/share/nginx/html

La sentencia FROM indica la imágen de repositorio base (nginx:alpine), mientras que la sentencia COPY envía los archivos a un destino en la ruta esperada del contendor. En este caso, se están copiando los archivos index.html y assets (que es un directorio).

Ubicándose en la carpeta que contiene el archivo Dockerfile, se construye (build) la imágen y se ejecuta (run) el contendor respectivo con los siguientes comandos:

docker build -t user/web .
docker run -p 9000:80 --name web user/web

Para cancelar su ejecución se usan las teclas CTRL + C. Se ha logrado obtener una nueva imágen de repositorio denominada user/web (dónde user es el usuario registrado en Docker Hub), basada en la imágen de nginx:alpine, y se traduce (con -p) el puerto 80 como 9000 en nuestro anfitrión (localhost). De este modo, se puede abrir un navegador y consultar la dirección localhost:9000.

Aunque es recomendable nombrar la nueva imagen producida (con --name), es posible simplificar los dos comandos anteriores de la siguiente manera:

docker build -t web .
docker run -p 9000:80 web

De este modo se ve más sencillo.

Veamos el siguiente ejemplo donde usaremos un archivo index.js y Dockerfile para Node.js:

var http = require('http');
http.createServer((req, res) => {
    res.write('Hi there!');
    res.end();
}).listen(8080);
FROM node:lts-buster-slim
WORKDIR /usr/src/app
COPY index.js /usr/src/app
EXPOSE 8080
CMD [ "node", "index.js" ]

Y ejecutamos las siguientes sentencias:

docker build -t nodeapp .
docker run -d -p 80:8080 nodeapp

Recordar que cuando se usa docker run -d se lanza el proceso en “background”. Se puede abrir un navegador y consultar la dirección localhost. Los comandos de Dockerfile se explicarán a continuación.

Sentencias esenciales en Dockerfile

Podemos citar las siguientes sentencias esenciales para el archivo Dockerfile:

Comando Descripción
FROM Define la imágen sobre la que se basará nuestro contendor
ENV Permite definir variables de entorno
COPY Es el comando apropiado para copiar archivos locales al contendor (como alternativa remota existe ADD)
ADD Permite copiar archivos locales o tomarlos remotamente desde un enlace, incluso descomprime empaquetados
RUN Ejecuta sentencias del sistema interno cuando se crea la imágen para aprovisionar el contenedor (fecuentemente con Linux)
CMD Da paso a comandos que se ejecutan cuando el contenedor se ha inicializado (distinto de RUN que se ejecuta cuando se construye el contenedor).
WORKDIR Define directorio de trabajo en el contenedor
EXPOSE Expone un puerto para ser mapeado por el anfitrión o máquina
ENTRYPOINT Para lanzar un ejecutable cuando arranca el contenedor (generalmente servicios)
ARG Define argumentos o variables que se reciben al construir la imagen. Se utilizan con el formato ${ARG}
MAINTAINER Autor de la imágen

Inspeccionando un Contenedor Docker

Suponiendo que deseas probar linux en Docker o una aplicción para Docker que ha fallado. Tomemos el último ejemplo de Dockerfile y reemplazamos (o comentamos con #) la última línea que corresponde a CMD para aplicar la siguiente:

CMD ["sleep", "8080"]

De este modo, al correr el contenedor puedes acceder o interactuar con el comando docker exec -it. Por ejemplo:

docker exec -it jdkapp /bin/bash

/bin/bash especifica el interprete de comandos, De modo genérico suele usarse /bin/sh que es más simple y aplica para la mayoría de imágenes (con Linux), pero si se trata de una imágen con Debian puede ir el del ejemplo.
jdkapp corresponde al nombre del contenedor. Si no se ha nombrado debe reportarse el identificdor del proceso (docker ps -a)

Entorno de Desarollo con Dockerfile

Se ilustra a modo de ejemplo un entorno de desarollo más elaborado con Node.js, Java y MariaDB. En muchos casos es preferible Docker Compose que se verá posteriormente.

FROM node:lts-buster-slim
WORKDIR /usr/src/app
COPY index.js /usr/src/app
RUN apt update && apt install -y git-core curl zip unzip
RUN apt install -y mariadb-server
RUN systemctl enable mariadb
EXPOSE 8080
CMD [ "node", "index.js" ]

Se guarda y se ejecutan las siguientes sentencias:

docker build -t nodeapp .
docker run -d -p 3000:8080 --name webapp nodeapp

Podría partirse también desde una version con JDK 11. Para comenzar a modo de ejemplo puedes probar lo siguiente:

FROM adoptopenjdk/openjdk11:debianslim-slim
WORKDIR /usr/src/app
COPY build/libs/app.jar /usr/src/app
EXPOSE 8080
CMD [ "java", "-jar", "app.jar", "mymain" ]

De modo semejante, ejecutamos:

docker build -t javaapp .
docker run -d --name jdkapp javaapp
docker exec jdkapp java -version

Una imágen para un programa sencillo bajo Java también podría configurarse del modo siguiente:

FROM alpine
WORKDIR /root/app
COPY app.java /root/app
RUN apk add openjdk11
ENV JAVA_HOME /usr/bin/jvm/java-11-openjdk
ENV PATH $PATH:JAVA_HOME/bin
RUN javac app.java
ENTRYPOINT java app

Ahora veamos un ejemplo de contendor con PHP

FROM php:7.4-cli
COPY . /usr/src/app
WORKDIR /usr/src/app
CMD [ "PHP", "./index.php" ]

Entorno de Desarrollo Docker + VSCode

Se ilustra a modo de ejemplo un entorno de desarollo esencial. En muchos casos es preferible algo más avanzado (Docker Compose).

Ahora usaremos para nuestro entorno de desarrollo el editor Visual Studio Code (VSCode) y Docker. Para ello ingresamos al editor (VSCode) e instalamos la extensión Remote - Containers.

Una vez instalada la extensión, usando una carpeta del disco duro, se plantea la siguiente estructura para el proyecto:

Como se puede observar, se requiere un archivo con nombre devcontainer.json dentro de la carpeta .devcontainer. Dentro de este archivo incluimos, por ejemplo, el siguiente contenido:

{
    "name": "Docker Server",
    "dockerFile": "Dockerfile",
    "appPort": 8080,
}

Puedes revisar la documentación sobre como funciona este archivo en el sitio de VSCode

Para nuestro archivo Dockerfile se tendría, por ejemplo, el siguiente contenido:

FROM node:lts-buster-slim
EXPOSE 8080

Con esto tendremos una imágen basada en node:lts-buster-slim para contar con un contenedor de sistema Linux (Debian).

Se debe reabrir VSCode para que identifique el archivo devcontainer.json dentro de la carpeta .devcontainer usado por la extensión instalada, la cual ejecutará nuestro contendor si confirmamos esta acción al reabrir el editor. De este modo, encontrarás que se abre una terminal para el espacio de trabajo y se puede usar el sistema de archivos de Linux y sus comandos (por ejemplo: mkdir src, cd src, touch app.js), es decir, puedes operar directamente en el sistema y el editor refleja la estructura de archivos del espacio de trabajo, gestionando el desarrollo con este editor.

Docker Compose

Docker Compose busca simplificar la gestión de contenedores de Docker cuando se requiren combinar servicios o aplicaciones con varios componentes, es decir, para múltiples contenedores que se comunican o enlazan, sugiriendo el uso de contenedor por servicio. Esta herramienta generaría “scripts” de docker simplemente indicando especificaciones en un archivo de tipo YAML.

En lugar de hacer “scripts” de comandos complejos y largos con Dockerfile, se define otro archivo de nombre docker-compose.yml con ciertos criterios a nivel declarativo y el archivo Dockerfile sólo se usaría para definir aspectos básicos del contenedor (semejante a los ejemplos que suelen ser sencillos).

Por tanto, se usa para crear multiples entornos aislados en un solo anfitrión y proporcionar una manera conveniente para que los desarrolaldores cuenten fácilmente con un entorno de desarrollo.

Veamos un ejemplo de la estructura del contenido con un archivo docker-compose.yml:

version: "3.8"
services:
  proxy:
    image: traefik:chevrotin
    command: --providers.docker
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  nginx:
    image: nginx
    labels:
      - "traefik.http.routers.nginx.rule=Host(`nginx.domain.com`)"

  apache:
    image: httpd
    labels:
      - "traefik.http.routers.apache.rule=Host(`apache.domain.com`)"

Para instalar Docker Compose en Linux Debian, por ejemplo, se ejecuta:

sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

La configuración establecida en el archivo docker-compose.yml se lanza con el comando:

docker-compose up -d

Este documento tiene un alcance sencillo y ágil a modo de abre bocas. Superado este alcance, para conocer más de este tema deberá investigarse.

Ejemplo con MongoDB

version: "3.8"
services:
  db:
    image : mongo
    environment:
      - PUID=1000
      - PGID=1000
    volumes:
      - mongodb:/datadb
    ports:
      - 27017:27017
    container_name: mongodb
    restart: unless-stopped
volumes:
  mongodb:
    driver: local

Para lanzar el contenedor e interactuar con la base de datos ejecutamos lo siguiente:

docker-compose up -d
sudo docker exec -it mongodb bash

Una vez se ingresa al contenedor se puede crear la base de datos con: use ....

Ejemplo con MariaDB

version: "3.8"
services:
  db:
    image : mariadb
    environment:
      - MYSQL_ROOT_PASSWORD=password
    volumes:
      - type: bind
        source: ./my.cnf
        target: /etc/mysql/my.cnf
    ports:
      - 3306:3306
    restart: unless-stopped

Ejemplo con PostgreSQL

version: "3.8"
services:
  db:
    image : postgres
    environment:
      - POSTGRES_DB=mytest
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=password
    volumes: 
      - postgres:/var/lib/postgresql/data
    ports:
      - '5432:5432'
    restart: unless-stopped
volumes:
  postgres:
    driver: local

Comandos esenciales con docker-compose

Podemos citar los siguientes comandos esenciales para el archivo docker-compose:

Comando Descripción
docker-compose build Construye los contenedores asociados a la composición
docker-compose rm Elimina los contenedores asociados a la composición
docker-compose up Sube todos los contenedores asociados. -d para correr en segundo plano
docker-compose down Detiene todos los contenedores asociados
docker-compose run Ejecuta comandos para la composición de contenedores
docker-compose logs Muestra “logs” para la composición de contenedores

© 2019 by César Arcila