Docker: faire les images les plus petites avec du NodeJS

Etape 1 : usage des "&&"

Il faut savoir que de base, Docker crée un "layer" pour chaque ligne de commande déclaré dans le fichier Dockerfile.

Prenons le cas suivant:

FROM ubuntu

RUN apt-get update
RUN apt-get install vim


Nous allons avoir 2 layers de créé: un pour chaque run. De plus, Docker, à chaque fois que vous lancer une commande de compilation, va regarder si les layers ont changé, si oui, il va en créé des nouveaux, sans effecer les anciens. Ce qui peut rapidement prendre de la place.

Et là, nous avons un exemple très simple, mais cela peut être critique quand vous avez de nombreuses lignes.

Vous pouvez remplacer l'exemple suivant par:

FROM ubuntu

RUN apt-get update && apt-get install vim


Et si vous voulez le rendre lisible:

FROM ubuntu

RUN apt-get update \
    && apt-get install vim


Etape 2 : faisons du multi-stage

Imaginons que nous avons une application NodeJS (utilisant Express, ou autre). Nous allons avoir tendance à écrire un fichier Dockerfile comme suit:

FROM node:8

EXPOSE 3000
WORKDIR /app
COPY package.json index.js ./
RUN npm install

CMD ["npm", "start"]


Où nous exposons le port 3000, nous créons un répertoire de travail "app", nous copions les fichiers nécessaires, nous installons les dépendances NPM et nous démarrons le serveur.


Classique, mais pas optimisé, car nous allons avoir 5 layers.

Essayons l'approche dite "multi-stage", qui consiste à nommer les "FROM" et à les réutiliser:

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 ["npm", "start"]


Comme nous pouvons le voir, nous avons uns "stage" se nommant "build" qui va regrouper 3 commandes

Nous avons ensuite le stage le plus haut, qui ne devrait changer d'ailleurs, qui accède au stage précédant.

L'avantage de cela c'est que désormais, nous avons plus que trois layers: un pour le stage "build", un pour le "EXPOSE" et un pour le "CMD".

C'est d'autant plus avantageux que ces derniers layers ne vont pas bouger, alors que le layer du stage build lui se mettre à jour et seulement lui.


Etape 3 - version 1 : Distroless

Derrière ce nom barbare se cache un principe tout simple: ne conserver que ce qui est nécessaire pour que cela fonctionne.

Par exemple, quand nous lançons notre image Docker, notre serveur n'a besoin au fond que de NodeJS lui même, plus de NPM, ni d'apt-get, etc ...

Un projet de Google a été créé dans ce sens et qui s'appelle tout simplement Distroless: https://github.com/GoogleCloudPlatform/distroless

Ils nous ont fait des images Docker et nous allons l'utiliser dans l'exemple précédent.

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 ["npm", "start"]


Comme vous pouvez voir, la différence est minime: nous avons changé juste le deuxième "FROM"

Mais l'impact est énorme. Car nous pouvons passer d'une image Docker de 600 Mo à ... 80 Mo !!!!

Par contre, attention: nous ne pouvons plus "débugguer".

Ce que j'entends, c'est que nous ne pouvons plus faire ce genre de chose:

> docker exec -ti dockerID bash

Car bash sera enlevé. Donc nous ne pouvons plus regardé les logs, ou ce qui se passe sur l'instance Docker. La seule chose que nous pourrons faire sera de lancer une commande node.

Après, cela a un bon côté des choses: en production, personne ne pourra alors analyser le système et le modifié, ce qui renforce la sécurité.


Etape 3 - version 2 : Alpine Linux

Alpine Linux est une distribution permettant d'avoir une image très légère de linux ou en étant très sécurisé.

Du coup, nous allons modifier notre script pour utiliser une version Alpine de Node:

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"]


Si nous analysons nos images, nous voyons ... que nous avons une image de 70 Mo ! Et en plus nous avons un shell, contrairement à la version distroless.

Le hic potentiel est que si nous utilisons des dépendances NPM qui ont d'être compilé via node-gyp, nous pouvons avoir des soucis, car Alpine Linux utilise "glibc" pour compiler.


Commentaires

Posts les plus consultés de ce blog

ISO: liens & outils utiles

NodeJs et SSL: une petite analyse

Créer sa commande slack en quelques minutes