Docker Image
El proyecto Docker, con software homónimo, se ha establecido como el estándar de la virtualización de contenedores. Un concepto central en la utilización de la plataforma Docker es la Docker Image. En este artículo, te explicamos cómo se construye una imagen de Docker y cómo funciona.
¿Qué es una Docker Image?
Conocerás el concepto “image” posiblemente en relación con las máquinas virtuales (VM). Las imágenes de máquinas virtuales suelen ser una copia de un sistema operativo. Una imagen VM también puede contener otros componentes instalados, como bases de datos o servidores web. Este término procede de una época en la que el software se compartía en soportes de datos ópticos, como los CD-ROM o los DVD. Para crear una copia local del soporte de datos, tenías que crear una imagen con un software especial.
La virtualización basada en contenedores es lo que sigue a la virtualización con VM. En vez de virtualizar un ordenador virtual (máquina) con su propio sistema operativo, una imagen de Docker suele contener solo una aplicación. Puede tratarse de un solo archivo binario o de una agrupación de diversos componentes de software.
Para ejecutar la aplicación, se creará un contenedor a partir de la imagen. Todos los contenedores de un Docker Host suelen recurrir al mismo núcleo del sistema operativo, por eso, los contenedores y las imágenes de Docker suelen pesar bastante menos que el equivalente en máquinas virtuales.
Los conceptos Docker Container y Docker Image están estrechamente vinculados. No solo puede crearse un contenedor de Docker a partir de una imagen Docker, también puede crearse una nueva imagen desde un contenedor en ejecución, así que entre contenedor e imagen Docker hay una relación similar a la que hay entre gallina y huevo:
Comando Docker | Significado | Analogía huevo-gallina |
docker run <image-id> | Crear un Docker Container a partir de una Image | La gallina nace del huevo |
docker commit <container-id> | Crear una Docker Image a partir del Container | La gallina pone un huevo |
En el sistema biológico de la gallina y el huevo, un polluelo nace de un huevo. En el proceso, el huevo se pierde. En cambio, a partir de una Docker Image pueden crearse tantos Containers similares como se quiera. Esta reproducibilidad hace que Docker sea adecuado como plataforma de aplicaciones y servicios escalables.
Una imagen de Docker es una plantilla inmutable a partir de la cual se crean contenedores de Docker. La imagen contiene toda la información y las dependencias para ejecutar un contenedor. Esto incluye bibliotecas de programas básicos e interfaces de usuario. En particular, suele haber un entorno de línea de comandos (“Shell”) y una implementación de la biblioteca estándar C. A continuación, te presentamos la imagen oficial de “Alpine Linux”:
Núcleo Linux | Biblioteca estándar C | Comandos de Unix |
del Host | musl libc | BusyBox |
Además de estos componentes fundamentales que complementan el núcleo o kernel de Linux, una imagen Docker suele contener software adicional. Aquí te damos algunos ejemplos de componentes de software para distintos usos. Ten en cuenta que una única imagen Docker suele contener una pequeña selección de los componentes que mostramos:
Ámbito de aplicación | Componentes software |
Lenguajes de programación | PHP, Python, Ruby, Java, JavaScript |
Herramientas de desarrollo | node/npm, React, Laravel |
Sistemas de bases de datos | MySQL, Postgres, MongoDB, Redis |
Servidores web | Apache, nginx, lighttpd |
Caches y Proxys | Varnish, Squid |
Sistemas de gestión de contenido | WordPress, Magento, Ruby on Rails |
¿Qué diferencia hay entre una Docker Image y un Docker Container?
Como ya hemos visto, existe una estrecha interacción entre una Docker Image y un Container. ¿Qué características diferencian estos conceptos?
En primer lugar, observamos que una imagen Docker es inerte. Solo ocupa algo de espacio en la memoria, pero por lo demás no consume ningún recurso del sistema. Además, una Docker Image es inmutable una vez se crea, por lo que es un medio de “solo lectura” o protegido contra la escritura. Una pequeña nota al respecto: sí, es posible añadir cambios a una imagen Docker existente. Lo que ocurre es que esto crea una nueva imagen; la imagen original sigue existiendo en la versión no modificada.
Como hemos mencionado anteriormente, es posible crear tantos contenedores similares como queramos a partir de una imagen Docker. Entonces, ¿qué diferencia exactamente a un contenedor Docker de una imagen? Un contenedor Docker es una instancia en funcionamiento (deberíamos decir, en ejecución) de una imagen Docker. Como cualquier software que se ejecuta en un ordenador, un contenedor Docker en funcionamiento consume recursos del sistema como memoria y ciclos de CPU. Además, el estado de un contenedor cambia a lo largo de su vida.
Si esta descripción te parece demasiado abstracta, quizá te sea de ayuda un ejemplo sencillo del día a día. Imagínate que una Docker Image es un DVD. El DVD como tal es inerte; está en su carcasa y no hace nada. Sin embargo, ocupa permanentemente todo y al mismo tiempo un espacio limitado. Solo cuando se reproduce en un entorno especial (un reproductor de DVD), el contenido “cobra vida”.
Al igual que la película creada para reproducirse en DVD, un contenedor Docker en ejecución tiene un estado. En el caso de una película, son el tiempo de reproducción actual, el idioma seleccionado, los subtítulos, etc. Este estado cambia con el tiempo, y una película en reproducción consume constantemente electricidad en el proceso. Al igual que los contenedores similares pueden iniciarse desde una imagen Docker tantas veces como se desee, la película de un DVD puede reproducirse una y otra vez. Además, la película en ejecución puede detenerse y reiniciarse, algo que también es posible con un contenedor Docker.
Concepto Docker | Analogía | Modo | Estado | Consumo de recursos |
Docker Image | DVD | inerte | Read only / invariable | Fijo |
Docker Container | Reproducción | Película que se reproduce | Cambia con el tiempo | Varía según el uso |
¿Cómo y dónde se utilizan las imágenes Docker?
Docker se utiliza actualmente en todas las fases del ciclo vital del software: desarrollo, testeo y uso. El concepto central en el ecosistema Docker es el contenedor, y para crearlo siempre necesitamos una imagen. Las Docker Images son útiles siempre que se utilice Docker. Echemos un vistazo a un par de ejemplos:
Docker Images en entornos de desarrollo locales
Si desarrollamos software en un dispositivo propio, querremos que el entorno de desarrollo local se mantenga tan consistente como sea posible. En la mayoría de casos, necesitamos versiones exactas e idénticas en términos de lenguaje de programación, bibliotecas y otros componentes de software. Desde que se modifique al menos uno de los muchos planos, surgirán rápidamente problemas desoladores, como que el código fuente no se compile o que el servidor web no se inicie. En este caso, la invariabilidad de la Docker Image hace maravillas: como desarrollador, puedes confiar en que el entorno que contiene la imagen siga siendo consistente.
Los grandes proyectos de desarrollo son liderados por equipos. Aquí es requisito previo utilizar un entorno que se mantenga estable con el tiempo en pro de la comparabilidad y reproducibilidad. Todos los desarrolladores de un mismo equipo recurren a la misma Image. Si llega un desarrollador nuevo al equipo, recibirá la imagen Docker adecuada y podrá empezar de inmediato. Para realizar cambios en el entorno de desarrollo, se creará una nueva imagen Docker. Los desarrolladores entrarán a la nueva imagen y estarán al día inmediatamente.
Docker Image en la arquitectura orientada a servicios (SOA)
Las Docker Images son la base de la arquitectura orientada a servicios moderna. En lugar de una aplicación única y monolítica, se desarrollan servicios individuales con interfaces bien definidas. Cada servicio está empaquetado en su propia imagen. Los contenedores lanzados a partir de ella se comunican entre sí a través de la red y establecen así la funcionalidad global de la aplicación. La encapsulación en imágenes Docker separadas permite desarrollar y mantener los servicios de forma independiente. Los servicios individuales pueden incluso estar escritos en diferentes lenguajes de programación.
Docker Image para proveedores de alojamiento/PaaS
Las imágenes Docker también se utilizan en el centro de datos. Cada servicio, como el equilibrador de carga, el servidor web, el servidor de base de datos, etc., se define como Docker Image. Cada uno de los contenedores creados a partir de ellas puede manejar una determinada carga. Un software orquestador supervisa los contenedores, su carga y su estado. Si la carga aumenta, el orquestador inicia contenedores adicionales desde la imagen correspondiente. Este enfoque permite que los servicios se amplíen rápidamente respondiendo a las condiciones cambiantes.
¿Cómo se construye una Docker Image?
A diferencia de lo que sucede con las imágenes de máquinas virtuales, una Docker Image en estado normal no es un archivo individual. Se trata más bien de una agrupación de distintos componentes. He aquí un resumen (más detalles a continuación):
- Capas de imágenes: contienen datos añadidos al operar en el sistema de archivos. Las layers o capas se superponen y reducen a un nivel coherente mediante un sistema de archivos de unión.
- Imagen padre: facilita las funciones fundamentales de la imagen y ancla la imagen al árbol genealógico del ecosistema Docker.
- Manifiesto de imagen: describe la agrupación e identifica las capas de imágenes que contiene.
¿Qué pasa cuando queremos trasladar una imagen Docker a un archivo individual? Esto también es posible con el comando “docker save” de la línea de comandos. Se creará un archivo tar, que puede moverse entre sistemas con bastante normalidad. Con el siguiente comando, se escribirá una Docker Image con el nombre “busybox” en un archivo “busybox.tar”:
docker save busybox > busybox.tar
Muchas veces, al accionar “docker save” en la línea de comandos se canaliza a Gzip, de manera que tras la salida los datos se comprimen en el archivo tar:
docker save myimage:latest | gzip > myimage_latest.tar.gz
Con “docker load”, se ingresará como imagen Docker en el host de Docker un archivo de imagen creado con “Docker save”:
docker load busybox.tar
Capas de imágenes
Una imagen Docker está compuesta de capas de solo lectura, en inglés “layers”. Cada capa describe cambios sucesivos en el sistema de archivos de la imagen. Por cada operación que provoque un cambio en el sistema de archivos de la imagen, se crea una nueva capa. El enfoque utilizado suele denominarse “copy‑on‑write”. El acceso de escritura crea una copia modificada en una nueva capa y los datos originales no se alteran. Quizá este principio te suene de algo, y es que el software de control de versiones Git sigue el mismo patrón.
Podemos mostrar las capas de una imagen Docker. Para ello, utilizamos el comando “Docker image inspect” de la línea de comandos. Esta llamada devuelve un documento JSON que procesamos con la herramienta estándar jq:
docker image inspect <image-id> | jq -r '.[].RootFS.Layers[]'
Para reunir los cambios contenidos en las capas, se utiliza un sistema de archivos especial. Este sistema de archivos de unión se superpone a todas las capas para dar lugar a una estructura de carpetas y archivos coherente en la superficie. Históricamente, se utilizaban varias tecnologías conocidas como controladores de almacenamiento (o storage driver en inglés) para implementar el sistema de archivos de la unión. En la actualidad, para la mayoría de los casos se recomienda el controlador de almacenamiento overlay2:
Controlador de almacenamiento | Comentario |
overlay2 | Recomendado actualmente |
aufs, overlay | Utilizado en versiones anteriores |
Es posible visualizar el controlador de almacenamiento de una imagen de Docker. Para ello, utilizaremos el comando “Docker image inspect” de la línea de comandos. Esta llamada devuelve un documento JSON que procesamos con la herramienta estándar jg:
docker image inspect <image-id> | jq -r '.[].GraphDriver.Name'
Cada capa de la imagen se identifica con un hash único que se calcula a partir de los cambios que contiene. Si dos imágenes utilizan la misma capa, localmente solo se almacena una vez. Entonces, ambas imágenes acceden a la misma capa. Así se consigue un almacenamiento local eficaz y se reducen las cantidades de transferencia al recuperar las imágenes.
Imagen padre
Una Docker Image suele basarse en una “Parent Image” (imagen padre). En la mayoría de los casos, la imagen padre se ha creado mediante una instrucción FROM en el Dockerfile. La imagen padre define una base a partir de la cual se construyen las imágenes que derivan de ella. Las capas de imagen existentes se superponen mediante capas adicionales.
Al “heredar” de la imagen padre, la imagen de Docker se ubica en un árbol genealógico que incluye todas las imágenes existentes. Quizá ya te estés preguntando dónde empieza el árbol genealógico. Las raíces del árbol genealógico están definidas por unas “imágenes base” especiales. En la mayoría de los casos, una imagen base se define con la instrucción “FROM scratch” en el Dockerfile, aunque hay otras formas de crear una imagen base. Más información sobre esto en la sección “¿De dónde vienen las imágenes Docker?”
Manifiesto de imagen
Como hemos visto, una Docker Image consta de varias capas. Si utilizamos el comando “docker image pull” para obtener una imagen Docker de un registro en línea, no se descarga ningún archivo de imagen. En vez de eso, el Daemon de Docker local descargará las capas individuales y las guardará. ¿De dónde procede la información de las distintas capas?
La información sobre qué capas de imagen componen una Docker Image viene en el llamado manifiesto de la imagen. El manifiesto de la imagen es un archivo JSON que describe totalmente una Docker Image. Entre otras cosas, el manifiesto de imagen contiene:
- Información sobre tamaño, diagrama y versión
- Hashes criptográficos de las capas de imagen que se utilizan
- Información sobre las arquitecturas de los procesadores disponibles
Para identificar claramente una Docker Image, se calcula un hash criptográfico del manifiesto de la imagen. Al accionar “docker image pull”, primero se descarga el archivo de manifiesto. El Daemon de Docker local extrae entonces las capas individuales de la imagen.
¿De dónde vienen las imágenes de Docker?
Tal y como hemos observado, las Docker Images son una parte importante del ecosistema Docker. Hay numerosas opciones para hacerse con una Docker Image. Distinguimos dos formas básicas, cuyas manifestaciones te mostramos más abajo:
- Extraer una Docker Image ya existente del Registry
- Crear una Docker Image nueva
Extraer una Docker Image de Registry
Muchas veces un proyecto Docker empieza con el paso de extraer una imagen Docker existente de un llamado Registry, que es una plataforma accesible mediante la red que proporciona imágenes Docker. El host Docker local se comunica con el Registry para descargarse una Docker Image tras el comando “docker image pull”.
Por un lado, hay registries online públicos y accesibles, que ofrecen una gran selección de Docker Images existentes. En el registry oficial de Docker “Docker Hub” hay, en el momento de redacción del artículo, más de ocho millones de Docker Images libres disponibles en la lista. El Azure Container Registry de Microsoft contiene además de Docker Images otras imágenes de contenedores en distintos formatos. Además, la plataforma permite a los usuarios crear sus propios registries de contenedores no públicos.
Además de los registries en línea mencionados anteriormente, también es posible alojar un registry local. Es algo que utilizan, por ejemplo, las grandes empresas para dar a sus propios equipos acceso protegido a las imágenes Docker que ellos mismos han creado. Para ello, la empresa Docker ofrece el “Docker Trusted Registry” (DTR), una solución local para proporcionar un registro interno de la empresa en su propio centro de datos.
Crear una nueva Docker Image
A veces queremos crear una imagen Docker que se adapte especialmente a un proyecto. Por lo general, usamos una Docker Image existente y la adaptamos según sea necesario. Pero recuerda que las Docker Images son inmutables. Si mantenemos los cambios, el resultado será una nueva imagen Docker. Existen las siguientes formas de crear una nueva Docker Image:
1. Construirla desde la imagen padre con Dockerfile
2. Crearla desde un contenedor en ejecución
3. Crear una nueva imagen base
Probablemente el enfoque más común para crear una nueva Docker Image es escribir un Dockerfile. El Dockerfile contiene instrucciones especiales que especifican la imagen padre así como todos los cambios necesarios. Accionando “docker image build” se crea la nueva Docker Image desde el Dockerfile. He aquí un ejemplo sencillo:
# Crear Dockerfile desde la línea de comandos
cat <<EOF > ./Dockerfile
FROM busybox
RUN echo "hello world"
EOF
# Crear Docker Image desde Dockerfile
docker image build
Históricamente, el concepto “Image” procede de la reproducción de un soporte de datos (“to image” en inglés). En el contexto de las máquinas virtuales (VM), se crea una instantánea de una imagen de VM en ejecución. Con Docker es posible hacer algo parecido. Con el comando “Docker commit”, creamos una reproducción de un contenedor en ejecución como nueva Docker Image. Todas las modificaciones efectuadas en el contenedor serán almacenadas:
docker commit <container-id>
También existe la opción de pasar instrucciones de Dockerfile al comando “docker commit”. Los cambios codificados con las instrucciones pasan a formar parte de la nueva Docker Image:
docker commit --change <dockerfile instructions> <container-id>
Para poder rastrear después qué cambios han llevado a una Docker Image, utilizamos el comando “docker image history”:
docker image history <image-id>
Como ya hemos visto, basamos una nueva Docker Image en una imagen padre o estado de un contenedor en ejecución. ¿Pero qué pasa si quieres empezar desde cero con una nueva imagen Docker? Hay dos maneras de proceder. Puedes utilizar un Dockerfile con la instrucción especial “FROM scratch”, tal y como expusimos anteriormente. Como resultado, se crea una nueva y mínima imagen base.
Si incluso quieres prescindir de la imagen scratch de Docker, puedes utilizar una herramienta especial como debootstrap y preparar con ella una distribución de Linux. A continuación, se empaquetará en un archivo “tarball” mediante el comando tar y se importará en el host local de Docker mediante “docker image import”.
Los comandos Docker Image más importantes
Comando Docker Image | Definición |
docker image build | Crear una Docker Image a partir de Dockerfile |
docker image history | Mostrar los pasos para crear una Docker Image |
docker image import | Crear una Docker Image a partir de un archivo Tarball |
docker image inspect | Mostrar información detallada sobre una Docker Image |
docker image load | Cargar un archivo de imagen creado con “docker image save” |
docker image ls / docker images | Ver imágenes disponibles en el host de Docker |
docker image prune | Eliminar Docker Images no utilizadas del host de Docker |
docker image pull | Extraer Docker Image del Registry |
docker image push | Enviar Docker Image al Registry |
docker image rm | Eliminar Docker Image del host de Docker local |
docker image save | Crear un archivo de imagen |
docker image tag | Etiquetar Docker Image |