Volver

5 buenas prácticas para crear archivos Dockerfile eficaces

Imagen del slider

1 de junio de 2021

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 MB

Lecció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.1MB

Se 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 clean

Esto 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 MB

Así 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.txt

Lecció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.