Como hacer imágenes de docker lo mas pequeñas posibles

Unos 4 minutos de lectura Publicado:

TL;DR Problema

  • Imagen demasiado grande = mayor superficie de ataque y mayor tamaño
  • Herramientas de debugueo = n dockerfiles

Solución

  • Dockerfile Multistage

Un dockerfile con muchos pasos o stages, es uno en el que, dicho mal y rápido, se crean varias imágenes en vez de una y todas menos la ultima son temporales.

FROM alpine:3.7 as builder
RUN apk add -U curl
RUN curl -Lo checkup.tar.gz https://github.com/sourcegraph/checkup/releases/download/v0.2.0/checkup_linux_amd64.tar.gz
RUN tar -xzf checkup.tar.gz && rm checkup.tar.gz

FROM scratch as production
COPY --from=builder /checkup/checkup /usr/bin/checkup
WORKDIR /checkup
ENTRYPOINT ["checkup"]

FROM alpine:3.7 as debug
COPY --from=builder /checkup/checkup /usr/bin/checkup
RUN apk add -U curl
WORKDIR /checkup

Original aquí.

En la linea 3, se descarga el tgz y luego se descomprime. En la linea 7 se copia este binario del primer contenedor al segundo, que es el productivo. Por lo tanto, en el segundo contenedor no hace falta instalar paquetes que solo se usaran en la compilación. Para compilar la imagen de “producción”:

docker build . -t prod/checkup --target production

Para construir la tercera imagen, con herramientas de debugueo:

docker build . -t debug/checkup --target debug

El tamaño de ambas imágenes:

debug/checkup 23.1MB
prod/checkup  16.4MB

The End

Ahora con la gente que tiene ganas de leer, analicemos el primer problema mas profundamente. Quien usa docker a menudo ya conocerá alpine. Esta imagen se creó con el objetivo de crear imágenes de docker funcionales lo mas pequeñas posibles. El motivo de querer esto es ofrecer una superficie menor de ataque, ya que cuanto mas pequeña es una imagen, menos posibilidades hay de que tenga alguna vulnerabilidad. Además, usa musl en vez de libc1. Un añadido es que es mas fácil gestionar imágenes de 5mb que de 100mb, tanto por el espacio que ocupan, como los recursos que consumen, como el tiempo de descarga, que aunque parezca una tontería, puede ser crítico en según que entorno.

Aun con todo, la realidad es que alpine por defecto sirve de poco, necesita programas extra. Para instalarlos tenemos el gestor de paquetes apk, que no tiene nada que ver con los artefactos de android. De este modo es muy fácil instalar git, wget, curl, make, las build-essentials y chorrocientos paquetes mas que suelen ser necesarios para descargar y compilar programas.

Pero estos paquetes solo son útiles cuando se quiere instalar un programa, por lo que si queremos tener una imagen lo mas pequeña posible hay que hacer limpieza al terminar. Por ejemplo, si se instala git, después de haber clonado el repositorio habría que desinstalarlo. Y después de eso, habría que borrar los índices del gestor de paquetes2. Y luego habrá mas cosas, según las particularidades de la imagen que se use.

Hay dos formas de gestionar este problema, la cutre y costosa o la molona. La forma mas cutre es examinar que recursos temporales e inútiles se crean y tener una sección del dockerfile en la que se destruyen. O la molona, que es usar multistage. Analicemos el dockerfile usado en el tl;dr.

En el primer stage o estadio3 vemos como en la imagen de alpine en la version 3.7 se instala curl, git y blablabla. Después se descarga el fichero del programa checkup4, se descomprime y termina el primer estadio. El segundo lo único que hace es copiar el binario de checkup a la raíz. Y si os fijáis, se usa una imagen llamada scratch. En realidad no es exactamente una imagen, es algo interno de docker. Básicamente te permite crear una imagen única y exclusivamente con el contenido que tu le digas.5

Esta seria la solución para tener imágenes pequeñas. Pero no es la única razón para usar dockerfile multistage, también se puede usar para para evitar tener mas de un dockerfile por entorno. En el ejemplo que estamos viendo tenemos tres stage: builder, production y debug. El de builder prepara todo lo necesario descargar el binario. El de production es lo mínimo de lo mínimo, solo contiene el binario de checkup. Pero para debuguear nos puede venir bien instalar algún programa extra, además de tener curl, que siempre viene bien para comprobar a que se puede llegar desde un contenedor. Por ello, el tercer stage es en realidad una imagen de alpine con curl ya instalado, con posibilidad de instalar mas paquetes de ser necesario. Como vimos en el TL;DR se puede concretar el estadio a construir con el parámetro --target.

En otro articulo veremos como hacer imágenes tan pequeñas cuando no se tiene binarios estáticos sino librerías compartidas o se usa un lenguaje interpretado como python, que es mas complicado.


  1. Musl es una copia de libc, pero orientada a la seguridad. Más información en su FAQ. [return]
  2. Todos los gestores de paquetes tienen un índice en el que salen todos los programas disponibles para descarga. [return]
  3. Un estadio va desde un FROM al siguiente. [return]
  4. Es un programa muy simple que monitoriza TCP, HTTP, SSL, DNS, … Se puede ver una imagen aquí. [return]
  5. La imagen scratch va íntimamente relacionada con el concepto de más bajo nivel que es un contenedor, lo que da para otro articulo que puede que escriba. De momento tendréis que creerme. [return]
Cualquier duda, se puede preguntar aquí o en los canales descritos en la página principal. Saludos.