Introducción a la tecnología de contenedores con Docker en Linux.

¿Qué es un contenedor?

Un contenedor es un grupo de procesos que comparten un conjunto de parámetros usados por uno o más subsistemas y permanece aislado del resto de procesos corriendo en el sistema operativo.

¿Qué es Docker?

Se refiere a todo aquello que facilita la creación, implementación y ejecución de aplicaciones mediante el uso de contenedores. Permite a un desarrollador empaquetar un aplicación con todos los componentes que necesita para su funcionamiento (librerías, dependencias y configuraciones). De esta manera dicha aplicación se podrá utilizar en cualquier sistema operativo GNU/Linux, independientemente de su configuración local.

Se puede decir que el funcionamiento es similar al de las máquinas virtuales, con la excepción de que las aplicaciones comparten el mismo kernel del sistema operativo subyacente, optimizando así el uso de recursos.

Docker implementa una API de alto nivel para proporcionar contenedores ligeros que ejecutan los procesos de manera aislada (cgroups), basándose en la funcionalidad provista por kernel del sistema operativo del host. A su vez implementa el aislamiento de recursos (CPU, memoria, disco) y espacios de nombres para mantener (namespaces) los contenedores separados. Docker accede a la capa de virtualización del kernel Linux a través de su propia biblioteca libcontainer.

Páginas de manual recomendadas:

man cgroups
man namespaces

¿Qué son las imágenes de Docker?

Docker se basa en el concepto de imagen para proveer contenedores de software prefabricados de diferentes sabores, por ejemplo: Debian, Apache, MySQL, PHP, etc. La mayoría de estas imágenes son "oficiales" en el sentido que son provistas por los propios fabricantes de cada pieza de software o por la comunidad de Docker, y suelen estar basadas entre sí.

De esta forma no es necesario construir contenedores desde cero. Sino que, si se necesita un servidor web Apache, se crea un contenedor a partir de la imagen oficial en lugar de pasar por el proceso completo de instalación de sistema operativo, dependencias y servicios (como sería en el caso de creación de una máquina virtual típica).

Dockerfile

Una imagen se define a través de un Dockerfile. Este archivo contiene todos los comandos que se deben ejecutar para ensamblar la imagen.

Ejemplo de Dockerfile

Este ejemplo (archivo Dockerfile-php7.4-apache) define una imagen de PHP 7.4 con Apache apta para un stack LAMP:

FROM php:7.4-apache

RUN apt update
RUN DEBIAN_FRONTEND=noninteractive apt upgrade -y 

RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
 libbz2-dev \
 curl \
 libcurl4 \
 libcurl4-openssl-dev \
 zlib1g-dev \
 libpng-dev \
 libicu-dev \
 libonig-dev \
 libedit-dev \
 libxml2-dev \
 libzip-dev \
 git \
 apt-utils

RUN docker-php-ext-install bz2
RUN docker-php-ext-install curl
RUN docker-php-ext-configure gd
RUN docker-php-ext-install -j$(nproc) gd
RUN docker-php-ext-install -j$(nproc) intl
RUN docker-php-ext-install json
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install opcache
RUN docker-php-ext-install readline
RUN docker-php-ext-install soap
RUN docker-php-ext-install xml
RUN docker-php-ext-install zip
RUN docker-php-ext-install pdo
RUN docker-php-ext-configure pdo_mysql --with-pdo-mysql=mysqlnd
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-configure mysqli --with-mysqli=mysqlnd
RUN docker-php-ext-install mysqli

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer

COPY ./build/apache2.conf /etc/apache2/sites-enabled/
RUN a2enmod rewrite

COPY ./build/php.ini /usr/local/etc/php/

Una imagen de Docker siempre parte de una imagen base (el Dockerfile debe comenzar con la sentencia FROM). Si examinamos, por ejemplo, la última versión disponible de la imagen httpd ("httpd:latest", servidor web httpd provisto por Apache), se observa que está basada en la imagen de Debian Buster (imagen "debian" versión "buster-slim"):

Para aquellos curiosos, una imagen origen (no basada en ninguna imagen existente) se define con FROM: scratch.

¿Dónde encuentro las imágenes?

Las imágenes se publican en "Hubs" (de igual forma que los repositorios git se publican en GitHub/GitLab). Existen muchos Hubs, aunque el proyecto Docker ofrece Docker Hub. Desde allí es posible descargar imágenes creadas por la comunidad o por los fabricantes como Red Hat, IBM o Google.

¿Qué es Docker Compose?

Docker Compose es una herramienta (utilitario de línea de comandos docker-compose) que permite definir y lanzar aplicaciones multi-container de forma programática (permite definir la arquitectura de una aplicación de forma declarativa). Con Compose se utiliza un archivo YAML para configurar los servicios necesarios para el funcionamiento de una aplicación. Para luego crear e iniciar todos los servicios a partir de la misma ejecutando un comando simple.

Ejemplo de docker-compose.yml

Este ejemplo demuestra cómo configurar dos contenedores, uno corriendo PHP 7.4 con Apache (a partir del Dockerfile ejemplificado anteriormente) y otro corriendo MySQL 5.7. De esta forma se completa un stack LAMP con Docker:

version: '3.8'

services:
    apache:
        container_name: apache
        hostname: apache.local
        build:
            context: .
            dockerfile: Dockerfile-php7.4-apache
        restart: always
        volumes:
            - ~/linuxito.com/:/var/www/linuxito.com/
            - ./var/apache/:/var/log/apache2
        ports:
            - 8080:80
        networks:
            - develop
        user: "${DOCKER_UID}:${DOCKER_GID}"
    mysql:
        container_name: mysql
        hostname: mysql.local
        image: mysql:5.7
        restart: always
        environment:
            MYSQL_DATABASE: 'linuxito'
            MYSQL_USER: 'admin'
            MYSQL_PASSWORD: 'abc123'
            MYSQL_ROOT_PASSWORD: '1234'
        volumes:
            - ./var/mysql:/var/lib/mysql
        ports:
            - '9306:3306'
        expose:
            - '3306'
        networks:
            - develop
        user: "${DOCKER_UID}:${DOCKER_GID}"

networks:
    develop:

Los contenedores se definen debajo de Services:, en este ejemplo se definen dos contenedores: "apache" y "mysql". Luego es posible definir redes y volúmenes (discos) aparte.

El nombre de cada contenedor se define en la variable "container_name". Este nombre será utilizado para gestionar el contenedor desde el host, por ejemplo para reiniciar el contenedor:

$ docker restart apache

Ambos contenedores se crean a partir de las imágenes oficiales de PHP y MySQL disponibles. El contenedor corriendo PHP+Apache se construye a partir de un Dockerfile ya que necesita una personalización adicional (instalación de librerías y extensiones de PHP necesarias). En cambio el contenedor de MySQL se levanta directamente a partir de la imagen oficial y se definen base de datos, usuarios y contraseñas directamente en el docker-compose.yml.

Volúmenes en Docker

Tal como se observa en la configuración anterior, ambos contenedores tienen una configuración de volúmenes específica. Docker permite acceder a un directorio del host (montar) desde el contenedor. Para ello basta definirlo como volumen:

        volumes:
            - ~/linuxito.com/:/var/www/linuxito.com/
            - ./var/apache/:/var/log/apache2

En esta configuración, se monta el directorio ~/linuxito.com/ (copia de trabajo de los archivos del sitio web a servir por Apache) en el directorio /var/www/linuxito.com del contenedor. Así el contenedor corriendo Apache puede servir directamente los archivos en el host.

Lo mismo ocurre con los logs de Apache en el contenedor, el directorio /var/log/apache2 es montado en el directorio del host ./var/apache/ (relativo al directorio actual donde se encuentra el archivo docker-compose.yml).

De esta forma, si se elimina el contenedor, no se pierde la copia de trabajo ni los logs de Apache. Al mismo tiempo es posible editar los archivos que sirve Apache dentro del contenedor directamente con un IDE en el host sin la necesidad de mover archivos modificados desde/hacia el contenedor:

Lo mismo ocurre con MySQL. El datadir del servidor de base de datos permanece en el host. Con lo cual es posible eliminar el contenedor sin perder las bases de datos. El directorio /var/lib/mysql del contenedor se monta desde el directorio ./var/mysql/ del host.

Notar que los directorios ~/linuxito.com/, ./var/apache/ y ./var/mysql/ son rutas relativas al directorio desde donde se debe ejecutar docker-compose.

Networking en Docker

Docker soporta diferentes tipos de drivers para dar conectividad a los contenedores (bridge, host, overlay y más). Si entrar en detalles de bajo nivel es posible asumir que, desde el punto de vista de un contenedor, el tipo de red usada es transparente.

Docker asigna por defecto una IP para cada red a la que se conecta un contenedor. Cabe destacar que los contenedores corren aislados del resto de los procesos del sistema. Entonces, para que dos contenedores en un mismo host puedan comunicarse entre ellos, deberá establecerse una conexión de red entre los mismos. En el ejemplo anterior, ambos contenedores son conectados a la red "develop", la cual es definida al final del archivo docker-compose.yml.

De forma simple: si dos contenedores están conectados a la misma red, podrán comunicarse entre ellos a través de su nombre de servicio. Por ejemplo, desde el contenedor "apache" es posible conectarse al motor MySQL simplemente utilizando el nombre de host "mysql".

Al mismo tiempo es posible definir qué puertos se exponen al exterior. Para el caso del contenedor "apache", se observa que el puerto 80 (HTTP) se expone al exterior a través del puerto 8080 del host:

        ports:
            - 8080:80

Cabe aclarar que no es posible que dos contenedores en el mismo host expongan un servicio al exterior a través del mismo puerto. Si una copia del contenedor Apache requiere exponer su puerto 80 al exterior, deberá hacerlo en otro puerto que no sea el 8080 (por ejemplo, 8081). Estas son cuetiones básicas de networking y sistemas operativos, pero vale aclararlas.

Usuarios y permisos

Para ambos contenedores se indica que deberán correr como el userid/groupid pasado como parámetro a través de las variables de entorno DOCKER_UID y DOCKER_GID respectivamente.

De esta forma, ambos contenedores pueden ser levantados a nombre de un usuario no privilegiado en una estación de trabajo. Tanto Apache como MySQL correrán a nombre de nuestro usuario local, el mismo usuario con el que editaremos los fuentes desde un IDE. Esto simplifica mucho el trabajo para los desarrolladores.

Gracias a esta configuración no es necesario realizar ningún tipo de ajustes de permisos en la copia de trabajo del repositorio de los archivos del sitio web a servir.

Ventajas de Docker

Más allá de las cuestiones de rendimiento vs. máquinas virtuales, algunas de las ventajas al adoptar Docker como entorno de desarrollo son las siguientes.

Mantenimiento y simplicidad

Desde el punto de vista del desarrollador, permite desentenderse de la instalación y mantenimiento de la arquitectura de software necesaria para el funcionamiento del entorno. Ya no hay que preocuparse por instalar, configurar y mantener actualizadas las versiones específicas de servicios, librerías y lenguajes en sus sistemas locales.

Esto redunda en menor cantidad de trabajo para instalar, mantener y resolver problemas con el entorno de desarrollo local.

Menos problemas con permisos debido a que todos los servicios corren a nombre de nuestro usuario de escritorio.

Consistencia

Desde el punto de vista del administrador, permite garantizar que los desarrolladores trabajen con las versiones específicas de servicios y librerías usadas por la plataforma, sin necesidad de recurrir a la creación y mantenimiento de máquinas virtuales (costosas en términos de recursos: cpu, disco y memoria). Además de lograr proveer de entornos de desarrollo locales de forma simple y ágil.

Con esta tecnología se garantiza que todos los desarrolladores trabajen con las mismas versiones y configuración que las utilizadas en el entorno de producción, independientemente del sistema operativo utilizado por cada uno.

¿Cómo eran los entornos locales anteriormente?:

Cada desarrollador utilizaba su propia versión de sistema operativo con diferentes versiones de Apache, PHP y MySQL instalados localmente. Para el caso de MySQL en particular existía la posibilidad de que las distribuciones reemplacen el paquete por MariaDB y versiones más actualizadas (no compatibles con la plataforma).

¿Cómo serán los entornos locales a partir de ahora?:

En este esquema, independientemente de la distribución GNU/Linux instalada, todos utilizarán los mismos contenedores, por ende correrán exactamente las mismas versiones de Apache, PHP y MySQL. En este esquema no se requiere instalar ni configurar Apache, PHP ni MySQL en el sistema operativo local, sino que todos estos servicios son creados automáticamente con docker-compose dentro de contenedores y a partir de las imágenes oficiales.

De esta forma todos los desarrolladores trabajan con las mismas versiones de servicios, lenguajes y bases de datos. Este último punto es de vital importancia para evitar problemas al testear y poner en producción.

Respecto a los sistemas operativos Microsoft Windows, es posible correr contenedores GNU/Linux en Windows 10 mediante Hyper-V.

Flexibilidad

¿Y si quiero desarrollar y testear sobre dos o más ramas diferentes al mismo tiempo? No hay problema, basta con levantar varias instancias del contenedor Apache:

Gracias a Docker es posible desarrollar y testear dos ramas diferentes del mismo proyecto al mismo tiempo en el mismo sistema local. Sólo basta con crear una nueva copia de trabajo y apuntarla desde una nueva instancia del contenedor de Apache+PHP. No hace falta duplicar el contenedor MySQL, sino que se puede compartir simplemente creando una nueva base de datos en la misma instancia.

Posibilidades a futuro...

Con la adopción de Docker se abre un abanico de posibilidades. Docker permite exportar un contenedor en funcionamiento para importarlo y ejecutarlo en otro sistema. Esto permite enviar a testing un contenedor local para hacer pruebas y levantar cualquier número de instancias apuntando a diferentes desarrollos.

Al mismo tiempo, comenzar a familiarizarse con esta tecnología (más temprano que tarde) nos permitirá disponer de una nueva herramienta para encarar nuevos proyectos y soluciones innovadoras, y por qué no considerar su uso en sistemas y aplicaciones en producción (ver ECS, Kubernetes, etc.).

Tutoriales para comenzar a trabajar con Docker

Referencias

Compartí este artículo