RUN múltiple versus RUN de cadena única en Dockerfile, ¿qué es mejor?

132

Dockerfile.1ejecuta múltiples RUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 se une a ellos:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

Cada uno RUNcrea una capa, por lo que siempre asumí que menos capas es mejor y, por lo tanto, Dockerfile.2es mejor.

Esto es obviamente cierto cuando un RUNelimina algo agregado por un anterior RUN(es decir yum install nano && yum clean all), pero en los casos en que cada RUNagrega algo, hay algunos puntos que debemos considerar:

  1. Se supone que las capas solo deben agregar un diff sobre el anterior, por lo que si la capa posterior no elimina algo agregado en una anterior, no debería haber mucha ventaja de ahorro de espacio en disco entre ambos métodos ...

  2. Las capas se extraen en paralelo desde Docker Hub, por lo que Dockerfile.1, aunque probablemente sea un poco más grande, en teoría se descargarían más rápido.

  3. Si se agrega una cuarta oración (es decir echo This is the D > d) y se reconstruye localmente, Dockerfile.1se construiría más rápido gracias a la memoria caché, pero Dockerfile.2tendría que ejecutar los 4 comandos nuevamente.

Entonces, la pregunta: ¿Cuál es una mejor manera de hacer un Dockerfile?

Yajo
fuente
1
No se puede responder en general, ya que depende de la situación y del uso de la imagen (optimizar el tamaño, la velocidad de descarga o la velocidad de construcción)
Henry

Respuestas:

99

Cuando es posible, siempre combino comandos que crean archivos con comandos que eliminan esos mismos archivos en un solo RUN línea. Esto se debe a que cada RUNlínea agrega una capa a la imagen, la salida es, literalmente, los cambios del sistema de archivos que puede ver docker diffen el contenedor temporal que crea. Si elimina un archivo que se creó en una capa diferente, todo lo que hace el sistema de archivos de unión es registrar el cambio del sistema de archivos en una nueva capa, el archivo todavía existe en la capa anterior y se envía a través de la red y se almacena en el disco. Entonces, si descarga el código fuente, lo extrae, lo compila en un binario y luego elimina los archivos tgz y fuente al final, realmente desea que todo esto se haga en una sola capa para reducir el tamaño de la imagen.

A continuación, dividí las capas personalmente en función de su potencial de reutilización en otras imágenes y el uso esperado del almacenamiento en caché. Si tengo 4 imágenes, todas con la misma imagen base (por ejemplo, Debian), puedo extraer una colección de utilidades comunes para la mayoría de esas imágenes en el primer comando de ejecución para que las otras imágenes se beneficien del almacenamiento en caché.

El orden en el Dockerfile es importante al mirar la reutilización de la memoria caché de imágenes. Miro los componentes que se actualizarán muy raramente, posiblemente solo cuando la imagen base se actualice y los coloque en el Dockerfile. Hacia el final del Dockerfile, incluyo todos los comandos que se ejecutarán rápidamente y pueden cambiar con frecuencia, por ejemplo, agregar un usuario con un UID específico del host o crear carpetas y cambiar los permisos. Si el contenedor incluye código interpretado (por ejemplo, JavaScript) que se está desarrollando activamente, se agrega lo más tarde posible para que una reconstrucción solo ejecute ese cambio único.

En cada uno de estos grupos de cambios, consolido lo mejor que puedo para minimizar las capas. Entonces, si hay 4 carpetas de código fuente diferentes, esas se colocan dentro de una sola carpeta para que se pueda agregar con un solo comando. Cualquier instalación de paquetes desde algo como apt-get se fusionan en un solo EJECUTAR cuando sea posible para minimizar la cantidad de sobrecarga del administrador de paquetes (actualización y limpieza).


Actualización para compilaciones de varias etapas:

Me preocupa mucho menos reducir el tamaño de la imagen en las etapas no finales de una construcción de varias etapas. Cuando estas etapas no se etiquetan y envían a otros nodos, puede maximizar la probabilidad de reutilización de caché dividiendo cada comando en una RUNlínea separada .

Sin embargo, esta no es una solución perfecta para aplastar capas, ya que todo lo que copia entre las etapas son los archivos y no el resto de los metadatos de la imagen, como la configuración de las variables de entorno, el punto de entrada y el comando. Y cuando instala paquetes en una distribución de Linux, las bibliotecas y otras dependencias pueden estar dispersas por todo el sistema de archivos, lo que dificulta la copia de todas las dependencias.

Debido a esto, uso compilaciones de varias etapas como reemplazo para compilar binarios en un servidor CI / CD, de modo que mi servidor CI / CD solo necesita tener las herramientas para ejecutarse docker buildy no tener un jdk, nodejs, go y cualquier otra herramienta de compilación instalada.

BMitch
fuente
30

Respuesta oficial enumerada en sus mejores prácticas (las imágenes oficiales DEBEN adherirse a estas)

Minimiza el número de capas

Debe encontrar el equilibrio entre la legibilidad (y, por lo tanto, la mantenibilidad a largo plazo) del Dockerfile y minimizar el número de capas que utiliza. Sea estratégico y cauteloso sobre la cantidad de capas que usa.

Desde docker 1.10 COPY, las declaraciones ADDy RUNagregan una nueva capa a su imagen. Tenga cuidado al usar estas declaraciones. Intenta combinar comandos en una sola RUNdeclaración. Separe esto solo si es necesario para facilitar la lectura.

Más información: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

Actualización: Etapa múltiple en docker> 17.05

Con las compilaciones de varias etapas, puede usar varias FROMdeclaraciones en su Dockerfile. Cada FROMdeclaración es una etapa y puede tener su propia imagen base. En la etapa final, utiliza una imagen base mínima como alpine, copie los artefactos de compilación de las etapas anteriores e instale los requisitos de tiempo de ejecución. El resultado final de esta etapa es tu imagen. Entonces aquí es donde te preocupas por las capas como se describió anteriormente.

Como de costumbre, Docker tiene excelentes documentos en compilaciones de varias etapas. Aquí hay un extracto rápido:

Con compilaciones de varias etapas, utiliza múltiples declaraciones FROM en su Dockerfile. Cada instrucción FROM puede usar una base diferente, y cada una de ellas comienza una nueva etapa de la construcción. Puede copiar selectivamente artefactos de una etapa a otra, dejando todo lo que no desea en la imagen final.

Puede encontrar una gran publicación de blog sobre esto aquí: https://blog.alexellis.io/mutli-stage-docker-builds/

Para responder a tus puntos:

  1. Sí, las capas son como diferencias. No creo que se agreguen capas si no hay absolutamente ningún cambio. El problema es que una vez que instala / descarga algo en la capa # 2, no puede eliminarlo en la capa # 3. Entonces, una vez que algo se escribe en una capa, el tamaño de la imagen ya no se puede reducir al eliminar eso.

  2. Aunque las capas se pueden tirar en paralelo, lo que lo hace potencialmente más rápido, indudablemente cada capa aumenta el tamaño de la imagen, incluso si están eliminando archivos.

  3. Sí, el almacenamiento en caché es útil si está actualizando su archivo acoplable. Pero funciona en una dirección. Si tiene 10 capas y cambia la capa # 6, aún tendrá que reconstruir todo desde la capa # 6- # 10. Por lo tanto, no es frecuente que acelere el proceso de creación, pero se garantiza que aumentará innecesariamente el tamaño de su imagen.


Gracias a @Mohan por recordarme que actualice esta respuesta.

Menzo Wijmenga
fuente
1
Esto ahora está desactualizado; vea la respuesta a continuación.
Mohan
1
@ Mohan gracias por el recordatorio! Actualicé la publicación para ayudar a los usuarios.
Menzo Wijmenga
19

Parece que las respuestas anteriores están desactualizadas. La nota de los documentos:

Antes de Docker 17.05, y aún más, antes de Docker 1.10, era importante minimizar el número de capas en su imagen. Las siguientes mejoras han mitigado esta necesidad:

[...]

Docker 17.05 y superior agregan soporte para compilaciones de varias etapas, que le permiten copiar solo los artefactos que necesita en la imagen final. Esto le permite incluir herramientas e información de depuración en las etapas intermedias de construcción sin aumentar el tamaño de la imagen final.

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

y

Tenga en cuenta que este ejemplo también comprime artificialmente dos comandos RUN junto con el operador Bash &&, para evitar crear una capa adicional en la imagen. Esto es propenso a fallas y difícil de mantener.

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

La mejor práctica parece haber cambiado al uso de compilaciones de varias etapas y mantener la Dockerfilelectura legible.

Mohan
fuente
Si bien las compilaciones en varias etapas parecen una buena opción para mantener el equilibrio, la solución real a esta pregunta vendrá cuando la docker image build --squashopción salga de la experimental.
Yajo
2
@Yajo: soy escéptico sobre squashpasar el experimento. Tiene muchos trucos y solo tenía sentido antes de las compilaciones de varias etapas. Con las compilaciones de etapas múltiples solo necesita optimizar la etapa final, lo cual es muy fácil.
Menzo Wijmenga
1
@Yajo Para ampliar eso, solo las capas en la última etapa hacen alguna diferencia en el tamaño de la imagen final. Entonces, si coloca todos sus gubbins de generador en etapas anteriores y tiene la etapa final, simplemente instale paquetes y copie en archivos de etapas anteriores, todo funciona de maravilla y no se necesita squash.
Mohan
3

Depende de lo que incluya en sus capas de imagen.

El punto clave es compartir tantas capas como sea posible:

Mal ejemplo:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

Buen ejemplo:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

Otra sugerencia es que eliminar no es tan útil solo si ocurre en la misma capa que la acción de agregar / instalar.

xdays
fuente
¿Estos 2 realmente compartirían el RUN yum install big-packagecaché de?
Yajo
Sí, compartirían la misma capa, siempre que comiencen desde la misma base.
Ondra Žižka