<!--
  canonical-url: https://www.avonture.be/fr/blog/docker-docusaurus-prod
  published:     2024-04-28
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Encapsuler un site Docusaurus complet dans une image Docker

> Déployez votre site de documentation Docusaurus en production avec Docker. Suivez ce guide pas à pas pour créer une image Docker multi-stage très efficace.

---
![Encapsuler un site Docusaurus complet dans une image Docker](/img/v2/docusaurus_using_docker.webp)

**TL;DR**

Cet article (aujourd'hui remplacé par un plus récent) montre comment empaqueter un site Docusaurus complet dans une seule image Docker autonome, à l'aide d'un `Dockerfile` en trois stages : un stage de base Node, un stage de build qui lance `yarn build`, et un stage final `nginx` qui n'embarque que la sortie statique — faisant passer l'image de \~740 Mo à \~87 Mo. On y voit le build, l'exécution, l'ouverture d'un shell interactif et la publication de l'image sur Docker Hub.

> **Cet article est obsolète:**
>
> Cet article a été revu récemment, lisez plutôt [Running Docusaurus using Docker](/blog/running-docusaurus-with-docker).

Commencé le 2 novembre 2023, cet article est déjà le centième que je publie sur ce blog. Pour marquer l'occasion, je voulais faire quelque chose d'un peu spécial en l'honneur du blog. Quoi de mieux qu'une version **100% Docker**, c'est-à-dire sous la forme d'une image Docker téléchargeable que **vous pouvez lancer avec une seule ligne de commande**.

Une seule commande pour télécharger le blog et le faire tourner sur votre machine (Linux, Mac ou Windows) avec un site parfaitement fonctionnel ; sympa, non ?

Et comme d'habitude, vous trouverez ci-dessous toutes les informations nécessaires pour faire de même avec votre propre installation Docusaurus.

> **N'attendez pas plus longtemps:**
>
> Ouvrez une console, lancez `docker pull cavo789/blog && docker run -d -p 80:80 --name blog cavo789/blog` pour télécharger une copie locale de mon blog et la démarrer. Ensuite, ouvrez simplement `http://localhost` sur votre ordinateur et... félicitations, vous venez d'obtenir une version hors ligne et exécutable !

Voyons maintenant comment faire la même chose pour votre propre instance Docusaurus. Mesdames et messieurs, suivez le guide...

## Résultat {#result}

Cette unique commande `docker run` de l'AlertBox ci-dessus vous donne ceci, entièrement hors ligne :

> Screenshot — http\://localhost
>
> ![Page d'accueil de votre instance Docusaurus en cours d'exécution](./images/homepage.webp)

> Screenshot — http\://localhost/blog
>
> ![Notre blog](./images/blog.webp)

La suite de cet article construit l'image qui produit ce résultat : un `Dockerfile` multi-stage qui
transforme n'importe quel site Docusaurus en une seule image autonome de \~87 Mo.

## Créer un blog bidon si besoin {#create-a-dummy-blog-if-needed}

*Si vous avez déjà une installation Docusaurus sur votre ordinateur, sautez ce chapitre. Sinon, [Running Docusaurus with Docker](/blog/docusaurus-docker) couvre la configuration de développement qui précède celle-ci.*

Dans cette partie, je suppose que vous n'avez pas encore d'installation Docusaurus. Nous allons donc créer un site bidon avec quelques articles de blog très basiques.

Ouvrez une console et lancez les commandes suivantes : `mkdir /tmp/docusaurus && cd $_`.

Vous êtes maintenant dans le dossier `/tmp/docusaurus`.

Copiez/collez dans la console les instructions ci-dessous, parenthèses incluses. Cela va créer un sous-dossier appelé `blog` avec trois fichiers markdown dedans, nos trois faux articles.

```bash
(
  $ mkdir -p blog && cd $_
  $ echo '---' > 2024-04-26-welcome-world.md
  $ echo 'title: Hello World!' >> 2024-04-26-welcome-world.md
  $ echo '---' >> 2024-04-26-welcome-world.md
  $ echo 'Hello world! Proud to be here!!!' >> 2024-04-26-welcome-world.md
  $ echo '---' > 2024-04-27-my-first-post.md
  $ echo 'title: My first blog post' >> 2024-04-27-my-first-post.md
  $ echo '---' >> 2024-04-27-my-first-post.md
  $ echo 'My first blog post' >> 2024-04-27-my-first-post.md
  $ echo '' >> 2024-04-27-my-first-post.md
  $ echo '![Unsplash random](https://source.unsplash.com/random?dinosaure)' >> 2024-04-27-my-first-post.md
  $ echo '---' > 2024-04-28-my-second-post.md
  $ echo 'title: My second blog post' >> 2024-04-28-my-second-post.md
  $ echo '---' >> 2024-04-28-my-second-post.md
  $ echo 'My second blog post' >> 2024-04-28-my-second-post.md
  $ echo '' >> 2024-04-28-my-second-post.md
  $ echo '![Unsplash random](https://source.unsplash.com/random?dinosaure)' >> 2024-04-28-my-second-post.md
)
```

Voilà, nous avons maintenant un blog bidon.

## Préparer notre installation Docusaurus pour Docker {#prepare-our-docusaurus-installation-for-docker}

*Si vous avez sauté le chapitre précédent parce que vous aviez déjà une instance Docusaurus, placez-vous dedans (`cd <your_docusaurus_folder>`).*

Nous allons devoir créer quelques fichiers pour Docker.

Lancez votre éditeur préféré et ouvrez le dossier ; de mon côté, j'utilise Visual Studio Code, donc je lance simplement `code .` dans ma console Linux.

Il nous faut créer quelques fichiers...

### Créer un fichier Dockerfile {#create-a-dockerfile-file}

Créez un fichier appelé `Dockerfile` avec ce contenu :

**Dockerfile**

```docker
# syntax=docker/dockerfile:1

# Inspired by https://docusaurus.community/knowledge/deployment/docker/?package-managers=yarn

# Stage 1: Base image.

FROM node:22-alpine AS base

# Disable color output from yarn to make logs easier to read.
ENV FORCE_COLOR=0

# Enable corepack.
RUN corepack enable

# --------------------------------------------------------------------

# Stage 2: Production build mode.

FROM base AS building_production

# Install the latest version of Docusaurus
RUN npx create-docusaurus@latest /opt/docusaurus/ classic --javascript \
  # Uncomment the line below to remove dummy files like dummy blog, docs, src, ...
  # && rm -rf /opt/docusaurus/blog /opt/docusaurus/docs /opt/docusaurus/src\
  && chown -R node:node /opt/docusaurus/

# Set the working directory to `/opt/docusaurus`.
WORKDIR /opt/docusaurus

# We need our package.json / yarn.lock file before running yarn install
COPY package.json yarn.lock ./

# Install dependencies with `--immutable` to ensure reproducibility.
RUN yarn install --immutable

# Copy over the source code.
COPY . /opt/docusaurus/

# Build the static site (generated files will be created in /opt/docusaurus/build)
RUN yarn build

# --------------------------------------------------------------------

# Stage 3: Serve with nginx

FROM nginx:stable-alpine3.19-perl AS production

# Copy the Docusaurus build output.
COPY --from=building_production /opt/docusaurus/build /usr/share/nginx/html

WORKDIR /usr/share/nginx/html
```

Ce fichier est un Dockerfile **multi-stage**. Les objectifs principaux sont d'avoir un meilleur système de cache des layers et une image finale plus petite en taille. Un fichier multi-stage est aussi très pratique pour pouvoir construire plusieurs images, par exemple une de développement et une de production.

Comme vous le voyez, nous utilisons trois stages (un stage commence par la clause `FROM`).

Notre but dans cet article est de créer une image Docker finale contenant une version statique de notre installation Docusaurus. **Nous voulons une image contenant un serveur web et notre site.**

Avant de pouvoir faire ça, il nous faut :

1. Utiliser une image `node` puisque Docusaurus est une application `NodeJS`. Nous utiliserons `node:21-alpine` comme image de base.
2. Ensuite, il faut installer `docusaurus` et lancer `yarn` pour installer les dépendances. Cela fait, nous copierons tous les fichiers de notre dossier courant dans l'image Docker. Une fois copiés, il faut builder notre site et convertir les fichiers markdown en pages HTML et, enfin,
3. Il nous faut un serveur web comme `Apache`, `Caddy` ou `nginx` *(`Caddy` donnera l'image la plus petite mais configurer le SSL avec Docker reste assez compliqué)*.

Voilà nos trois stages.

**Le stage 1 s'appelle `base`**. Dans ce stage, on ne fait presque rien : télécharger une version alpine de `node` et initialiser quelques variables. Cette étape pourra, à l'avenir, aussi servir à un stage `development` mais, dans cet article, concentrons-nous sur celui de production.

**Dockerfile**

```docker
FROM node:21-alpine AS base

# Disable color output from yarn to make logs easier to read.
ENV FORCE_COLOR=0

# Enable corepack.
RUN corepack enable
```

**Dans le stage 2, appelé `building_production`**, nous étendons le stage `base`.

Comme vous le voyez ci-dessous, nous allons installer Docusaurus et ses dépendances.

Ensuite, nous copierons les fichiers de notre dossier courant dans l'image en cours de création et, enfin, nous générerons (builderons) les pages web statiques (HTML, CSS, JavaScript et images).

À la fin de ce stage, nous obtiendrons une version statique de notre site Docusaurus dans le dossier `/opt/docusaurus/build`.

**Dockerfile**

```docker
FROM base AS building_production

# Install the latest version of Docusaurus
RUN npx create-docusaurus@latest /opt/docusaurus/ classic --javascript \
  # Uncomment the line below to remove dummy files like dummy blog, docs, src ...
  # && rm -rf /opt/docusaurus/blog /opt/docusaurus/docs /opt/docusaurus/src \
  && chown -R node:node /opt/docusaurus/

# Set the working directory to `/opt/docusaurus`.
WORKDIR /opt/docusaurus

# We need our package.json / yarn.lock file before running yarn install
COPY package.json yarn.lock ./

# Install dependencies with `--immutable` to ensure reproducibility.
RUN yarn install --immutable

# Copy over the source code.
COPY . /opt/docusaurus/

# Build the static site (generated files will be created in /opt/docusaurus/build)
RUN yarn build
```

Ce qui est important à noter ici, c'est notre répertoire de travail : `/opt/docusaurus/`. Notre site statique a été copié dans ce dossier, donc le résultat de la dernière instruction du deuxième stage (`yarn build`) va créer un sous-dossier `build` dans `/opt/docusaurus/`.

À la fin de ce stage, nous avons notre site statique mais pas encore de serveur web.

**Dans le stage 3**, nous allons utiliser [nginx](https://hub.docker.com/_/nginx).

**Dockerfile**

```docker
FROM nginx:stable-alpine3.19-perl AS production

# Copy the Docusaurus build output.
COPY --from=building_production /opt/docusaurus/build /usr/share/nginx/html

WORKDIR /usr/share/nginx/html
```

Comme vous le voyez, ce stage est très basique. On utilise juste nginx, on copie le site statique créé plus tôt dans son dossier web par défaut et c'est tout.

> **Pourquoi une image multi-stage est-elle meilleure qu'un stage `monolithique` ?:**
>
> Regardez le dernier stage, notre serveur web. On utilise juste le serveur web `nginx` et pour le faire fonctionner, on doit copier notre dossier `build` où Docusaurus a mis ses fichiers statiques (html, css, js et images).
>
> Comme on ne récupère que le dossier `build` du stage précédent, l'image finale ne contiendra plus `NodeJs`, `Yarn`, `Docusaurus` ni rien d'autre installé auparavant. Nous n'aurons pas non plus de fichiers temporaires éventuellement installés ; nous n'avons pas besoin de la version originale de notre blog, c'est-à-dire de nos fichiers markdown d'origine.
>
> Nous utiliserons l'image `nginx` et dans cette image, nous copierons uniquement les fichiers dont nous avons besoin depuis le stage précédent. Ainsi, notre image finale sera plus petite et ne contiendra que ce qu'il faut pour faire tourner l'application finale, ici notre site statique Docusaurus.
>
> La taille de l'image du stage `building_production` était de 740 Mo et celle du stage `nginx` n'est que de 87 Mo. **Environ 9 fois moins !!!**

> **Voir mon propre Dockerfile:**
>
> Pour mon propre blog, j'utilise un stage de développement. Jetez un œil à mon [Dockerfile](https://github.com/cavo789/blog/blob/main/Dockerfile) pour voir comment je procède. Regardez aussi mon [makefile](https://github.com/cavo789/blog/blob/main/makefile) et les différents fichiers `docker-compose-xxx.yml` de mon projet.

### Créer un fichier .dockerignore {#create-a-dockerignore-file}

Le deuxième fichier à créer doit s'appeler `.dockerignore` et contenir ceci :

**.dockerignore**

```
build/
node_modules/
.git/

.dockerignore
.gitignore
.markdownlint_ignore
.markdownlint.json
*.log
compose.yaml
Dockerfile
LICENSE
makefile
README.md
```

Le fichier `.dockerignore` est là pour demander à Docker de ne pas tout copier dans votre image finale lors de l'exécution de l'instruction `COPY . /opt/docusaurus/` présente dans le `Dockerfile`.

En effet, dans notre image finale, nous n'avons pas besoin par exemple des dossiers `build/` ou `node_modules/` puisqu'ils seront créés pendant le build de l'image. Nous n'avons pas besoin de notre dossier `.git/` (s'il existe) non plus, ni des fichiers temporaires ou nécessaires uniquement au build.

Le fichier `.dockerignore` sert donc à garder une image plus petite et à éviter de copier des fichiers sensibles (ceux contenant des secrets ou des éléments de configuration) dans l'image finale.

## Petit résumé {#small-summary}

Si vous avez suivi la création du dossier temporaire décrite plus haut, vous devriez avoir la situation suivante : deux fichiers dans le dossier `/tmp/docusaurus` et trois fichiers Markdown dans le sous-répertoire `blog`.

```bash
$ ls -alhR
drwxr-xr-x  3 root root 4.0K Apr 28 11:02 .
drwxrwxrwt 25 root root  12K Apr 28 10:44 ..
-rw-r--r--  1 root root  155 Apr 27 09:47 .dockerignore
-rw-r--r--  1 root root 1.2K Apr 28 10:45 Dockerfile
drwxr-xr-x  2 root root 4.0K Apr 28 11:02 blog
.
$ ls -alh ./blog:
.
drwxr-xr-x 2 root root 4.0K Apr 28 11:02 .
drwxr-xr-x 3 root root 4.0K Apr 28 11:02 ..
-rw-r--r-- 1 root root   61 Apr 28 11:02 2024-04-26-welcome-world.md
-rw-r--r-- 1 root root  119 Apr 28 11:02 2024-04-27-my-first-post.md
-rw-r--r-- 1 root root  121 Apr 28 11:02 2024-04-28-my-second-post.md
```

## Builder notre image Docker {#build-our-docker-image}

Notre objectif était de créer une image Docker contenant notre site Docusaurus.

Quand on crée une image Docker, il faut lui donner un nom.

> **Le nom de l'image doit respecter le motif `owner/name`.:**
>
> `owner` doit être votre nom d'utilisateur sur Docker Hub. Dans mon cas, mon nom d'utilisateur est `[cavo789](https://hub.docker.com/u/cavo789)`, donc si je veux publier une image, je dois utiliser `cavo789` comme première partie. Ensuite, je dois indiquer un nom unique pas encore présent dans mon profil.
>
> Dans mon cas, `cavo789/blog` est donc un bon choix. Pour cet article, j'utiliserai `johndoe/blog` puisque je ne vais pas publier cette image sur Internet.

Toujours dans votre console, lancez la commande suivante :

```bash
$ docker build --tag johndoe/blog --target production .
```

Le `.` final dans l'instruction ci-dessus signifie *dossier courant* ; `/tmp/docusaurus` dans mon cas.

> **Caution:**
>
> Comme notre `Dockerfile` est multi-stage, nous devons préciser quel stage nous voulons. Cela se fait avec le flag CLI `--target`.
>
> Si vous regardez le fichier `Dockerfile` créé plus tôt, nos trois stages s'appellent `base`, `building_production` et `production`. Pour builder l'image avec le serveur web, vous devez spécifier `production` comme cible, mais si ce sont les fichiers générés qui vous intéressent, pas le serveur web, vous pouvez lancer `docker build --tag johndoe/blog --target building_production .`.

La commande `docker build` prendra une ou deux minutes selon la vitesse de votre connexion réseau et de votre ordinateur. Une fois exécutée avec succès, vous aurez une nouvelle image Docker sur votre machine. Vous pouvez la retrouver en lançant `docker image list` pour obtenir la liste des images locales.

> **Information:**
>
> En lançant `docker image list | grep -i blog`, vous pouvez retrouver l'image et sa taille. Pour moi, c'est 87 Mo à cet instant pour le blog bidon créé dans cet article.
>
> En lançant `docker build --tag johndoe/blog --target building_production .` (donc sans `nginx` mais avec `Node`), la taille sera de 740 Mo. Comme vous le voyez, nous avons divisé la taille par presque 9.

## Et l'utiliser {#and-use-it}

Maintenant que notre image est créée, on peut faire plusieurs choses : la lancer simplement pour obtenir une version hors ligne de notre site Docusaurus, jouer avec un container et démarrer une session shell interactive ou, bien sûr, la publier sur Docker Hub.

### Lancer l'image, c'est-à-dire créer un container et faire tourner le site {#run-the-image-ie-create-a-container-and-run-the-site}

Pour ça, lancez simplement la commande suivante :

```bash
$ docker run -d --publish 80:80 --name blog johndoe/blog
```

Très vite, vous obtiendrez un très long ID en résultat, du genre `cae6989bee2a2339a4c0116be2b86ee3dae0b46d47a6c53dcb6e50098726c0b1`. Ignorez-le pour le moment, ça veut juste dire que votre container a été créé avec succès.

Maintenant, lancez votre navigateur et rendez-vous sur `http://localhost
:
80` — c'est la page d'accueil et le blog montrés en haut de cet article.

Comme dit plus haut, le site tournera sur votre ordinateur, donc hors ligne. Vous pouvez vous déconnecter d'Internet, tout tourne en RAM ; sur votre machine.

Si vous êtes curieux, lancez `docker ps` (ou `docker container list`, qui est un synonyme) pour voir la liste des containers. Vous verrez le vôtre.

En lançant `docker container stop 
blog` (blog est le nom défini dans le `docker run` utilisé plus haut dans ce chapitre), on peut arrêter le blog. Retournez dans votre navigateur, allez sur `http://localhost
:
80` et vous verrez : le site n'est plus actif. Lancez `docker container start 
blog` pour le réactiver.

### Démarrer une session shell interactive {#start-an-interactive-shell-session}

Si vous voulez démarrer un shell interactif dans votre image, lancez simplement `docker run -it johndoe/blog /bin/sh`. Vous serez alors *à l'intérieur* du container et vous pourrez inspecter les fichiers, par exemple.

Attention à bien comprendre la notion de container : votre site tourne dans un container et vous venez d'en démarrer un second. Pour le prouver, lancez `rm -rf blog` pour tuer le blog dans votre container shell, retournez dans le navigateur, rafraîchissez la page : ça fonctionne toujours.

Tapez `exit` pour quitter le shell et revenir à votre console. Relancez `docker run -it johndoe/blog /bin/sh` et vérifiez si le dossier `blog` supprimé est là ou pas : il est là.

Un container est quelque chose qui est recréé à chaque fois, donc tout ce qui est fait dans un container (quand il n'y a pas de volumes montés comme celui utilisé ici) reste juste en RAM.

> **Utilisez Docker Desktop - Containers si vous voulez interagir avec votre site:**
>
> Purement informatif : si vous êtes sous Windows, vous pouvez aller dans *Docker Desktop*, ouvrir la liste des containers, cliquer sur celui qui tourne (le blog) puis cliquer sur l'onglet *Exec* pour démarrer un shell interactif dans *ce* container. Si vous supprimez par exemple le dossier blog comme illustré ci-dessous, alors en retournant dans le navigateur, oui, vous avez supprimé le blog et rafraîchir le site mènera à une page d'erreur 404.
>
> ![Docker Desktop](./images/docker_desktop.webp)
>
> Supprimez simplement le container et recréez-le pour retrouver le blog.

### La pousser sur Docker {#push-it-on-docker}

Avant de pouvoir publier votre image sur Docker Hub, assurez-vous d'abord d'être connecté.

Lancez `docker login` dans votre ligne de commande et connectez-vous avec un compte valide. *Si vous étiez déjà authentifié, vous verrez `Authenticating with existing credentials...`*

Maintenant, poussez votre image en lançant `docker image push` suivi du nom de votre image ; dans mon cas, ce sera `cavo789/blog` puisque je suis connecté en tant que `cavo789`.

### La récupérer depuis Docker Hub {#retrieve-it-from-docker-hub}

La boucle est bouclée... Vous pouvez dire à vos amis et collègues qu'ils peuvent désormais utiliser votre image en lançant simplement la commande `` `docker pull <your_image> && docker run -d -p `
80 `:80 --name `
blog ` <your_image>` `` comme je l'indiquais en préface de cet article.

Amusez-vous bien !
