
Cuando se trata de crear contenedores Docker, es mejor siempre esforzarse por minimizar el tamaño de las imágenes. Imágenes que usan las mismas capas y pesan menos: se transfieren y despliegan más rápidamente.
Pero, ¿cómo controlar el tamaño cuando cada ejecución de la instrucción RUN
crea una nueva capa? Además, aún necesita artefactos intermedios antes de crear la imagen en sí ...
Probablemente sepa que la mayoría de los archivos Docker tienen sus propias características bastante extrañas, por ejemplo:
FROM ubuntu RUN apt-get update && apt-get install vim
Bueno, ¿por qué está &&
aquí? ¿No es más fácil ejecutar dos sentencias RUN
, como aquí?
FROM ubuntu RUN apt-get update RUN apt-get install vim
A partir de Docker versión 1.10, los operadores COPY
, ADD
y RUN
agregan una nueva capa a la imagen. En el ejemplo anterior, se crearon dos capas en lugar de una.

Capas como git commits.
Las capas Docker conservan las diferencias entre la versión anterior y actual de la imagen. Y como git commits, son útiles si los comparte con otros repositorios o imágenes. De hecho, al solicitar una imagen del registro, solo se cargan las capas que faltan, lo que simplifica la separación de imágenes entre contenedores.
Pero al mismo tiempo, cada capa tiene lugar, y cuanto más, más pesada es la imagen final. Los repositorios de Git son similares a este respecto: el tamaño del repositorio crece con el número de capas, porque debe almacenar todos los cambios entre confirmaciones. Solía ser una buena práctica combinar varias declaraciones RUN
en la misma línea, como en el primer ejemplo. Pero ahora, por desgracia, no.
1. Combine varias capas en una usando el ensamblaje por fases de imágenes Docker
Cuando crezca el repositorio de Git, simplemente puede resumir todo el historial de cambios en una confirmación y olvidarse de él. Resultó que algo similar se puede implementar en Docker a través del ensamblaje por fases.
Creemos un contenedor Node.js.
Comencemos con index.js
:
const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => { console.log(`Example app listening on port 3000!`) })
y package.json
:
{ "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" } }
Empaquete la aplicación con el siguiente Dockerfile
:
FROM node:8 EXPOSE 3000 WORKDIR /app COPY package.json index.js ./ RUN npm install CMD ["npm", "start"]
Crea una imagen:
$ docker build -t node-vanilla .
Comprueba que todo funciona:
$ docker run -p 3000:3000 -ti --rm --init node-vanilla
Ahora puede seguir el enlace: http: // localhost: 3000 y ver "¡Hola, mundo!".
En el Dockerfile
ahora tenemos los operadores COPY
y RUN
, por lo que corregimos el aumento en al menos dos capas, en comparación con la imagen original:
$ docker history node-vanilla IMAGE CREATED BY SIZE 075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 0B bc8c3cc813ae /bin/sh -c npm install 2.91MB bac31afb6f42 /bin/sh -c #(nop) COPY multi:3071ddd474429e1… 364B 500a9fbef90e /bin/sh -c #(nop) WORKDIR /app 0B 78b28027dfbf /bin/sh -c #(nop) EXPOSE 3000 0B b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
Como puede ver, la imagen final ha aumentado en cinco nuevas capas: una para cada operador en nuestro Dockerfile
. Probemos ahora la compilación gradual de Docker. Usamos el mismo Dockerfile
, que consta de dos partes:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8 COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
La primera parte del Dockerfile
crea tres capas. Luego, las capas se combinan y copian en la segunda y última etapa. Se agregan dos capas más a la imagen de arriba. Como resultado, tenemos tres capas.

Probémoslo. Primero, cree un contenedor:
$ docker build -t node-multi-stage .
Comprobando el historial:
$ docker history node-multi-stage IMAGE CREATED BY SIZE 331b81a245b1 /bin/sh -c #(nop) CMD ["index.js"] 0B bdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0B f8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MB b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
Vea si el tamaño del archivo ha cambiado:
$ docker images | grep node- node-multi-stage 331b81a245b1 678MB node-vanilla 075d229d3f48 679MB
Sí, se ha vuelto más pequeño, pero aún no significativamente.
2. Eliminamos todo lo innecesario del contenedor usando distroless
La imagen actual nos proporciona Node.js, yarn
, npm
, bash
y muchos otros binarios útiles. Además, está basado en Ubuntu. Por lo tanto, al implementarlo, obtenemos un sistema operativo completo con muchos binarios y utilidades útiles.
Sin embargo, no los necesitamos para ejecutar el contenedor. La única dependencia necesaria es Node.js.
Los contenedores Docker deben admitir la operación de un proceso y contener el conjunto mínimo de herramientas necesarias para ejecutarlo. No se requiere un sistema operativo completo para esto.
Entonces podemos sacar todo de él excepto Node.js.
Pero como?
Google ya ha presentado una solución similar: GoogleCloudPlatform / distroless .
La descripción del repositorio dice:
Las imágenes sin distribución contienen solo la aplicación y sus dependencias. No hay administradores de paquetes, shells u otros programas que generalmente se encuentran en la distribución estándar de Linux.
¡Esto es lo que necesitas!
Ejecute Dockerfile
para obtener una nueva imagen:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
Recopilamos la imagen como de costumbre:
$ docker build -t node-distroless .
La aplicación debería funcionar bien. Para verificar, ejecute el contenedor:
$ docker run -p 3000:3000 -ti --rm --init node-distroless
Y vaya a http: // localhost: 3000 . ¿La imagen se ha vuelto más fácil sin binarios adicionales?
$ docker images | grep node-distroless node-distroless 7b4db3b7f1e5 76.7MB
Solo asi! Ahora pesa solo 76.7 MB, ¡hasta 600 MB menos!
Todo es genial, pero hay un punto importante. Cuando el contenedor se está ejecutando y necesita verificarlo, puede conectarse usando:
$ docker exec -ti <insert_docker_id> bash
Conectarse a un contenedor en ejecución e iniciar bash
muy similar a crear una sesión SSH.
Pero dado que distroless es una versión simplificada del sistema operativo original, ¡no hay binarios adicionales, ni, en realidad, un shell!
¿Cómo conectarse a un contenedor en ejecución si no hay shell?
Lo más interesante es que nada.
Esto no es muy bueno, ya que solo se pueden ejecutar binarios en un contenedor. Y el único que se puede iniciar es Node.js:
$ docker exec -ti <insert_docker_id> node
De hecho, hay una ventaja en esto, porque si algún atacante puede obtener acceso al contenedor, hará mucho menos daño que si tuviera acceso al shell. En otras palabras, menos binarios: menos peso y mayor seguridad. Pero, a costa de una depuración más compleja.
Aquí debe tenerse en cuenta que no vale la pena conectar y depurar contenedores en el entorno de producción. Es mejor confiar en sistemas de registro y monitoreo configurados adecuadamente.
Pero, ¿qué sucede si todavía necesitamos depurar y, sin embargo, queremos que la imagen del acoplador sea la más pequeña?
3. Reduce las imágenes base con Alpine
Puede reemplazar distroless con una imagen alpina.
Alpine Linux es una distribución liviana orientada a la seguridad basada en musl libc y busybox . Pero no tomaremos una palabra, sino que la revisaremos.
Ejecute Dockerfile
usando el node:8-alpine
:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8-alpine COPY --from=build /app / EXPOSE 3000 CMD ["npm", "start"]
Crea una imagen:
$ docker build -t node-alpine .
Comprobar tamaño:
$ docker images | grep node-alpine node-alpine aa1f85f8e724 69.7MB
En la salida, tenemos 69.7MB, esto es incluso menos que una imagen sin distro.
Verifiquemos si es posible conectarse a un contenedor que funcione (en el caso de la imagen de distribución, no podríamos hacer esto).
Lanzar el contenedor:
$ docker run -p 3000:3000 -ti --rm --init node-alpine Example app listening on port 3000!
Y conecta:
$ docker exec -ti 9d8e97e307d7 bash OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown
Sin éxito Pero tal vez el contenedor tiene sh
'ell ...:
$ docker exec -ti 9d8e97e307d7 sh / #
Genial Logramos conectarnos al contenedor y, al mismo tiempo, su imagen también es más pequeña. Pero aquí había algunos matices.
Las imágenes alpinas se basan en muslc, una biblioteca estándar alternativa para C. Mientras que la mayoría de las distribuciones de Linux, como Ubuntu, Debian y CentOS, se basan en glibc. Se cree que ambas bibliotecas proporcionan la misma interfaz para trabajar con el núcleo.
Sin embargo, tienen objetivos diferentes: glibc es el más común y rápido, mientras que muslc ocupa menos espacio y está escrito con un sesgo de seguridad. Cuando una aplicación compila, por regla general, se compila en una biblioteca específica de C. Si necesita usarla con otra biblioteca, tendrá que volver a compilar.
En otras palabras, la construcción de contenedores en imágenes Alpine puede conducir a eventos inesperados, ya que la biblioteca C estándar utilizada en ella es diferente. La diferencia se notará cuando trabaje con archivos binarios precompilados, como las extensiones Node.js para C ++.
Por ejemplo, el paquete PhantomJS terminado no funciona en Alpine.
Entonces, ¿cuál es la imagen básica para elegir?
Aspecto alpino, distroless o vainilla: por supuesto, es mejor decidir según la situación.
Si se trata de productos y la seguridad es importante, quizás la distribución sin distorsiones sería lo más apropiado.
Cada binario agregado a la imagen de Docker agrega un cierto riesgo a la estabilidad de toda la aplicación. Este riesgo puede reducirse si solo se instala un binario en el contenedor.
Por ejemplo, si un atacante podría encontrar una vulnerabilidad en una aplicación que se ejecuta sobre la base de una imagen ininterrumpida, ¡no podría ejecutar el shell en el contenedor porque no estaba allí!
Si por alguna razón el tamaño de la imagen acoplable es extremadamente importante para usted, definitivamente vale la pena echar un vistazo más de cerca a las imágenes basadas en Alpine.
Son realmente pequeños, pero a costa de la compatibilidad. Alpine utiliza una biblioteca C estándar ligeramente diferente, muslc, por lo que a veces surgen problemas. Los ejemplos están disponibles en los siguientes enlaces: https://github.com/grpc/grpc/issues/8528 y https://github.com/grpc/grpc/issues/6126 .
Las imágenes de vainilla son ideales para pruebas y desarrollo.
Sí, son grandes, pero se ven lo más posible en una máquina completa con Ubuntu instalado. Además, todos los binarios en el sistema operativo están disponibles.
Resuma el tamaño de las imágenes Docker recibidas:
node:8
681 MB
node:8
con 678MB de compilación incremental
gcr.io/distroless/nodejs
76.7MB
node:8-alpine
69.7MB
Palabras de despedida del traductor
Lea otros artículos en nuestro blog:
Copias de seguridad con estado en Kubernetes
Copia de seguridad de una gran cantidad de proyectos web heterogéneos
Telegram bot para Redmine. Cómo simplificar la vida para ti y para las personas