Docker - Docker Images

Tutorial #4

Introducción

Una imagen de docker, también conocida como docker image, es un archivo que se utiliza para ejecutar el código de una aplicación dentro de un contenedor. Las imágenes funcionan como un conjunto de instrucciones para construir un contenedor docker, similar a una plantilla. Podemos comparar una imagen con un snapshot en el contexto de las máquinas virtuales.

Una imagen contiene el código de una aplicación, librerías, herramientas, dependencias y en general cualquier archivo necesario para que la aplicación pueda ser ejecutada. A partir de una imagen se puede crear una o varios (muchos) contenedores. Una característica importante de las imágenes es que son inmutables, es decir, no cambian entre ejecuciones. Con cada imagen se obtienen los mismos resultados.

Como mencionamos previamente, una imagen está conformada por múltiples capas. Cada capa se origina de una capa previa, pero es diferente de esta. Todas las capas son de solo lectura y los cambios se almacenan en la siguiente capa superior durante la creación de la imagen y cada capa solo tiene un conjunto de diferencias con respecto a la capa anterior.

Las capas están empaquetadas para facilitar el transporte entre diferentes entornos e incluyen información sobre la arquitectura requerida para ejecutarse. Las imágenes incluyen información sobre cómo se debe ejecutar el proceso, dónde persiste la información, qué puertos expondrá el proceso para comunicarse, etc.

En el caso de docker, las imágenes se pueden construir con métodos reproducibles usando Dockerfiles o almacenar cambios realizados en contenedores en ejecución para obtener una nueva imagen.

Estructura de una imagen

Una imagen de docker tiene muchas capas y cada imagen incluye todo lo necesario para configurar un entorno de contenedor: bibliotecas del sistema, herramientas, dependencias y otros archivos. Algunas de las partes de una imagen incluyen:

  • Imagen base: El usuario puede construir esta primera capa completamente desde cero con el comando de construcción.

  • Imagen padre: Es una imagen reutilizada que sirve como base para todas las demás capas.

  • Capas: Las capas se agregan a la imagen base mediante un código que permitirá que se ejecute en un contenedor. Todas estas capas son de solo lectura.

  • Capa de contenedores: Una imagen de docker no solo crea un nuevo contenedor, si no también una capa contenedora o de escritura. Esta capa aloja los cambios realizados en el contenedor en ejecución y almacena los archivos recién escritos y eliminados, así como los cambios en los archivos existentes. Esta capa también se utiliza para personalizar contenedores en ejecución.

  • Manifiesto de docker: Esta parte de la imagen de docker es un archivo adicional. Utiliza el formato JSON para describir la imagen, utilizando información como etiquetas de imagen y firma digital.

Dockerfile

docker crea una imagen automáticamente leyendo las instrucciones de un archivo Dockerfile. El Dockerfile es un documento que contiene todos los comandos necesarios para ensamblar una imagen. Al utilizar el comando docker build crea una compilación automatizada que ejecuta las instrucciones del Dockerfile para generar una nueva imagen.

Vamos a crear un archivo Dockerfile para crear una imagen con una aplicación que hemos construido previamente. Esta aplicación está escrita en Python con el framework Flask y lo que hace es exponer una API Rest con dos endpoints.

La aplicación podemos descargarla desde el siguiente repositorio en GitLab: https://gitlab.com/jpadillaa/sasaki-python/

Clone o descargue el repositorio localmente. En el readme del proyecto encontrará las instrucciones necesarias para ejecutar la aplicación si desea hacerlo antes de empaquetarla en una imagen de docker. Podemos ejecutar el comando:

$ git clone git@gitlab.com:jpadillaa/sasaki-python.git

Para empaquetar la imagen, en la carpeta del proyecto vamos a crear un archivo llamado Dockerfile, sin extensión:

$ nano Dockerfile

Vamos a crear un archivo a partir de una imagen que contiene la última versión de Python disponible. Podemos especificar una versión en particular usando el tag respectivo. A esta imagen le vamos a instalar todas las dependencias de Flask que requiere nuestra aplicación y finalmente vamos a copiar la aplicación a la imagen y especificar como la vamos a ejecutar.

FROM python

RUN pip install flask
RUN pip install flask-restful
RUN pip install flask-marshmallow

COPY . /opt/api

ENTRYPOINT FLASK_APP=/opt/api/app.py flask run -h 0.0.0.0

Expliquemos un poco el archivo:

FROM debe ser la primera instrucción sin comentarios en el Dockerfile. Los valores de la etiqueta son opcionales. Si omite alguno de ellos, durante el proceso de construcción docker asume la etiqueta latest de forma predeterminada. El constructor devuelve un error si el valor de la etiqueta no es correcto.

Alternativas de uso:

FROM <image>
FROM <image>:<tag>

RUN es la directiva o la instrucción central principal en un Dockerfile para ejecutar instrucciones en el momento de construir una imagen de docker. Cada RUN genera una nueva capa.

COPY copia nuevos archivos o directorios desde un punto de origen al sistema de archivos de la imagen en una ruta específica de destino. En nuestro Dockerfile ejecutamos la instrucción:

COPY . /opt/api

El operador punto (.) indica que el origen es el directorio actual, con todos sus archivos y subdirectorios incluidos. El valor /opt/api es la ruta de destino localizada en la imagen de docker que estamos creando donde vamos a copiar todos los archivos y subdirectorios de nuestro proyecto.

Finalmente, ENTRYPOINT especifica una instrucción que se ejecute cuando sé instancia un contenedor a partir de esta imagen. A diferencia de RUN que ejecuta instrucciones durante el proceso de generación de la imagen y genera una nueva capa por cada instrucción ejecutada, ENTRYPOINT define el conjunto de instrucciones que se deben ejecutar cuando se crea un nuevo contenedor.

Como podemos observar lo que estamos indicando con ENTRYPOINT en nuestro Dockerfile es que al generarse un nuevo contenedor levante el servidor de nuestra aplicación Flask.

ENTRYPOINT FLASK_APP=/opt/api/app.py flask run -h 0.0.0.0

Para construir la imagen a partir del Dockerfile generado se requiere utilizar el comando build de docker. El comando docker build solo requiere como parámetro el directorio donde se localiza el archivo Dockerfile, encontrándonos en el mismo directorio para generar la imagen, bastaría utilizar el operador punto (.).

$ sudo docker build .

Sin embargo, ejecutar el comando de esta manera tiene un problema, la imagen generada no tendrá nombre ni etiqueta, razón por la cual se recomienda utilizar el parámetro -t para asignar un nombre y una etiqueta a la imagen.

$ sudo docker build -t image_name .
$ sudo docker images

El resultado de la ejecución anterior es que tiene una nueva imagen con el nombre indicado y con la etiqueta latest por defecto. Para especificar una nueva etiqueta bastará con utilizar el operador dos puntos (:) luego del nombre de la imagen y especificando el valor de la etiqueta.

$ sudo docker build -t image_name:latest 
$ sudo docker images

Incluso podemos especificar varias etiquetas para la misma imagen en una sola línea:

$ sudo docker build -t image_name:latest -t image_name:v1.0 .

Finalmente, cree un contenedor a partir de esta imagen y desde una herramienta como Postman valide si los endpoints del API responden sus peticiones.

$ sudo docker run -d -p 5000:5000 --name container_name image_name

Demostración

En esta demostración, vamos a crear una imagen personalizada para una aplicación clásica de "Hola Mundo", desarrollada en Python y el framework FastAPI. A través de este ejemplo, exploraremos cómo encapsular la aplicación web en un contenedor Docker. No nos enfocaremos en el código de la aplicación en sí, sino en el archivo de configuración Dockerfile y en los comandos clave involucrados en esta demostración.

En primer lugar, crea un directorio llamado 'demo' y, dentro de este, crea otro directorio nuevo llamado 'src'. Dentro del directorio 'src', debe crear un archivo de Python llamado 'main.py' (este archivo corresponderá a nuestra aplicación web).

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

Regrese a la carpeta principal y, dentro del directorio 'demo', crea un archivo llamado 'requirements.txt' que contenga las siguientes dependencias.

fastapi
uvicorn

Ahora, debe crear un archivo llamado 'Dockerfile' en la misma carpeta 'demo'.

FROM python:3.10

WORKDIR /src

COPY requirements.txt ./

RUN pip install -r requirements.txt

COPY . .

EXPOSE 8000

CMD uvicorn src.main:app --host 0.0.0.0 --port 8000

Este Dockerfile crea un contenedor basado en Python 3.10, configura un directorio de trabajo, instala las dependencias del archivo requirements.txt, copia los archivos de la aplicación Python, expone el puerto 8000 y finalmente ejecuta la aplicación utilizando el servidor uvicorn.

Este Dockerfile realizará lo siguiente:

  1. Utilizará la imagen oficial de Python 3.10 como imagen base.

  2. Creará un directorio de trabajo llamado /app.

  3. Copiará el archivo requirements.txt al directorio de trabajo.

  4. Instalará las dependencias enlistadas en el archivo requirements.txt.

  5. Copiará todo el directorio del proyecto al directorio de trabajo.

  6. Expondrá el puerto 8000.

  7. CMD uvicorn src.main:app --host 0.0.0.0 --port 8000 define el comando por defecto que se ejecutará cuando el contenedor se inicie. En este caso, se está usando el servidor ASGI uvicorn para ejecutar la aplicación definida en el archivo main.py dentro del directorio src. El host 0.0.0.0 permite que el contenedor escuche en todas las interfaces de red, y el puerto 8000 coincide con el puerto que fue expuesto.

Para construir la imagen de Docker, puedes utilizar el siguiente comando:

docker build -t hello-api .

Nota: Puede asignar un nombre diferente a hello-api.

Esto creará una imagen de Docker llamada hello-api. Luego, puede crear un contenedor a partir de esta imagen de Docker, utilizando el siguiente comando:

docker run -it -p 8000:8000 hello-api

Esto iniciará el servidor de desarrollo de FastAPI en el puerto 8000. Luego, podrá acceder a la API abriendo un navegador web e ingresando a la siguiente URL:

http://localhost:8000/

Deberías ver el texto "¡Hola, mundo!" en el cuerpo de la respuesta.

Ejercicio

Actualice su archivo Dockerfile para crear una nueva imagen, en este caso utilizando como imagen base python:slim. Instrucciones

  • Verifique el peso actual de la imagen creada

  • Valide que la imagen seleccionada tenga un peso inferior a 200 MB

  • Actualice su Dockerfile especificando que imagen base de Python va a utilizar

  • Construya la imagen especificando la etiqueta slim

  • Verifique y compare el peso de las dos imágenes generadas en este tutorial

  • Cree un contenedor a partir de esta imagen y realice las pruebas respectivas de la aplicación

Referencias

Para profundizar en los Dockerfile recomiendo las siguientes referencias:

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!