Agregar valor con imágenes de Docker personalizadas

Beginner

This tutorial is from open-source community. Access the source code

Introducción

En este laboratorio, construiremos sobre los conocimientos adquiridos en el laboratorio 1, donde usamos comandos de Docker para ejecutar contenedores. Crearemos una imagen personalizada de Docker construida a partir de un Dockerfile. Una vez que construyamos la imagen, la subiremos a un registro central donde se puede extraer para ser desplegada en otros entornos. Además, describiremos brevemente las capas de imagen y cómo Docker incorpora el "copiado al escribir" y el sistema de archivos union para almacenar eficientemente imágenes y ejecutar contenedores.

Usaremos algunos comandos de Docker en este laboratorio. Para obtener la documentación completa de los comandos disponibles, consulte la documentación oficial.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 83%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Crea una aplicación de Python (sin usar Docker)

Ejecuta el siguiente comando para crear un archivo llamado app.py con un programa de Python simple. (Copiar y pegar todo el bloque de código)

cd ~/project
echo 'from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello world!"

if __name__ == "__main__":
    app.run(host="0.0.0.0")' > app.py

Esta es una simple aplicación de Python que utiliza Flask para exponer un servidor web HTTP en el puerto 5000 (5000 es el puerto predeterminado de Flask). No te preocupes si no estás muy familiarizado con Python o Flask, estos conceptos se pueden aplicar a una aplicación escrita en cualquier lenguaje.

Opcional: Si tienes Python y pip instalados, puedes ejecutar esta aplicación localmente. Si no, pasa al siguiente paso.

$ python3 --version
$ pip3 --version
$ pip3 install flask

$ python3 app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Abre la aplicación en una nueva pestaña del navegador usando http://0.0.0.0:5000/.

Flask app browser output

Crea y construye la imagen de Docker

Ahora, ¿y si no tienes Python instalado localmente? ¡No te preocupes! Porque no lo necesitas. Una de las ventajas de usar contenedores es que puedes instalar Python dentro de tus contenedores, sin tener que instalar Python en tu máquina host.

Crea un Dockerfile ejecutando el siguiente comando. (Copiar y pegar todo el bloque de código)

echo 'FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py' > Dockerfile

Un Dockerfile enumera las instrucciones necesarias para construir una imagen de Docker. Vamos a revisar el archivo anterior línea por línea.

FROM python:3.8-alpine Este es el punto de partida de tu Dockerfile. Todo Dockerfile debe comenzar con una línea FROM que es la imagen base sobre la cual se construirán tus capas.

En este caso, estamos seleccionando la capa base python:3.8-alpine (ver Dockerfile para python3.8/alpine3.12) ya que ya tiene la versión de Python y pip que necesitamos para ejecutar nuestra aplicación.

La versión alpine significa que utiliza la distribución Alpine Linux, que es significativamente más pequeña que muchas otras versiones de Linux alternativas, alrededor de 8 MB de tamaño, mientras que una instalación mínima en disco podría ser de alrededor de 130 MB. Una imagen más pequeña significa que se descargará (desplegará) mucho más rápido, y también tiene ventajas en seguridad porque tiene una superficie de ataque más pequeña. Alpine Linux es una distribución de Linux basada en musl y BusyBox.

Aquí estamos usando la etiqueta "3.8-alpine" para la imagen de Python. Echa un vistazo a las etiquetas disponibles para la imagen oficial de Python en el Docker Hub. Es una buena práctica usar una etiqueta específica cuando se hereda una imagen padre para controlar los cambios en la dependencia padre. Si no se especifica una etiqueta, la etiqueta "latest" entrará en vigor, que actúa como un puntero dinámico que apunta a la última versión de una imagen.

Por razones de seguridad, es muy importante entender las capas sobre las que se construye tu imagen de Docker. Por eso, se recomienda encarecidamente usar solo imágenes "oficiales" encontradas en el docker hub, o imágenes no comunitarias encontradas en el docker-store. Estas imágenes son revisadas para cumplir ciertos requisitos de seguridad, y también tienen una muy buena documentación para que los usuarios la sigan. Puedes encontrar más información sobre esta imagen base de Python, así como todas las otras imágenes que puedes usar, en el docker hub.

Para una aplicación más compleja, es posible que necesites usar una imagen FROM que esté más arriba en la cadena. Por ejemplo, el Dockerfile padre de nuestra aplicación de Python comienza con FROM alpine, luego especifica una serie de comandos CMD y RUN para la imagen. Si necesitabas un control más detallado, podrías comenzar con FROM alpine (o una distribución diferente) y ejecutar esos pasos tú mismo. Para empezar, sin embargo, recomiendo usar una imagen oficial que se ajuste a tus necesidades.

RUN pip install flask El comando RUN ejecuta los comandos necesarios para configurar tu imagen para tu aplicación, como instalar paquetes, editar archivos o cambiar permisos de archivos. En este caso estamos instalando flask. Los comandos RUN se ejecutan durante la construcción y se agregan a las capas de tu imagen.

CMD ["python","app.py"] CMD es el comando que se ejecuta cuando se inicia un contenedor. Aquí estamos usando CMD para ejecutar nuestra aplicación de Python.

Pueden haber solo un CMD por Dockerfile. Si se especifican más de un CMD, entonces el último CMD entrará en vigor. La imagen padre python:3.8-alpine también especifica un CMD (CMD python3). Puedes encontrar el Dockerfile para la imagen oficial python:alpine aquí.

Puedes usar la imagen oficial de Python directamente para ejecutar scripts de Python sin instalar Python en tu host. Pero hoy, estamos creando una imagen personalizada para incluir nuestro código fuente, para que podamos construir una imagen con nuestra aplicación y enviarla a otros entornos.

COPY app.py /app.py Esto copia el app.py en el directorio local (donde ejecutarás docker image build) en una nueva capa de la imagen. Esta instrucción es la última línea en el Dockerfile. Las capas que cambian con frecuencia, como copiar el código fuente en la imagen, deben ubicarse cerca del final del archivo para aprovechar al máximo la memoria caché de las capas de Docker. Esto nos permite evitar la reconstrucción de capas que de otra manera podrían estar en caché. Por ejemplo, si hubiera un cambio en la instrucción FROM, invalidaría la memoria caché para todas las capas subsiguientes de esta imagen. Lo demostraremos un poco más adelante en este laboratorio.

Parece contraintuitivo poner esto después de la línea CMD ["python","app.py"]. Recuerda, la línea CMD se ejecuta solo cuando se inicia el contenedor, por lo que no obtendremos un error de archivo no encontrado aquí.

Y ahí lo tienes: un Dockerfile muy simple. Una lista completa de comandos que puedes poner en un Dockerfile se puede encontrar aquí. Ahora que definimos nuestro Dockerfile, usémoslo para construir nuestra imagen personalizada de Docker.

Construye la imagen de Docker.

Pasa -t para nombrar tu imagen python-hello-world.

docker image build -t python-hello-world.

Verifica que tu imagen aparezca en tu lista de imágenes.

docker image ls

Nota que tu imagen base python:3.8-alpine también está en tu lista.

Puedes ejecutar un comando de historial para mostrar el historial de una imagen y sus capas,

docker history python-hello-world
docker history python:3.8-alpine

Ejecuta la imagen de Docker

Ahora que has construido la imagen, la puedes ejecutar para comprobar que funciona.

Ejecuta la imagen de Docker

docker run -p 5001:5000 -d python-hello-world

La bandera -p mapea un puerto que se ejecuta dentro del contenedor a tu host. En este caso, estamos mapeando la aplicación de Python que se ejecuta en el puerto 5000 dentro del contenedor, al puerto 5001 en tu host. Tenga en cuenta que si el puerto 5001 ya está en uso por otra aplicación en su host, es posible que tenga que reemplazar 5001 con otro valor, como 5002.

Navega a la pestaña PUERTOS en la ventana del terminal y haz clic en el enlace para abrir la aplicación en una nueva pestaña del navegador.

Terminal ports tab link

En un terminal, ejecuta curl localhost:5001, que devuelve hello world!.

Verifica la salida de los registros del contenedor.

Si quieres ver los registros de tu aplicación, puedes usar el comando docker container logs. Por defecto, docker container logs imprime lo que se envía a la salida estándar por tu aplicación. Utiliza docker container ls para encontrar el id de tu contenedor en ejecución.

labex:project/ $ docker container ls
CONTAINER ID   IMAGE                COMMAND           CREATED         STATUS         PORTS                                       NAMES
52df977e5541   python-hello-world   "python app.py"   2 minutes ago   Up 2 minutes   0.0.0.0:5001->5000/tcp, :::5001->5000/tcp   heuristic_lamport
labex:project/ $ docker container logs 52df977e5541
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.2:5000
Press CTRL+C to quit
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [23/Jan/2024 02:43:10] "GET /favicon.ico HTTP/1.1" 404 -

El Dockerfile es cómo creas construcciones reproducibles para tu aplicación. Un flujo de trabajo común es que tu automatización de CI/CD ejecute docker image build como parte de su proceso de compilación. Una vez que se construyen las imágenes, se enviarán a un registro central, donde pueden accederse todos los entornos (como un entorno de prueba) que necesiten ejecutar instancias de esa aplicación. En el siguiente paso, vamos a subir nuestra imagen personalizada al registro público de Docker: el Docker Hub, donde otros desarrolladores y operadores pueden consumirla.

Sube a un registro central

Navega a Docker Hub y crea una cuenta si aún no la tienes. Alternativamente, también puedes usar https://quay.io, por ejemplo.

Para este laboratorio, usaremos Docker Hub como nuestro registro central. Docker Hub es un servicio gratuito para almacenar imágenes disponibles públicamente, o puedes pagar para almacenar imágenes privadas. Ve a la página web de Docker Hub y crea una cuenta gratuita.

La mayoría de las organizaciones que usan Docker intensivamente establecerán su propio registro internamente. Para simplificar las cosas, usaremos Docker Hub, pero los siguientes conceptos se aplican a cualquier registro.

Inicia sesión

Puedes iniciar sesión en la cuenta del registro de imágenes escribiendo docker login en tu terminal, o si estás usando podman, escribe podman login.

labex:project/ $ export DOCKERHUB_USERNAME=<tu_nombre_de_usuario_de_docker>
labex:project/ $ docker login docker.io -u $DOCKERHUB_USERNAME
Contraseña:
ADVERTENCIA: Tu contraseña se almacenará sin encriptar en /home/labex/.docker/config.json.
Configura un ayudante de credenciales para quitar esta advertencia. Ver
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Inicio de sesión exitoso

Etiqueta tu imagen con tu nombre de usuario

La convención de nombres de Docker Hub es etiquetar tu imagen con [nombre de usuario de dockerhub]/[nombre de imagen]. Para hacer esto, vamos a etiquetar nuestra imagen previamente creada python-hello-world para que se ajuste a ese formato.

docker tag python-hello-world $DOCKERHUB_USERNAME/python-hello-world

Sube tu imagen al registro

Una vez que tenemos una imagen correctamente etiquetada, podemos usar el comando docker push para subir nuestra imagen al registro de Docker Hub.

docker push $DOCKERHUB_USERNAME/python-hello-world

Mira tu imagen en Docker Hub en tu navegador

Navega a Docker Hub y ve a tu perfil para ver tu imagen recién subida en https://hub.docker.com/repository/docker/<nombre_de_usuario_de_dockerhub>/python-hello-world.

Ahora que tu imagen está en Docker Hub, otros desarrolladores y operaciones pueden usar el comando docker pull para desplegar tu imagen en otros entornos.

Nota: Las imágenes de Docker contienen todas las dependencias que necesita para ejecutar una aplicación dentro de la imagen. Esto es útil porque ya no tenemos que preocuparnos por la deriva del entorno (diferencias de versión) cuando dependemos de dependencias que se instalan en cada entorno al que desplegamos. Tampoco tenemos que pasar por pasos adicionales para aprovisionar estos entornos. Solo un paso: instala Docker, y ya estás listo.

Desplegando un cambio

La aplicación "hello world!" está sobrevalorada, actualicemos la aplicación para que diga "Hello Beautiful World!" en lugar de eso.

Actualiza app.py

Reemplaza la cadena "Hello World" con "Hello Beautiful World!" en app.py. Puedes actualizar el archivo con el siguiente comando. (Copiar y pegar todo el bloque de código)

echo 'from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello beautiful world!"

if __name__ == "__main__":
    app.run(host="0.0.0.0")' > app.py

Vuelve a construir y sube tu imagen

Ahora que tu aplicación está actualizada, debes repetir los pasos anteriores para volver a construir tu aplicación y subirla al registro de Docker Hub.

Primero, vuelve a construir, esta vez usa tu nombre de usuario de Docker Hub en el comando de construcción:

docker image build -t $DOCKERHUB_USERNAME/python-hello-world.

Observa "Using cache" para los pasos 1-3. Estas capas de la imagen de Docker ya se han construido y docker image build usará estas capas de la memoria caché en lugar de reconstruirlas.

docker push $DOCKERHUB_USERNAME/python-hello-world

También hay un mecanismo de caché para subir capas. Docker Hub ya tiene todas las capas excepto una de una subida anterior, por lo que solo sube la capa que ha cambiado.

Cuando cambias una capa, todas las capas construidas encima de esa tendrán que ser reconstruidas. Cada línea en un Dockerfile construye una nueva capa que se construye sobre la capa creada a partir de las líneas anteriores. Por eso es importante el orden de las líneas en nuestro Dockerfile. Optimizamos nuestro Dockerfile para que la capa que es más probable que cambie (COPY app.py /app.py) sea la última línea del Dockerfile. En general, para una aplicación, tus cambios de código son los que más frecuentemente cambian. Esta optimización es particularmente importante para los procesos de CI/CD, donde quieres que tu automatización funcione lo más rápido posible.

Comprendiendo las capas de imágenes

Una de las principales propiedades de diseño de Docker es el uso del sistema de archivos union.

Considera el Dockerfile que creamos anteriormente:

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app.py"]
COPY app.py /app.py

Cada una de estas líneas es una capa. Cada capa contiene solo el delta, la diferencia o los cambios con respecto a las capas anteriores. Para unir estas capas en un solo contenedor en ejecución, Docker utiliza el sistema de archivos union para superponer las capas de manera transparente en una sola vista.

Cada capa de la imagen es de solo lectura, excepto la última capa, que se crea para el contenedor en ejecución. La capa del contenedor de lectura/escritura implementa "copiar al escribir", lo que significa que los archivos que se almacenan en las capas inferiores de la imagen se copian a la capa del contenedor de lectura/escritura solo cuando se editan esos archivos. Luego, esos cambios se almacenan en la capa del contenedor en ejecución. La función "copiar al escribir" es muy rápida y, en casi todos los casos, no tiene un efecto notable en el rendimiento. Puedes inspeccionar qué archivos se han copiado a nivel de contenedor con el comando docker diff. Más información sobre cómo usar docker diff se puede encontrar aquí.

understanding image layers

Dado que las capas de imágenes son de solo lectura, pueden ser compartidas por imágenes y por contenedores en ejecución. Por ejemplo, crear una nueva aplicación de Python con su propio Dockerfile con capas base similares, compartiría todas las capas que tuviera en común con la primera aplicación de Python.

FROM python:3.8-alpine
RUN pip install flask
CMD ["python","app2.py"]
COPY app2.py /app2.py

understanding image layers

También puedes experimentar la compartición de capas cuando se inician múltiples contenedores a partir de la misma imagen. Dado que los contenedores usan las mismas capas de solo lectura, puedes imaginar que iniciar los contenedores es muy rápido y tiene un impacto muy bajo en el host.

Es posible que note que hay líneas duplicadas en este Dockerfile y en el Dockerfile que creó anteriormente en este laboratorio. Aunque este es un ejemplo muy trivial, puedes extraer las líneas comunes de ambos Dockerfiles a un Dockerfile "base", que luego puedes apuntar desde cada uno de tus Dockerfiles hijos usando el comando FROM.

La estratificación de imágenes habilita el mecanismo de caché de Docker para compilaciones y subidas. Por ejemplo, la salida de tu última docker push muestra que algunas de las capas de tu imagen ya existen en Docker Hub.

$ docker push $DOCKERHUB_USERNAME/python-hello-world

Para examinar más detenidamente las capas, puedes usar el comando docker image history de la imagen de Python que creamos.

$ docker image history python-hello-world

Cada línea representa una capa de la imagen. Notarás que las líneas superiores coinciden con el Dockerfile que creaste, y las líneas inferiores se extraen de la imagen de Python padre. No te preocupes por las etiquetas "<missing>". Estas todavía son capas normales; simplemente el sistema de Docker no les ha dado un ID.

Limpia

Completar este laboratorio genera una serie de contenedores en ejecución en tu host. Limpiemos estos.

Ejecuta docker container stop [id_del_contenedor] para cada contenedor que esté en ejecución

Primero, obtén una lista de los contenedores en ejecución usando docker container ls.

$ docker container ls

Luego, ejecuta el comando para cada contenedor de la lista.

$ docker container stop <id_del_contenedor>

Elimina los contenedores detenidos

docker system prune es un comando muy útil para limpiar tu sistema. Eliminará cualquier contenedor detenido, volúmenes y redes no utilizados, y imágenes sueltas.

$ docker system prune
ADVERTENCIA! Esto eliminará:
- todos los contenedores detenidos
- todos los volúmenes no utilizados por al menos un contenedor
- todas las redes no utilizadas por al menos un contenedor
- todas las imágenes sueltas
¿Estás seguro de que quieres continuar? [y/N] y
Contenedores eliminados:
0b2ba61df37fb4038d9ae5d145740c63c2c211ae2729fc27dc01b82b5aaafa26

Espacio recuperado en total: 300.3kB

Resumen

En este laboratorio, comenzaste a agregar valor creando tus propios contenedores de Docker personalizados.

Conclusiones clave:

  • El Dockerfile es cómo creas compilaciones reproducibles para tu aplicación y cómo integras tu aplicación con Docker en la canalización de CI/CD.
  • Las imágenes de Docker pueden estar disponibles en todos tus entornos a través de un registro central. Docker Hub es un ejemplo de registro, pero puedes desplegar tu propio registro en servidores que controlas.
  • Las imágenes de Docker contienen todas las dependencias que necesita para ejecutar una aplicación dentro de la imagen. Esto es útil porque ya no tenemos que preocuparnos por la deriva del entorno (diferencias de versión) cuando dependemos de dependencias que se instalan en cada entorno al que desplegamos.
  • Docker utiliza el sistema de archivos union y "copiar al escribir" para reutilizar capas de imágenes. Esto reduce la huella de almacenamiento de las imágenes y aumenta significativamente el rendimiento al iniciar contenedores.
  • Las capas de imágenes se almacenan en caché por el sistema de compilación y subida de Docker. No es necesario reconstruir o volver a subir capas de imágenes que ya están presentes en el sistema deseado.
  • Cada línea en un Dockerfile crea una nueva capa, y debido a la memoria caché de capas, las líneas que cambian con más frecuencia (por ejemplo, agregar código fuente a una imagen) deben estar listadas cerca del final del archivo.