
Lorsqu'il s'agit de créer des conteneurs Docker, il est préférable de toujours s'efforcer de minimiser la taille des images. Images utilisant les mêmes couches et pesant moins - transférées et déployées plus rapidement.
Mais comment contrôler la taille lorsque chaque exécution de l'instruction RUN
crée une nouvelle couche? De plus, vous avez toujours besoin d'artefacts intermédiaires avant de créer l'image elle-même ...
Vous savez probablement que la plupart des fichiers Docker ont leurs propres fonctionnalités plutôt étranges, par exemple:
FROM ubuntu RUN apt-get update && apt-get install vim
Eh bien, pourquoi &&
est-il ici? N'est-il pas plus facile d'exécuter deux instructions RUN
, comme ici?
FROM ubuntu RUN apt-get update RUN apt-get install vim
À partir de Docker version 1.10, les opérateurs COPY
, ADD
et RUN
ajoutent un nouveau calque à l'image. Dans l'exemple précédent, deux couches ont été créées au lieu d'une.

Couches comme git le commet.
Les couches Docker préservent les différences entre la version précédente et la version actuelle de l'image. Et comme git commits, ils sont pratiques si vous les partagez avec d'autres référentiels ou images. En effet, lors de la demande d'une image auprès du registre, seuls les calques manquants sont chargés, ce qui simplifie la séparation des images entre les conteneurs.
Mais en même temps, chaque couche a lieu, et plus elles sont nombreuses, plus l'image finale est lourde. Les référentiels Git sont similaires à cet égard: la taille du référentiel augmente avec le nombre de couches, car il doit stocker toutes les modifications entre les validations. RUN
, il était RUN
de combiner plusieurs instructions RUN
sur la même ligne, comme dans le premier exemple. Mais maintenant, hélas, non.
1. Combinez plusieurs couches en une seule en utilisant l'assemblage progressif d'images Docker
Lorsque le référentiel Git grandit, vous pouvez simplement résumer tout l'historique des modifications en un seul commit et l'oublier. Il s'est avéré que quelque chose de similaire peut être implémenté dans Docker - via un assemblage progressif.
Créons un conteneur Node.js.
Commençons par 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!`) })
et package.json
:
{ "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" } }
Emballez l'application avec le Dockerfile
suivant:
FROM node:8 EXPOSE 3000 WORKDIR /app COPY package.json index.js ./ RUN npm install CMD ["npm", "start"]
Créez une image:
$ docker build -t node-vanilla .
Vérifiez que tout fonctionne:
$ docker run -p 3000:3000 -ti --rm --init node-vanilla
Vous pouvez maintenant suivre le lien: http: // localhost: 3000 et voir "Bonjour tout le monde!".
Dans le Dockerfile
maintenant les opérateurs COPY
et RUN
, nous Dockerfile
donc l'augmentation d'au moins deux couches, par rapport Ă l'image d'origine:
$ 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
Comme vous pouvez le voir, l'image finale a augmenté de cinq nouveaux calques: un pour chaque opérateur de notre Dockerfile
. Essayons maintenant la version Docker phasée. Nous utilisons le même Dockerfile
, composé de deux parties:
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 première partie du Dockerfile
crée trois couches. Ensuite, les couches sont combinées et copiées dans les deuxième et dernière étapes. Deux autres calques sont ajoutés à l'image ci-dessus. En conséquence, nous avons trois couches.

Essayons. Créez d'abord un conteneur:
$ docker build -t node-multi-stage .
Vérification de l'historique:
$ 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
Vérifiez si la taille du fichier a changé:
$ docker images | grep node- node-multi-stage 331b81a245b1 678MB node-vanilla 075d229d3f48 679MB
Oui, il est devenu plus petit, mais pas encore de manière significative.
2. Nous retirons tout ce qui est inutile du récipient en utilisant
L'image actuelle nous fournit Node.js, yarn
, npm
, bash
et bien d'autres binaires utiles. En outre, il est basé sur Ubuntu. Ainsi, en le déployant, nous obtenons un système d'exploitation à part entière avec de nombreux binaires et utilitaires utiles.
Cependant, nous n'en avons pas besoin pour exécuter le conteneur. La seule dépendance nécessaire est Node.js.
Les conteneurs Docker doivent prendre en charge le fonctionnement d'un processus et contenir le minimum d'outils nécessaires pour l'exécuter. Un système d'exploitation complet n'est pas requis pour cela.
Nous pouvons donc tout en retirer, sauf Node.js.
Mais comment?
Google a déjà proposé une solution similaire - GoogleCloudPlatform / distroless .
La description du référentiel se lit comme suit:
Les images sans distraction ne contiennent que l'application et ses dépendances. Il n'y a aucun gestionnaire de packages, shells ou autres programmes que l'on trouve généralement dans la distribution Linux standard.
VoilĂ ce dont vous avez besoin!
Exécutez Dockerfile
pour obtenir une nouvelle image:
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"]
Nous collectons l'image comme d'habitude:
$ docker build -t node-distroless .
L'application devrait fonctionner correctement. Pour vérifier, exécutez le conteneur:
$ docker run -p 3000:3000 -ti --rm --init node-distroless
Et accédez à http: // localhost: 3000 . L'image est-elle devenue plus facile sans binaires supplémentaires?
$ docker images | grep node-distroless node-distroless 7b4db3b7f1e5 76.7MB
Comme ça! Maintenant, il ne pèse que 76,7 Mo, autant que 600 Mo de moins!
Tout est cool, mais il y a un point important. Lorsque le conteneur est en cours d'exécution et que vous devez le vérifier, vous pouvez vous connecter en utilisant:
$ docker exec -ti <insert_docker_id> bash
La connexion à un conteneur en cours d'exécution et le démarrage de bash
très similaires à la création d'une session SSH.
Mais comme distroless est une version allégée du système d'exploitation d'origine, il n'y a pas de binaires supplémentaires, ni, en fait, un shell!
Comment se connecter à un conteneur en cours d'exécution s'il n'y a pas de shell?
La chose la plus intéressante est que rien.
Ce n'est pas très bon, car seuls les binaires peuvent être exécutés dans un conteneur. Et le seul qui peut être lancé est Node.js:
$ docker exec -ti <insert_docker_id> node
En fait, il y a un avantage à cela, car si un attaquant peut accéder au conteneur, il fera beaucoup moins de mal que s'il avait accès au shell. En d'autres termes, moins de fichiers binaires - moins de poids et plus de sécurité. Mais, au prix d'un débogage plus complexe.
Ici, il convient de noter qu'il ne vaut pas la peine de connecter et de déboguer des conteneurs sur l'environnement de prod. Il est préférable de s'appuyer sur des systèmes de journalisation et de surveillance correctement configurés.
Mais que se passe-t-il si nous avons encore besoin du débogage, et pourtant nous voulons que l'image du docker soit la plus petite?
3. Réduisez les images de base avec Alpine
Vous pouvez remplacer sans distraction une image alpine.
Alpine Linux est une distribution légère et orientée sécurité basée sur musl libc et busybox . Mais nous ne prendrons pas un mot, mais vérifierons plutôt.
Exécutez Dockerfile
utilisant 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"]
Créez une image:
$ docker build -t node-alpine .
Vérifier la taille:
$ docker images | grep node-alpine node-alpine aa1f85f8e724 69.7MB
Ă€ la sortie, nous avons 69,7 Mo - c'est encore moins qu'une image sans distraction.
Vérifions s'il est possible de se connecter à un conteneur fonctionnel (dans le cas de l'image distrolles, nous n'avons pas pu le faire).
Lancez le conteneur:
$ docker run -p 3000:3000 -ti --rm --init node-alpine Example app listening on port 3000!
Et connectez:
$ 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
En vain. Mais peut-ĂŞtre que le conteneur a sh
'ell ...:
$ docker exec -ti 9d8e97e307d7 sh / #
Super! Nous avons réussi à nous connecter au conteneur, et en même temps son image est également plus petite. Mais ici, il y avait quelques nuances.
Les images Alpine sont basées sur muslc, une bibliothèque standard alternative pour C. Alors que la plupart des distributions Linux, telles que Ubuntu, Debian et CentOS, sont basées sur glibc. On pense que ces deux bibliothèques fournissent la même interface pour travailler avec le noyau.
Cependant, ils ont des objectifs différents: la glibc est la plus courante et la plus rapide, tandis que la muslc prend moins de place et est écrite avec un biais de sécurité. Lorsqu'une application compile, en règle générale, elle se compile dans une bibliothèque spécifique C. Si vous devez l'utiliser avec une autre bibliothèque, vous devrez recompiler.
En d'autres termes, la construction de conteneurs sur des images alpines peut entraîner des événements inattendus, car la bibliothèque C standard utilisée est différente. La différence sera notable lors de l'utilisation de fichiers binaires précompilés, tels que les extensions Node.js pour C ++.
Par exemple, le package PhantomJS terminé ne fonctionne pas sur Alpine.
Quelle est donc l'image de base Ă choisir?
Look alpin, sans distraction ou vanille - bien sûr, il vaut mieux décider en fonction de la situation.
Si vous traitez avec la prod et la sécurité est importante, peut-être la distraction serait la plus appropriée.
Chaque binaire ajouté à l'image Docker ajoute un certain risque à la stabilité de l'application entière. Ce risque peut être réduit en ayant un seul binaire installé dans le conteneur.
Par exemple, si un attaquant pouvait trouver une vulnérabilité dans une application s'exécutant sur la base d'une image sans distraction, il ne pourrait pas exécuter le shell dans le conteneur car il n'y était pas!
Si, pour une raison quelconque, la taille de l'image du docker est extrêmement importante pour vous, cela vaut vraiment la peine de regarder de plus près les images alpines.
Ils sont vraiment petits, mais au détriment de la compatibilité. Alpine utilise une bibliothèque C standard légèrement différente, muslc, donc parfois des problèmes surgissent. Des exemples sont disponibles sur les liens suivants: https://github.com/grpc/grpc/issues/8528 et https://github.com/grpc/grpc/issues/6126 .
Les images vanille sont idéales pour les tests et le développement.
Oui, ils sont gros, mais ils se ressemblent autant que possible sur une machine à part entière avec Ubuntu installé. De plus, tous les binaires du système d'exploitation sont disponibles.
Résumez la taille des images Docker reçues:
node:8
681 Mo
node:8
avec une construction incrémentielle de 678 Mo
gcr.io/distroless/nodejs
76.7MB
node:8-alpine
69,7 Mo
Séparer les mots du traducteur
Lisez d'autres articles sur notre blog:
Sauvegardes avec état dans Kubernetes
Sauvegarde d'un grand nombre de projets Web hétérogènes
Bot télégramme pour Redmine. Comment simplifier la vie pour vous et les gens