Por Jordan A. - Experto en DevOps
Durante la DockerCon 2021, celebrada el 27 de mayo de 2021, pudimos asistir a varias conferencias muy interesantes. Una de ellas nos llamó especialmente la atención, ya que presentaba conceptos básicos para la redacción de archivos Dockerfile y, por lo tanto, para crear contenedores eficaces y eficientes.
Esta conferencia, impartida por Aaron Kalin, evangelista técnico de Datadog, se titula«Lecciones aprendidas con Dockerfiles y Docker Builds»y ofrece siete lecciones que conviene recordar; las voy a detallar y repasar con ejemplos concretos.

Lección 1: Presta atención a la imagen base que utilices
Las imágenes Alpine han tenido mucho éxito en los últimos años, debido a su reducido tamaño y al escaso número de vulnerabilidades. ¿Son, por tanto, una base ideal para crear tu propia imagen de Docker?
Sí, pero… Con el paso del tiempo, las distribuciones Alpine ya no cuentan con el apoyo unánime de los desarrolladores. Uno de los principales problemas es el uso de musl en lugar de glibc (mientras que las distribuciones más populares suelen utilizar glibc). Esto significa que los componentes que se compilen en las distribuciones Alpine podrían no ser compatibles con Ubuntu (y viceversa).
Además, ¿qué pasa con los paquetes que aún no están disponibles en Alpine, aunque sí lo estén en otras distribuciones, y que son imprescindibles para gestionar las dependencias de tu código?
Aaron Kalin nos invita a utilizar, en su lugar, imágenes en versiones «slim», que tienen un tamaño reducido, a veces bastante similar al de las imágenes «alpines», como en este caso:
$ docker image ls | grep python
python 3.9.1-slim-buster 8c84baace4b3 Hace 3 meses 114 MB
python 3.7.4-alpine3.9 32a1b98d0495 Hace 19 meses 98,5 MBLección 2: Encadena tus comandos RUN
El principio de encadenar los comandos RUN para instalar las dependencias permite crear una sola capa (ya que por cada comando del archivo Dockerfile se crea una nueva capa) para tus dependencias.
Aaron Kalin también recomienda organizar los nombres de los paquetes que se van a instalar por orden alfabético, con un solo paquete por línea (lo que facilita su mantenimiento y reorganización).
Por ejemplo, tomemos un archivoDockerfile en el que los paquetes que hay que instalar aparecen en una sola línea:
FROM ubuntu:bionic\
\
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nginx \
python\
\
EXPOSE 80\
\
CMD ["nginx", "-g", "daemon off;"]
Se obtiene el siguiente historial:
$ docker history docker_1_layer:latest
IMAGE CREATED CREATED BY SIZE COMMENT
6892a0a503de 18 seconds ago /bin/sh -c #(nop) CMD
["nginx" "-g" "daemon… 0B
374bdfdad2b2 18 seconds ago /bin/sh -c #(nop) EXPOSE
80 0B
3f7201caacaa 20 seconds ago /bin/sh -c apt-get update
&& apt-get install… 189MB
81bcf752ac3d 8 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 8 days ago /bin/sh -c mkdir -p
/run/systemd && echo 'do… 7B
<missing> 8 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 8 days ago /bin/sh -c set -xe &&
echo '#!/bin/sh' > /… 745B
<missing> 8 days ago /bin/sh -c #(nop) ADD file:e05689b5b0d51a231… 63.1MB Ahora, hagamos el mismo experimento con los elementos distribuidos línea por línea en su archivo Dockerfile:
FROM ubuntu:bionic
RUN apt-get update && apt-get install -y --no-install-recommends curl
RUN apt-get install -y git
RUN apt-get install -y nginx
RUN apt-get install -y python3
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
El resultado es el siguiente historial:
IMAGE CREATED CREATED BY SIZE COMMENT
0d25db122b31 16 seconds ago /bin/sh -c #(nop) CMD
["nginx" "-g" "daemon… 0B
3cf4fb051b11 17 seconds ago /bin/sh -c #(nop) EXPOSE
80 0B
f736c0e7e9e6 18 seconds ago /bin/sh -c apt-get install
-y python3 29.4MB
c6c35fc73cad 28 seconds ago /bin/sh -c apt-get install
-y nginx 53.3MB
53e8b93b739a 39 seconds ago /bin/sh -c apt-get install
-y git 83.4MB
57e76bf1ae81 52 seconds ago /bin/sh -c apt-get update
&& apt-get install… 48.9MB
81bcf752ac3d 8 days ago /bin/sh -c #(nop) CMD
["/bin/bash"] 0B
<missing> 8 days ago /bin/sh -c mkdir -p
/run/systemd && echo 'do… 7B
<missing> 8 days ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 8 days ago /bin/sh -c set -xe &&
echo '#!/bin/sh' > /… 745B
<missing> 8 days ago /bin/sh -c #(nop) ADD file:e05689b5b0d51a231… 63.1MBSe obtienen dos imágenes de distintos tamaños, y la segunda presenta un mayor nivel de complejidad:
$ docker image ls | grep docker
docker_4_layers latest 0d25db122b31
Hace aproximadamente un minuto 278 MB
docker_1_layer latest 6892a0a503de
Hace 4 minutos 252 MB
$Lección 3: Limpieza tras la instalación de paquetes
Volvamos a nuestro siguiente ejemplo:
FROM ubuntu:bionic\
\
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nginx \
python\
\
EXPOSE 80\
\
CMD ["nginx", "-g", "daemon off;"]
En este caso, tras instalar los paquetes mediante apt, no hemos realizado ninguna limpieza. Sin embargo, para reducir aún más el tamaño de la imagen y, por lo tanto, el tiempo de compilación y carga, se pueden añadir los siguientes comandos:
rm -rf /var/lib/apt/lists/* && apt cleanEsto nos daría, por tanto:
FROM ubuntu:bionic\
\
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nginx \
python \
&& rm -rf /var/lib/apt/lists/* \
&& apt clean
EXPOSE 80\
\
CMD ["nginx", "-g", "daemon off;"]
De este modo, podemos comparar el tamaño de la imagen sin haber realizado la limpieza: docker_1_layer, y el de la imagen tras haber realizado la limpieza: docker_1_layer_clean:
$ docker image ls | grep docker
docker_1_layer_clean latest 494fb62a6e8c
Hace 16 segundos 216 MB
docker_4_layers latest 0d25db122b31
Hace 7 minutos 278 MB
docker_1_layer latest 6892a0a503de
Hace 10 minutos 252 MBAsí pues, se observa que la imagen en la que se ha aplicado el «clean» tiene un tamaño menor que la imagen en la que no se ha aplicado. Por lo tanto, hemos conseguido reducir aún más el tamaño de nuestra imagen.
Lección 4: Iniciar la instalación de las dependencias de la aplicación por separado al final del Dockerfile
De hecho, dado que estas dependencias suelen cambiar de vez en cuando a medida que evoluciona el código, conviene colocarlas en primer lugar en las secciones inferiores del Dockerfile. De este modo, se evita tener que volver a compilar todas las capas siguientes en caso de que se produzcan modificaciones.
Tampoco hay que olvidar indicar a la herramienta que no guarde datos en la caché (algo parecido a lo que ocurre con apt).
A continuación se muestra un ejemplo de cómo instalar bibliotecas de Python:
Ejecuta pip install --no-cache-dir -r requirements.txtLección 5: No olvides usar el archivo .dockerignore
Aaron Kalin nos recuerda, con toda razón, que debemos utilizar el archivo .dockerignore de forma inteligente. De hecho, este archivo permite excluir directorios y archivos de cualquier copia que se pueda realizar dentro de la imagen de Docker.
Entre los archivos y directorios que a menudo se olvida excluir, el primero de la lista es: .git
De hecho, si los archivos relacionados con tu código se controlan mediante la herramienta Git, es inevitable que se cree un directorio oculto llamado .git dentro de tu directorio de trabajo.
¡Qué pena que se haya descargado dentro de tu imagen de Docker!
Otros archivos que solemos olvidar son todos los archivos Dockerfile que hay en nuestro directorio de trabajo.
Así es como quedaría nuestro archivo .dockerignore:
.git
Dockerfile*Las «lecciones» 6 y 7 se tratarán en el próximo artículo. Se refieren al uso de la construcción de imágenes mediante la funcionalidad «multi-stage» y al uso de etiquetas.
