Journal docker multi-stage build

Posté par . Licence CC by-sa.
16
6
fév.
2020

Multi-stage builds

Les multi-stage builds sont disponible depuis la version v17.05 de Docker. Voyons,
par l'exemple, comment cette fonctionnalité peut nous être utile.

Prenons le cas d'un projet de tribune libre.
Pour tester ce projet, voici le premier Dockerfile que j'ai écrit

FROM adoptopenjdk/maven-openjdk11

# Update apt
RUN apt-get update

# Install mongodb
RUN apt-get install -y mongodb-server && rm -rf /var/lib/apt/lists/*
RUN service mongodb start

WORKDIR /code

# Prepare by downloading dependencies
ADD pom.xml /code/pom.xml
RUN ["mvn", "dependency:resolve"]
RUN ["mvn", "verify"]

# Adding source, compile and package into a fat jar
ADD src /code/src
RUN ["mvn", "package"]

EXPOSE 27017
EXPOSE 8080
ENTRYPOINT service mongodb start && java -jar target/jb3-1.2-SNAPSHOT.jar

Cette image est pratique, et contient tout ce qu'il faut pour tester une tribune jb3 :
* java
* mongodb
* jb3 bien sûr

Mais le résultat final est inutilement lourd, et contient des logiciels qui ne sont pas necessaire au bon fonctionnement de l'application. Pourquoi avoir maven, toutes les dépendances, etc. alors que seule jb3 est nécessaire ?

REPOSITORY TAG IMAGE ID CREATED SIZE
jb3 latest 1539ab9dd37d About a minute ago 1.14GB

1.14 GB, on doit pouvoir faire plus léger !

C'est là que les multi-stage build rentrent en jeu. En découpant une image suivant les différentes étapes de sa construction, et en réutilisant les produits de ses étapes, on peut faire mincir une image.

FROM maven:3.6-jdk-11 as build
RUN mkdir /jb3
WORKDIR /jb3
ADD pom.xml pom.xml
ADD src src

RUN echo "spring.data.mongodb.host=mongodb" >> /jb3/src/main/resources/config/application.properties


# RUN mvn dependency:resolve && mvn verify && mvn package
RUN mvn package

FROM adoptopenjdk/openjdk11:alpine-slim as distribution

WORKDIR /code

COPY --from=build /jb3/target/jb3-1.2-SNAPSHOT.jar /code/

EXPOSE 8080
ENTRYPOINT java -jar jb3-1.2-SNAPSHOT.jar
REPOSITORY TAG IMAGE ID CREATED SIZE
jb3only latest d80a49aa2953 31 seconds ago 284MB
mongo latest a0e2e64ac939 7 weeks ago 364MB

284 MB. Pas mal. Évidement, il faut rajouter le poids de mongodb, que j'ai retiré de l'image pour le mettre dans un autre container (voir le docker-compose pour les plus curieux). On passe quand même de 1,14 GB à 648 MB

Les docker pull sont maintenant tout léger, plus aucune raison de se priver de jb3 !

  • # distroless

    Posté par (page perso) . Évalué à 6 (+4/-0).

    Et pour compléter le tableau, Google propose les images distroless qui sont des images dépouillées de tout ce qui est superflu (il y en a une pour Java : gcr.io/distroless/java-debian10).

    • [^] # Re: distroless

      Posté par . Évalué à 2 (+1/-0).

      Oui, j'utilise distroless aussi, c'est très bien. Pour continuer sur du java, il y a aussi les containers d'AdoptOpenJdk https://hub.docker.com/_/adoptopenjdk

    • [^] # Re: distroless

      Posté par . Évalué à 4 (+3/-0).

      Ils ne sont pas très bons pour les misesà jour du jdk et je crois qu'ils gardent jshell et javac. J'ai fini par préfèrer créer la mienne. C'est pas plus compliqué et je suis les mises à jour de sécurité d'adoptopenjdk et de la glibc.

      Mais avec le passage à buster les mises à jour de sécurité doivent mieux se passer.

    • [^] # Re: distroless

      Posté par . Évalué à 2 (+0/-0).

      Additionnellement, ce n'est pas très compliqué de créer une image à partir de rien.

      • [^] # Re: distroless

        Posté par . Évalué à 3 (+1/-0).

        Vraiment?
        Je n'ai perso aucune idée de comment faire, j'ai la flemme de chercher, et je soupçonne les résultats des moteurs de recherche d'être blindés d'articles de blogs qui se contentent de gratter la surface.

        Du coup, y'a moyen que tu postes un lien? Je suis intéressé.

        • [^] # Re: distroless

          Posté par (page perso) . Évalué à 6 (+3/-0).

          Tu fais un tar avec ton image et tu l'importe dans docker https://docs.docker.com/develop/develop-images/baseimages/

          « Rappelez-vous toujours que si la Gestapo avait les moyens de vous faire parler, les politiciens ont, eux, les moyens de vous faire taire. » Coluche

          • [^] # Re: distroless

            Posté par . Évalué à 1 (+0/-0).

            C'est un chouia plus. Tu fais une première étape pour le contenu du système de fichier avec docker import, puis tu pars de cette dernière pour construire ton master initial dans le quel tu ajoute les meta données. À minima pour définir l'entrypoint.

            Rien de sorcier et c'est assez rapide au final.

        • [^] # Re: distroless

          Posté par (page perso) . Évalué à 2 (+0/-0).

          En gros, pour créer une image Debian (par exemple), tu prends une image vide et tu y copies le / d’une Debian que tu as « deboostrapé » à l’avance.

          Pour distroless, c’est un peu pareil, ils prennent une image vide et y copient les fichiers nécessaires à une image basique (glibc, trousseau de certificats, etc.).

          • [^] # Re: distroless

            Posté par . Évalué à 2 (+0/-0).

            Hon, donc, en (très) gros:

            dd if=/dev/zero of=/tmp/target count=10240 #par exemple
            mkfs.ext4 /tmp/target
            mount /tmp/target /mnt
            debootstrap --variant=minbase buster /mnt
            tar -czf /mnt /tmp/result.tar.gz

            ça ferais un truc utilisable par docker? (inutile, certes, mais c'est une base…)

            • [^] # Re: distroless

              Posté par . Évalué à 3 (+2/-0). Dernière modification le 10/02/20 à 23:00.

              Les 3 premières lignes sont inutiles.

              • [^] # Re: distroless

                Posté par . Évalué à 3 (+1/-0).

                Heu, t'es en train de me dire que l'on peut debootstrap sur un dossier inexistant?

                • [^] # Re: distroless

                  Posté par . Évalué à 2 (+1/-0).

                  C'est ce que tu fais. debootstrap (ou cdebootstrap) ne sait pas que tu as monté /mnt d'une façon ou d'une autre son boulot c'est uniquement de construire l'arborescence minimale d'une debian. Tu peux t'en servir pour faire simplement des chroot sans docker ni lxc par exemple.

                • [^] # Re: distroless

                  Posté par (page perso) . Évalué à 3 (+1/-0). Dernière modification le 12/02/20 à 11:40.

                  /mnt est probablement pas « inexistant ».

  • # Qui a la plus petite?

    Posté par (page perso) . Évalué à 8 (+5/-0).

    Tu dois pouvoir encore réduire la taille en utilisant une mini jvm ( je le fais pour Newton Adventure ) et bientôt il n'y aura plus besoin du tout de jvm: Spring a annoncé être compatible avec la compilation native de Graalvm en 2020. Je suis de prêt l'affaire !

    Incubez l'excellence sur https://linuxfr.org/board/

  • # philosophie de docker

    Posté par . Évalué à 1 (+3/-4).

    Le premier dockerfile n'est pas du tout dans la philosophie de docker : un service pour un container + le conteneur ne doit pas contenir de données persistante. Donc les deux dockerfiles sont difficilement comparables.

    • [^] # Re: philosophie de docker

      Posté par . Évalué à 4 (+3/-0). Dernière modification le 07/02/20 à 10:22.

      Merci de venir pointer une faiblesse du premier dockerfile, même si ce n'est pas l'objet de ce journal et que ce fût corrigé dans le deuxième. Je pense que, pour le sujet du journal, nous pouvons quand même comparer, non pas les dockerfiles mais bien les résultats

    • [^] # Re: philosophie de docker

      Posté par (page perso) . Évalué à 3 (+3/-3).

      Le premier Dockerfile est dérivé de celui que j'avais fait pour proposer une démo de jb3.

      Une démo est non persistante, c'est une friture pas un bug, pouce, je suis perché et on retouche pas son père !

      Incubez l'excellence sur https://linuxfr.org/board/

  • # up

    Posté par (page perso) . Évalué à 9 (+7/-0).

    Quelques points qui peuvent être améliorés :

    • le mkdir est inutile, WORKDIR le fait déjà
    • COPY est à préférer par rapport à ADD qui fait plein d'autres trucs. Voir https://github.com/hadolint/hadolint/wiki/DL3020 par exemple
    • on peut remplacer les targets de COPY par . dans le workdir
    • c'est plus personnel, mais j'aime bien l'entrypoint (et cmd) en tableau de string
    • je ne taggerais pas la dernière image, pour pouvoir faire un docker build sans avoir besoin de lui passer la target
    FROM maven:3.6-jdk-11 as build
    WORKDIR /jb3
    
    COPY pom.xml .
    COPY src .
    
    RUN echo "spring.data.mongodb.host=mongodb" >> /jb3/src/main/resources/config/application.properties
    
    RUN mvn package
    
    FROM adoptopenjdk/openjdk11:alpine-slim
    WORKDIR /code
    
    COPY --from=build /jb3/target/jb3-1.2-SNAPSHOT.jar .
    
    EXPOSE 8080
    
    ENTRYPOINT [ "java", "-jar", "jb3-1.2-SNAPSHOT.jar" ]

    Sinon les multistage builds c'est bon mangez-en !

    • [^] # Re: up

      Posté par (page perso) . Évalué à 2 (+0/-0).

      Ton ENTRYPOINT devrait être un CMD et tu pourrais mettre dumb-init (par exemple) en ENTRYPOINT.

      • [^] # Re: up

        Posté par (page perso) . Évalué à 5 (+3/-0).

        entrypoint/cmd ça se défend, ça dépend surtout de l'usage. Ici avec l'entrypooint ça veut dire que tout ce que tu rajoutes au docker run sera envoyé dans cmd et entrypoint + cmd est executé.
        C'est parfois super pratique.

        Par contre dumb-init je vois pas du tout l'intérêt voir je marquerais ça comme une mauvaise pratique.
        A partir du moment où on commence à rentrer des notions d'init dans les conteneurs faut se poser des questions sur ce qu'on fait et pourquoi. Il peut y avoir des cas où c'est nécessaire, mais plutôt rare et à éviter.

        • [^] # Re: up

          Posté par (page perso) . Évalué à 2 (+0/-0).

          Ici avec l'entrypooint ça veut dire que tout ce que tu rajoutes au docker run sera envoyé dans cmd et entrypoint + cmd est executé.

          De mémoire, l’entrypoint est aussi exécuté quand tu fais un docker exec (ce qui t’empêche de rentrer dans ton container pour le débuger), non ?

          A partir du moment où on commence à rentrer des notions d'init dans les conteneurs faut se poser des questions sur ce qu'on fait et pourquoi. Il peut y avoir des cas où c'est nécessaire, mais plutôt rare et à éviter.

          dumb-init n’est pas un init classique, il ne gère pas de service, il ne fait qu’exécuter une commande, lui passer les signaux qu’il reçoit et s’occuper du « reaping » de Zombie.

          Il est utile dans tout les cas ou tu veux pouvoir envoyer un signal à ton application alors que celui-ci ne le « trap » pas explicitement et tous les cas où il peut y avoir des sous-processus. Presque tout le temps en fait.

          Dans Kubernetes tu as un peu la même chose avec le container pause qui fait le même travail (mais comme l’espace de nom des PID n’est pas partagé par défaut entre les différent containers d’un pod, dumb-init reste quand même utile dans ce contexte).

          • [^] # Re: up

            Posté par (page perso) . Évalué à 4 (+2/-0).

            De mémoire, l’entrypoint est aussi exécuté quand tu fais un docker exec (ce qui t’empêche de rentrer dans ton container pour le débuger), non ?

            À priori non.

            Voici un Dockerfile avec un entrypoint qui est une boucle infinie :

            FROM alpine:3.11
            
            ENTRYPOINT while :; do sleep 1; done
            docker build -t loop .
            CID=`docker run --rm -d loop`
            docker exec -it $CID sh
            / #
            
            1. build du conteneur
            2. lancer le conteneur et récupérer son ID
            3. exec dans le conteneur en lançant sh

            Si l'entrypoint n'était pas overridé on ne pourrait pas.

            Il est utile dans tout les cas ou tu veux pouvoir envoyer un signal à ton application alors que celui-ci ne le « trap » pas explicitement et tous les cas où il peut y avoir des sous-processus. Presque tout le temps en fait.

            Je dois pas faire assez de java en conteneur alors, car je ne le vois (quasiment) jamais.

            Si l'application ne gère pas les signaux c'est un problème, mais ça devrait être corrigé dans l'application et non par l'adjonction d'un init.

            • [^] # Re: up

              Posté par . Évalué à 1 (+0/-0).

              Personnellement j'utilise un petit wrapper autour du binaire java pour certains paramètre que je veux gérer dynamiquement (en particulier donner un nom qui suit une nomenclature donnée à l'éventuel memory dump. Mais je n'ai jamais eu besoin de gérer des processus par exemple.

            • [^] # Re: up

              Posté par (page perso) . Évalué à 2 (+0/-0).

              À priori non.

              Au temps pour moi.

              Je dois pas faire assez de java en conteneur alors, car je ne le vois (quasiment) jamais.

              Je n’ai jamais fait de Java dans Docker, mais ça reste un problème suffisamment présent pour que Kubernetes l’intègre de base avec son container pause.

              Si l'application ne gère pas les signaux c'est un problème, mais ça devrait être corrigé dans l'application et non par l'adjonction d'un init.

              Tu en connais beaucoup des applications qui trap SIGKILL ?

              • [^] # Re: up

                Posté par . Évalué à 1 (+0/-0).

                The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

                signal(7)

                Je comprends pas bien ce que tu veux dire ?

                • [^] # Re: up

                  Posté par (page perso) . Évalué à 2 (+0/-0).

                  PID 1 ne reçoit que les signaux qu'il trap explicitement. J'ai juste mis 2 lien qui en parle dans mon précédent commentaire…

                  • [^] # Re: up

                    Posté par . Évalué à 0 (+0/-1).

                    Ok après avoir relu plusieurs fois tes 2 liens je viens de comprendre ce que tu entends. Essaie de ne pas considérer que les autres sont de mauvaises fois quand tu t'exprime avec ironie. C'est quelque chose qui se c'est une tournure qui marche bien à l'oral pas à l'écrit.

                    Bref.

                    Ce que tu cherche c'est à nettoyer les fils correctement. Ça peut être utile quand tu as une hiérarchie de processus d'une profondeur d'au moins 2, car dans ce cas là si un processus fils du premier meurt avant son fils. Le PID1 va le récupérer et doit le nettoyer. Dans les autres cas il faut juste nettoyer ses propres fils ne pas le faire est un bug (avec ou sans docker).

                    Du coup je comprends si comme dans les exemples donnés tu fais du nginx + cgi + grep depuis ton code PHP c'est utile. Ce genre d'architecture a tendance à disparaître je pense (au profit des solutions type php-fpm).

                    AMHA ce n'est pas un problème suffisamment présent, c'est juste un problème qui peut exister et qui n'est pas chère à assurer.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.