From 21677258c6855c140c514e5893768eb13c190ac2 Mon Sep 17 00:00:00 2001 From: hadestructhor <60148800+hadestructhor@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:38:48 +0100 Subject: [PATCH] feat: Alpine Dockerfile version --- .woodpecker/.workflow.yml | 5 +- Dockerfile | 4 +- README.md | 122 +++++++++++++++++++++++- assets/react-multistage-alpine-size.png | Bin 0 -> 16396 bytes 4 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 assets/react-multistage-alpine-size.png diff --git a/.woodpecker/.workflow.yml b/.woodpecker/.workflow.yml index e1e1b84..498fb10 100644 --- a/.woodpecker/.workflow.yml +++ b/.woodpecker/.workflow.yml @@ -21,6 +21,5 @@ steps: - /var/run/docker.sock:/var/run/docker.sock commands: - docker login forgejo.transprot.eu -u $DOCKER_USERNAME -p $DOCKER_PASSWORD - - docker build -t 'forgejo.transprot.eu/public/react-multistage:${CI_PIPELINE_NUMBER}' -t 'forgejo.transprot.eu/public/react-multistage:latest' . - - docker push forgejo.transprot.eu/public/react-multistage:${CI_PIPELINE_NUMBER} - - docker push forgejo.transprot.eu/public/react-multistage:latest \ No newline at end of file + - docker build -t 'forgejo.transprot.eu/public/react-multistage-alpine:latest' . + - docker push forgejo.transprot.eu/public/react-multistage-alpine:latest \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index db90bcb..a543662 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage of the application -FROM node:18 AS build +FROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install @@ -7,7 +7,7 @@ COPY . . RUN npm run build # Production stage to run the application -FROM node:18 AS production +FROM node:18-alpine AS production COPY --from=build /app/build /prod RUN npm install --global serve EXPOSE 3000 diff --git a/README.md b/README.md index 0efbde1..613c680 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ Pour la version en français, c'est ici: [Français](#français) # English + +# You can pull these images from my public repository and try them out ! + +All the images I built during this project are available on my public repository that you can find in the [Packages](https://forgejo.transprot.eu/public/react-docker-optimization/packages) section of my Frgejo instance. + +You can run the following command to pull the disastrous 2.03 GB image for example: +```shell +docker pull forgejo.transprot.eu/public/react-simple +``` + +Then you can use the following command to run the image you just pulled: +```shell +docker run -d -p 80:3000 --name react-simple react-simple +``` + +You'll then have access to the deployed basic app at the following address: +[http://localhost:80](http://localhost:80) + ## How to reduce the size of your docker 🐳 images 💿? We're starting from a basic React application générated with a simple Dockerfile with disastrous conséquences: ```Dockerfile @@ -89,8 +107,68 @@ I am still usig Nushell on my end to have a pretty print and filter on the image The image now weighs `44 MB` less, which is 1.27 times smaller, and a whopping 21.67% reduction in size! +### Second optimisation: using alpine images + +Alpine images are based on the Alpine Linux distro, which is a minimalistic and light weight version of Linux. It's realle stupid and simple, but if your images are built using a small base image, your resulting image will also be small ! + +It's as easy as adding `-alpine` on your images in your Dockerfile. +Here's the new version of the Dockerfile using an alpine image of node: +```Dockerfile +# Build stage of the application +FROM node:18-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Production stage to run the application +FROM node:18-alpine AS production +COPY --from=build /app/build /prod +RUN npm install --global serve +EXPOSE 3000 +CMD ["serve", "prod/"] +``` + +You can rebuild the image by running the following command: +```shell +docker build -t 'react-multistage-alpine' . +``` + +You can also verify the image sizes by running the following command: +```shell +docker image ls +``` + +And as always, I'm using Nushell to have a prettier output, which gives me the following results which are incredibly impressive: +![Taille image node alpine](./assets/react-multistage-alpine-size.png) + +A size reduction of `1.82 GB`, which is an image that is 9.8 smaller than the first one and is equivalent to a size reduction of 89.8% !!!! + +And the best in all of this ? Not only is the image smaller, but it is way more performant as it only includes what is necessary to run the application on the alpine version of node. + +But we can go even further beyond in optimizing this image. + # Français + +# Vous pouvez utiliser ces images et les pull sur votre machine pour les testes via mon repository publique ! + +Toutes les images que je construis durant ce projet sont disponible sous l'onglet [Packages](https://forgejo.transprot.eu/public/react-docker-optimization/packages)de mon instance de forgejo. + +Vous pouvez par exemple lancer la commande suivante pour télécharger l'image simple qui pèse un désastreux 2.03 GB: +```shell +docker pull forgejo.transprot.eu/public/react-simple +``` + +Vous pouvez ensuite lancer cette image avec la commande suivante : +```shell +docker run -d -p 80:3000 --name react-simple react-simple +``` + +Ce qui vous donnera accès à l'application basique à l'adresse suivante : +[http://localhost:80](http://localhost:80) + ## Comment réduire la taille de vos images 💿 docker 🐳 ? On part d'une application React tout juste généré et avec un Dockerfile simple mais avec de grosses conséquences : @@ -178,4 +256,46 @@ docker image ls Pour ma part j'utilise toujours Nushell pour avoir cette belle présentation des résultats filtré: ![Taille de l'image multistage](./assets/react-multistage-size.png) -L'image pèse maintenant `44 MB` de moins, on a une image 1.27 fois plus petite, soit une diminution de la taille de 21.67% ! \ No newline at end of file +L'image pèse maintenant `44 MB` de moins, on a une image 1.27 fois plus petite, soit une diminution de la taille de 21.67% ! + +### Deuxième optimisation: utilisation d'images alpines + +Les images alpine sont basé sur la distro Alpine Linux qui est une version minimale et peu volumineuse de Linux. C'est tout bête, mais si l'image de base de votre image est petite, votre image finale le sera aussi ! + +C'est tout aussi simple que de rajouter `-alpine` sur vos image docker dans le Dockerfile. +Voici la nouvelle version du Dockerfile: +```Dockerfile +# Build stage of the application +FROM node:18-alpine AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Production stage to run the application +FROM node:18-alpine AS production +COPY --from=build /app/build /prod +RUN npm install --global serve +EXPOSE 3000 +CMD ["serve", "prod/"] +``` + +Puis vous pouvez reconstruire l'image en lançant la commande suivante: +```shell +docker build -t 'react-multistage-alpine' . +``` + +Vous pouvez maintenant lancer la commande suivante vérifier la taille de votre image: +```shell +docker image ls +``` + +Et comme d'habitude, j'utilise Nushell pour avoir un meilleur rendu visuelle, et voici les résultats qui sont impréssionnants: +![Taille image node alpine](./assets/react-multistage-alpine-size.png) + +On a une réduction de `1.82 GB`, soit une image 9.8 fois plus petite et une réduction de 89.8% !!!! + +Et le meilleur dans tout ça ? Non seulement l'image est plus petite, mais elle est plus performante car elle ne contient que le strict minimum nécessaire pour faire tourner node via la version alpine. + +Mais on peut encore aller plus loins dans l'optimisation. \ No newline at end of file diff --git a/assets/react-multistage-alpine-size.png b/assets/react-multistage-alpine-size.png new file mode 100644 index 0000000000000000000000000000000000000000..44c14777e24b1c2ab3c705650661de60f587405a GIT binary patch literal 16396 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 YhBqK|1;6ok@ZV5;tM;Z;=KYud52htiUH||9 literal 0 HcmV?d00001