Comprender las capas de Docker

27

Tenemos el siguiente bloque en nuestro Dockerfile:

RUN yum -y update
RUN yum -y install epel-release
RUN yum -y groupinstall "Development Tools"
RUN yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Me han dicho que deberíamos unir estos RUNcomandos para reducir las capas acopladas creadas:

RUN yum -y update \
    && yum -y install epel-release \
    && yum -y groupinstall "Development Tools" \
    && yum -y install python-pip git mysql-devel libxml2-devel libxslt-devel python-devel openldap-devel libffi-devel openssl-devel

Soy muy nuevo en Docker y no estoy seguro de entender completamente las diferencias entre estas dos versiones de especificar múltiples comandos RUN. ¿Cuándo uniría los RUNcomandos en uno solo y cuándo tiene sentido tener múltiples RUNcomandos?

alecxe
fuente

Respuestas:

35

Una imagen acoplable es en realidad una lista vinculada de capas del sistema de archivos. Cada instrucción en un Dockerfile crea una capa del sistema de archivos que describe las diferencias en el sistema de archivos antes y después de la ejecución de la instrucción correspondiente. El docker inspectsubcomando se puede usar en una imagen acoplable para revelar su naturaleza de ser una lista vinculada de capas del sistema de archivos.

El número de capas utilizadas en una imagen es importante.

  • al empujar o extraer imágenes, ya que afecta la cantidad de cargas o descargas simultáneas que se producen.
  • cuando se inicia un contenedor, ya que las capas se combinan para producir el sistema de archivos utilizado en el contenedor; Cuantas más capas estén involucradas, peor será el rendimiento, pero los diferentes backends del sistema de archivos se ven afectados de manera diferente por esto.

Esto tiene varias consecuencias sobre cómo se deben construir las imágenes. El primer y más importante consejo que puedo dar es:

Consejo # 1 Asegúrese de que los pasos de compilación en los que está involucrado su código fuente lleguen lo más tarde posible en el Dockerfile y no estén vinculados a comandos anteriores usando a &&o a ;.

La razón de esto es que todos los pasos anteriores se almacenarán en caché y no será necesario descargar las capas correspondientes una y otra vez. Esto significa compilaciones más rápidas y lanzamientos más rápidos, que es probablemente lo que desea. Curiosamente, es sorprendentemente difícil hacer un uso óptimo de la memoria caché del acoplador.

Mi segundo consejo es menos importante, pero lo encuentro muy útil desde el punto de vista del mantenimiento:

Consejo # 2 No escriba comandos complejos en el Dockerfile, sino que use scripts que se copiarán y ejecutarán.

Un Dockerfile siguiendo este consejo sería

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh
COPY install_pacakges.sh /root/
RUN sh -x /root/install_packages.sh

y así. El consejo de vincular varios comandos &&solo tiene un alcance limitado. Es mucho más fácil escribir con scripts, donde puede usar funciones, etc. para evitar redundancias o con fines de documentación.

Las personas interesadas por los preprocesadores y dispuestos a evitar los pequeños gastos generales causados ​​por los COPYpasos y en realidad están generando sobre la marcha un Dockerfile donde el

COPY apt_setup.sh /root/
RUN sh -x /root/apt_setup.sh

las secuencias son reemplazadas por

RUN base64 --decode … | sh -x

donde es la versión codificada en base64 de apt_setup.sh.

Mi tercer consejo es para las personas que desean limitar el tamaño y el número de capas al posible costo de construcciones más largas.

Consejo # 3 Use with-idiom para evitar archivos presentes en capas intermedias pero no en el sistema de archivos resultante.

Un archivo agregado por alguna instrucción acoplable y eliminado por alguna instrucción posterior no está presente en el sistema de archivos resultante, pero se menciona dos veces en las capas acoplables que constituyen la imagen del acoplador en la construcción. Una vez, con el nombre y el contenido completo en la capa resultante de la instrucción que lo agrega, y una vez como un aviso de eliminación en la capa resultante de la instrucción que lo elimina.

Por ejemplo, supongamos que necesitamos temporalmente un compilador de C y alguna imagen y considere el

# !!! THIS DISPLAYS SOME PROBLEM --- DO NOT USE !!!
RUN apt-get install -y gcc
RUN gcc --version
RUN apt-get --purge autoremove -y gcc

(Un ejemplo más realista construiría algún software con el compilador en lugar de simplemente afirmar su presencia con la --versionbandera).

El fragmento de Dockerfile crea tres capas, la primera contiene el paquete completo de gcc, de modo que incluso si no está presente en el sistema de archivos final, los datos correspondientes siguen siendo parte de la imagen de la misma manera y deben descargarse, cargarse y desempacarse siempre que La imagen final es.

El with-idiom es una forma común en la programación funcional para aislar la propiedad del recurso y la liberación del recurso de la lógica que lo usa. Es fácil transponer este modismo al scripting de shell, y podemos reformular los comandos anteriores como el siguiente script, para usarlo COPY & RUNcomo en el Consejo # 2.

# with_c_compiler SIMPLE-COMMAND
#  Execute SIMPLE-COMMAND in a sub-shell with gcc being available.

with_c_compiler()
(
    set -e
    apt-get install -y gcc
    "$@"
    trap 'apt-get --purge autoremove -y gcc' EXIT
)

with_c_compiler\
    gcc --version

Los comandos complejos se pueden convertir en funciones para que se puedan alimentar al with_c_compiler. También es posible encadenar llamadas de varias with_whateverfunciones, pero tal vez no sea muy deseable. (Usando características más esotéricas del shell, ciertamente es posible hacer que los with_c_compilercomandos complejos sean aceptados, pero es preferible en todos los aspectos incluir estos comandos complejos en funciones).

Si queremos ignorar el Consejo # 2, el fragmento de Dockerfile resultante sería

RUN apt-get install -y gcc\
 && gcc --version\
 && apt-get --purge autoremove -y gcc

que no es tan fácil de leer y mantener debido a la ofuscación. Vea cómo la variante shell-script gcc --versionresalta la parte importante mientras que la &&variante encadenada entierra esa parte en medio del ruido.

Michael Le Barbier Grünewald
fuente
1
¿Podría incluir el resultado del tamaño del cuadro después de construir usando un script y usando los comandos múltiples en una declaración RUN?
030
1
Me parece una mala idea mezclar la configuración de la base de la imagen (es decir, el material del sistema operativo) e incluso libs con la configuración de la fuente que escribió. Usted dice "Asegúrese de que los pasos de compilación en los que está involucrado su código fuente lleguen lo más tarde posible". ¿Hay algún problema en hacer de esa parte un artefacto completamente independiente?
JimmyJames
1
@ 030 ¿Qué quiere decir con el tamaño de "caja"? No tengo idea de a qué cuadro te refieres.
Michael Le Barbier Grünewald
1
Me refería al tamaño de la imagen de la ventana acoplable
030
1
@JimmyJames Depende en gran medida de su escenario de implementación. Si asumimos un programa compilado, lo "correcto" sería empaquetarlo e instalar las dependencias de ese paquete y el paquete en sí mismo como dos pasos distintos casi definitivos. Esto para maximizar la utilidad de la memoria caché de la ventana acoplable y evitar la descarga una y otra vez de capas con los mismos archivos. Me resulta más fácil compartir recetas de compilación para construir imágenes acoplables que construir largas cadenas de imágenes de dependencia, porque esto último dificulta la reconstrucción.
Michael Le Barbier Grünewald
13

Cada instrucción que cree en su Dockerfile da como resultado que se cree una nueva capa de imagen. Cada capa trae datos adicionales que no siempre son parte de la imagen resultante. Por ejemplo, si agrega un archivo en una capa, pero lo elimina en otra capa más tarde, el tamaño de la imagen final incluirá el tamaño del archivo agregado en forma de un archivo especial "blanco" aunque lo haya eliminado.

Digamos que tiene el siguiente Dockerfile:

FROM centos:6

RUN yum -y update 
RUN yum -y install epel-release

El tamaño de la imagen resultante será

bigimage     latest        3c5cbfbb4116        2 minutes ago    407MB

Como opuesto, con Dockerfile "similar":

FROM centos:6

RUN yum -y update  && yum -y install epel-release

El tamaño de la imagen resultante será

smallimage     latest        7edeafc01ffe        3 minutes ago    384MB

Obtendrá un tamaño aún más pequeño si limpia la memoria caché de yum en una sola instrucción RUN.

Por lo tanto, desea mantener el equilibrio entre la legibilidad / facilidad de mantenimiento y el número de capas / tamaño de imagen.

oryades
fuente
4

Las RUNdeclaraciones representan cada una de las capas. Imagine que uno descarga un paquete, lo instala y desea eliminarlo. Si uno usa tres RUNdeclaraciones, el tamaño de la imagen no se reducirá, ya que hay capas separadas. Si uno ejecuta todos los comandos usando una RUNinstrucción, el tamaño de la imagen del disco podría reducirse.

030
fuente