Skip to content

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 de Docker ​

  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:

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

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

shell
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:

shell
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:

shell
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 última y 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:

shell
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:

bash
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

Docker en Linux ​

Semejante a macOS, podríamos usar Colima. Además, en algunas distribuciones de servidor puede venir como servicio elegible desde la instalación, tal es el caso de Linux Ubuntu Server. Sin embargo, si quieres una instalación directa, podrías usar cada una de las siguientes sentencias:

bash
sudo apt install -y apt-transport-https ca-certificates software-properties-common

sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker

Con estas sentencias se dispone de paquetes necesarios, se obtiene llave GPG del repositorio de Docker, se configura el repositorio, y se instalan los paquetes respectivos. Al final, se habilita el usuario para usar privilegios de Docker.

Comandos esenciales de Docker ​

Podemos citar los siguientes comandos esenciales:

ComandoDescripción
docker --versionMuestra la versión de Docker (también puede usarse -v)
docker runArranca 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 psLista procesos asociados a contenedores en ejecución. Suele usarse con el parámetro -a
docker image lsPara listar las imagenes disponibles localmente
docker pullDescarga una imagen de contenedor disponible en docker-hub
docker pushPublica una imagen de contenedor en docker-hub
docker tagEtiqueta una imagen de contenedor para publicar en docker-hub
docker container lsLista los contenedores en ejecución. --all lista todos
docker container stopDetiene la ejecución del contenedor indicado. Puede abreviarse como docker stop
docker container startInicia la ejecución del contenedor indicado. Puede abreviarse como docker start
docker volume lsPara listar volumenes (discos de docker)
docker network lsPara listar recursos de red (permitiendo comunicar otros contenedores)
docker buildCrea una imágen a partir de un archivo de script con nombre Dockerfile. Ej. docker build -t image .
docker logsMuestra "logs" de un contenedor
docker execPuede 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

bash
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í:

bash
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

bash
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í:

bash
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

bash
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

bash
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:

bash
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:

Dockerfile
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:

shell
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:

shell
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:

javascript
var http = require('http');
http.createServer((req, res) => {
    res.write('Hi there!');
    res.end();
}).listen(8080);
Dockerfile
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:

shell
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:

ComandoDescripción
FROM Define la imágen sobre la que se basará nuestro contendor
ENVPermite definir variables de entorno
COPYEs el comando apropiado para copiar archivos locales al contendor (como alternativa remota existe ADD)
ADDPermite copiar archivos locales o tomarlos remotamente desde un enlace, incluso descomprime empaquetados
RUNEjecuta sentencias del sistema interno cuando se crea la imágen para aprovisionar el contenedor (fecuentemente con Linux)
CMDDa paso a comandos que se ejecutan cuando el contenedor se ha inicializado (distinto de RUN que se ejecuta cuando se construye el contenedor).
WORKDIRDefine directorio de trabajo en el contenedor
EXPOSEExpone un puerto para ser mapeado por el anfitrión o máquina
ENTRYPOINTPara lanzar un ejecutable cuando arranca el contenedor (generalmente servicios)
ARGDefine argumentos o variables que se reciben al construir la imagen. Se utilizan con el formato ${ARG}
MAINTAINERAutor 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:

Dockerfile
CMD ["sleep", "8080"]

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

shell
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.

Dockerfile
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:

shell
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:

Dockerfile
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:

shell
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:

Dockerfile
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

Dockerfile
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:

  • 📂 docker
    └── .devcontainer
    ⋅⋅⋅⋅⋅⋅⋅⋅├── devcontainer.json
    ⋅⋅⋅⋅⋅⋅⋅⋅└── Dockerfile

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:

json
{
    "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:

Dockerfile
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.


© 2019 by César Arcila