react-docker-optimization/README.md

470 lines
16 KiB
Markdown
Raw Permalink Normal View History

2025-01-03 23:24:22 +01:00
Pour la version en français, c'est ici: [Français](#français)
# English
2025-01-04 18:38:48 +01:00
# 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)
2025-01-03 23:24:22 +01:00
## 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
FROM node:18
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
```
For your information, the image weight a whopping 2.03GB 🤯 !!!!! A mountain 🏔️ of data for a simple web application that is empty at the moment!!!
You can test it yourself by first running the following command:
```shell
npm install
```
Then run the following command to build the docker image:
```shell
docker build -t 'react-simple' .
```
You can inspect your images and verify the size yourself with the following command:
```shell
docker image ls
```
I use Nushell and rigrep to filter only on the image in question using the following command:
```shell
docker image ls | from ssv | where repository starts-with 'react'
```
Here's the result of running this command that shows how shamefully big the image is:
![Size of the image](./assets/react-simple-size.png)
2025-01-04 17:39:34 +01:00
### First optimisation: using a .dockerignore file and multi-stage build
The first optimization that we can do is using a `.dockerignore` file. Just like a `.gitignore` file, a `.dockerignore` file allows us to exclude file and folders that we don't want to include in the docker image that we are building.
Here's the `.dockerignore` file that I added to this simple project to mainly exclude the dark hole that node_modules are `node_modules`:
```
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
build
assets
```
In our context, this command only optimizes the build time and not the build size, as the `309 MB` that the node_modules folder occupies are not needed to build the docker imagen but as we run the `npm install` command in any case inside the Dockerfile, this folder is generated and included from inside the Dockerfile.
A better optimisation of the image is to use multi-stage build. Multi-stage builds allows us to include only what is necessary to run the image, and to not include the build files or node_modules for example.
Here's the new version of the `Dockerfile` using multi-stage build:
```Dockerfile
# Build stage of the application
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage to run the application
FROM node:18 AS production
COPY --from=build /app/build /prod
RUN npm install --global serve
EXPOSE 3000
CMD ["serve", "prod/"]
```
You can run the following command to build the image:
```shell
docker build -t 'react-multistage' .
```
You can run the following command to verify that the new image size is smaller than the previous one:
```shell
docker image ls
```
I am still usig Nushell on my end to have a pretty print and filter on the images that I want which gives me the following output:
![Multistage image size](./assets/react-multistage-size.png)
The image now weighs `44 MB` less, which is 1.27 times smaller, and a whopping 21.67% reduction in size!
2025-01-04 18:38:48 +01:00
### 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.
2025-01-04 21:39:24 +01:00
### Third optimization: Using a better http server: Nginx
The next optimization that we can perform is using a nginx server instead of a node server to run our application.
This will serve the static build files faster than the node server and it will also be better perfomance, memory and requests wise as node performs worse than nginx to serve files under the same conditions.
Here's the updated Dockerfile to do so:
```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 nginx AS production
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
You can run the following build command to generate the image:
```shell
docker build -t 'react-nginx' .
```
To run this image, you will need to run the following command:
```shell
docker run -d -p 80:80 --name react react-nginx
```
And you can also run the following command to check the size of the image:
```shell
docker image ls
```
But there's a tiny problem with this solution, this only optimizes the perfomance of the image but the size of the regular Nginx server is bigger than the Node Alpine version.
Here's an image showing the size of all the images so far:
![Nginx image size](./assets/react-nginx-size.png)
2025-01-04 22:06:17 +01:00
### Fourth optimization: The obvious, Nginx Alpine
The size of the regular Nginx server maybe be bigger than the Node Alpine version, but that's not really a fair comparison.
So to keep this fair, we'll also use the Nginx Alpine version to build the application.
Here's the updated Dockerfile to do so:
```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 nginx:stable-alpine AS production
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
You can run the following build command to generate the image:
```shell
docker build -t 'react-nginx-alpine' .
```
To run this image, you will need to run the following command:
```shell
docker run -d -p 80:80 --name react react-nginx-alpine
```
And you can also run the following command to check the size of the image:
```shell
docker image ls
```
Here's an image showing the size of all the images so far:
![Nginx alpine image size](./assets/react-nginx-alpine-size.png)
The image now weighs 1 954,5 MB less, which is 26,88 times smaller, and a whopping 96,28% reduction in size!!!!! And with better perfomances of the server to top it off!!!
2025-01-03 23:24:22 +01:00
# Français
2025-01-04 18:38:48 +01:00
# 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)
2025-01-03 23:24:22 +01:00
## 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 :
```Dockerfile
FROM node:18
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
```
Pour indication, l'image qui en résulte pèse environ 2.03GB 🤯 !!!!! Une montagne 🏔️ de données pour une simple application web vide pour l'instant !!!
Vous pouvez le testez vous même en lançant d'abord la commande suivante:
```shell
npm install
```
Puis en lançant la commande suivante pour builder l'image:
```shell
docker build -t 'react-simple' .
```
Vous pouvez inspecter la taille de votre image avec la commande suivante:
```shell
docker image ls
```
J'utilise personnellement Nushell et rigrep pour ne filtrer que sur l'image en question:
```shell
docker image ls | from ssv | where repository starts-with 'react'
```
Voici un aperçu de ce que cette commande m'affiche avec le poids de l'image assez désastreux:
2025-01-04 17:39:34 +01:00
![Taille de cette image](./assets/react-simple-size.png)
### Première optimisation: utilisation de .dockerignore et multi-stage
Une des première optimisation que l'on peut faire est d'utiliser un fichier `.dockerignore`. Tout comme un fichier `.gitignore`, le fichier `.dockerignore` permet de ne pas include certains dossiers ou fichier qui ne sont pas nécessaire pour l'image que l'on construit.
Voici le `.dockerignore` que j'ai rajouté à ce simple projet pour notamment éviter d'inclure le trou noir qu'est le dossier `node_modules`:
```
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
build
assets
```
Cependant, ce que cette commande optimise est le temps du build de l'image, car les `309 MB` qu'occupe actuellement ce dossier ne sont pas nécessaire à inclure dans l'image lors du build de celle-ci, mais comme nous lançons la commande `npm install` dans tous les cas dans le Dockerfile, ils sont réinstallé et ré-inclut dans l'image.
Une meilleure optimisation de l'image est d'utiliser le multi-stage build. Le multi-stage build permet de n'inclure que ce qui est nécessaire pour exécuter votre programme et de ne pas inclure les fichier de build ou les node_modules par exemple.
Voici la nouvelle version du `Dockerfile` découpé en multi-stage:
```Dockerfile
# Build stage of the application
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage to run the application
FROM node:18 AS production
COPY --from=build /app/build /prod
RUN npm install --global serve
EXPOSE 3000
CMD ["serve", "prod/"]
```
Vous pouvez lancer la commande suivante pour construire l'image:
```shell
docker build -t 'react-multistage' .
```
Vous pouvez relancer la comande suivante pour vérifier que la taille a bien diminué cette fois-ci:
```shell
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)
2025-01-04 18:38:48 +01:00
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.
2025-01-04 21:39:24 +01:00
Mais on peut encore aller plus loins dans l'optimisation.
### Troisième optimisation : Utilisation d'un meilleur serveur http: Nginx
La prochaine optimisation que l'on peut effectuer est l'utilisation de Nginx comme serveur pour servir notre application React plutôt que Node.
Les fichiers statiques construits de notre application seront envoyés aux utilisateurs plus rapidement, et le serveur utilisera moins de mémoire , performera mieux et pourra traiter plus de requête que le serveur node car nginx est plus performant sous des conditions similaires.
Voici à quoi ressemble notre Dockerfile à présent:
```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 nginx AS production
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
Vous pouvez lancer la commande suivante pour construire l'image:
```shell
docker build -t 'react-nginx' .
```
Pour démarrer un conteneur, vous pouvez lancer la commande suivante:
```shell
docker run -d -p 80:80 --name react react-nginx
```
Vous pouvez également vérifier la taille de l'image en lançant cette commande:
```shell
docker image ls
```
Mais il y a un petit hic avec cette solution, l'image est plus performante mais plus volumineuse, étant donnée que l'image classique de Nginx est plus volumineuse que celle de Node Alpine.
Voici une image de la taille de nos images docker jusqu'à présent :
2025-01-04 22:06:17 +01:00
![Nginx image size](./assets/react-nginx-size.png)
### Quatrième optimisation: l'évident, Nginx Alpine
La taille de l'image classique de Nginx est plus grosse que celle de Node Alpine, mais ce n'est pas vraiment une comparaison équitable.
Pour rendre cette situation plus équitable, nous allons utiliser la version Alpine de Nginx.
Voici à quoi ressemble notre Dockerfile à présent:
```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 nginx:stable-alpine AS production
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
Vous pouvez lancer la commande suivante pour construire l'image:
```shell
docker build -t 'react-nginx-alpine' .
```
Pour démarrer un conteneur, vous pouvez lancer la commande suivante:
```shell
docker run -d -p 80:80 --name react react-nginx-alpine
```
Vous pouvez également vérifier la taille de l'image en lançant cette commande:
```shell
docker image ls
```
Voici une image de la taille de nos images docker jusqu'à présent:
![Nginx alpine image size](./assets/react-nginx-alpine-size.png)
L'image pèse 1 954,5 MB de moins que celle de départ, ce qui est 26,88 fois plus petit, et une énorme reduction 96,28% de la taille de l'image !!!!! Et pour couronner le tout, avec de meilleur performances !!!