# Running Docusaurus using Docker A series of 3 article(s), in reading order. # Exécuter Docusaurus avec Docker > Faites tourner Docusaurus avec Docker ! Suivez ce tutoriel simple pour créer votre image Docker Docusaurus, utiliser docker compose et lancer rapidement votre documentation ou votre blog. --- ![Exécuter Docusaurus avec Docker](/img/v2/docusaurus_tips.webp) **TL;DR** Ce guide explique comment faire tourner Docusaurus avec Docker pour éviter d'installer Node.js en local. Vous allez créer un Dockerfile sur mesure, mettre en place un `compose.yaml` pour le développement avec rechargement à chaud, et gérer le contenu de votre blog dans un environnement containerisé. Comme vous le savez, ce blog tourne avec [Docusaurus](https://docusaurus.io/). J'écris mes articles dans des fichiers Markdown (un article = un fichier `.md`) et Docusaurus les convertit en pages HTML. Dans ce premier article, nous allons apprendre à installer Docusaurus... oups, pardon, pas à installer Docusaurus puisque nous allons utiliser Docker pour nous simplifier la vie. ## Trois fichiers, une commande {#three-files-one-command} Trois fichiers dans un dossier vide — un `Dockerfile`, un `.dockerignore` et un `compose.yaml` — puis : ```bash $ docker compose up --detach --build ``` Quelques minutes plus tard (uniquement la première fois), `http://localhost:`3000 répond : > Screenshot — http\://localhost:3000 > > ![Page d'accueil de Docusaurus](./images/homepage.webp) Et le menu `Blog` liste les fichiers Markdown présents dans le dossier `blog` de votre propre machine : > Screenshot — http\://localhost:3000/blog > > ![Nos articles](./images/posts.webp) Pas de Node.js installé, pas de `npm`, pas de `yarn`, aucun conflit de version avec le reste de vos projets. ## Pourquoi ça fonctionne {#why-it-works} * **L'image construit le squelette Docusaurus au moment du build**, donc le moteur du site vit dans le container et ne touche jamais à votre machine. * **Votre dossier `blog` est monté depuis l'host**, pas copié : le container lit exactement les fichiers que vous éditez dans votre éditeur. * **Le rechargement à chaud suit vos fichiers** : enregistrer un fichier Markdown dans VS Code rafraîchit la page dans votre navigateur — le container exécute `yarn start`, pas un build statique. ## Créez votre propre image Docusaurus {#create-your-own-docusaurus-image} Créez un répertoire temporaire en lançant `mkdir /tmp/docusaurus && cd $_`. Ceci fait, démarrez 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. ### Créer un fichier Dockerfile {#create-a-dockerfile-file} Dans le répertoire de votre projet (donc `/tmp/docusaurus`), créez un fichier nommé `Dockerfile` avec ce contenu : **/tmp/docusaurus/Dockerfile** ```docker FROM node:22-alpine RUN npx create-docusaurus@latest /app classic --javascript && \ chown -R node:node /app USER node WORKDIR /app RUN cd /app && yarn install COPY . . CMD ["yarn", "start", "--host", "0.0.0.0"] ``` Sept lignes, et chacune mérite sa place — si vous voulez le détail ligne par ligne, une section dédiée se trouve à la fin de cet article. ### Créer un fichier .dockerignore {#create-a-dockerignore-file} Nous avons vu ci-dessus la commande `COPY . .` qui demande de copier tout le répertoire du projet depuis notre machine locale vers l'image Docker mais, en fait, non, nous n'avons pas besoin de tout copier. Dans l'image Docker, nous n'avons pas besoin de dossiers comme `build` ou `node_modules`, ni de fichiers comme `.gitignore` et quelques autres. Nous n'en avons pas besoin parce qu'ils seront créés dans Docker (`node_modules` est créé par la commande `yarn install`, donc inutile de perdre du temps à copier ce dossier). Mais nous n'avons pas non plus besoin de copier des dossiers comme `blog`, `pages`, `static`, ... puisque ces dossiers doivent rester sur notre machine et simplement être *synchronisés* avec le container Docker en cours d'exécution (donc inutile de les mettre dans l'image à ce stade). Créez donc un fichier `.dockerignore` avec ce contenu : **.dockerignore** ``` build/ node_modules/ .dockerignore .gitignore .markdownlint_ignore .markdownlint.json *.log compose.yaml Dockerfile LICENSE makefile README.md blog/ pages/ static/ ``` ### Créer un fichier compose.yaml {#create-a-composeyaml-file} Le troisième fichier à créer est `compose.yaml` avec ce contenu : **compose.yaml** ```yaml name: my_docusaurus_blog services: blog: build: . ports: - 3000:3000 user: 1000:1000 volumes: - ./blog:/app/blog ``` Comme vous le voyez, nous devons avoir un dossier nommé `blog` sur notre machine et nous allons synchroniser ce dossier dans le container Docker. Notre dossier `blog` sera *monté* dans le dossier `/app/blog` du container (si la notion de volumes est nouvelle pour vous, lisez [Share data between your running Docker container and your computer](/blog/docker-volume)). ### Créer quelques articles de blog simples {#create-simple-blog-items} Juste pour avoir un peu de contenu, créons automatiquement quelques articles. Lancez les commandes suivantes pour créer trois fichiers Markdown (nos articles) dans le dossier `blog` : ```bash $ ( mkdir -p blog && cd $_ echo '---' > 2024-02-04-welcome-world.md echo 'title: Hello World!' >> 2024-02-04-welcome-world.md echo '---' >> 2024-02-04-welcome-world.md echo 'Hello world! Proud to be here!!!' >> 2024-02-04-welcome-world.md echo '---' > 2024-02-05-my-first-post.md echo 'title: My first blog post' >> 2024-02-05-my-first-post.md echo '---' >> 2024-02-05-my-first-post.md echo 'My first blog post' >> 2024-02-05-my-first-post.md echo '' >> 2024-02-05-my-first-post.md echo '![Unsplash random](https://source.unsplash.com/random?dinosaure)' >> 2024-02-05-my-first-post.md echo '---' > 2024-02-06-my-second-post.md echo 'title: My second blog post' >> 2024-02-06-my-second-post.md echo '---' >> 2024-02-06-my-second-post.md echo 'My second blog post' >> 2024-02-06-my-second-post.md echo '' >> 2024-02-06-my-second-post.md echo '![Unsplash random](https://source.unsplash.com/random?dinosaure)' >> 2024-02-06-my-second-post.md ) ``` Si vous prenez le temps de regarder ce que contient maintenant notre dossier `/tmp/docusaurus`, voici la liste des fichiers / dossiers : ```tree expanded=true showJSX=false debug=false title="/tmp/docusaurus" . ├── .dockerignore ├── Dockerfile ├── blog │ ├── 2024-02-04-welcome-world.md │ ├── 2024-02-05-my-first-post.md │ └── 2024-02-06-my-second-post.md └── compose.yaml ``` > **`tree` ne fait pas partie de l'installation Linux de base:** > > Si l'utilitaire `tree` vous intéresse et que vous ne l'avez pas encore, lancez simplement `sudo apt-get update && sudo apt-get install tree` pour l'installer. Cette étape est optionnelle. ### Lancer Docusaurus {#run-docusaurus} À ce stade du tutoriel, vous avez tous les fichiers requis et quelques articles de blog. Démarrons donc le tout avec la commande `docker compose up --detach --build` montrée au début de cet article. Votre blog est alors accessible sur votre machine ici : `http://localhost:`3000. > **Quel numéro de port utiliser ?:** > > Le numéro de port est celui que vous avez indiqué dans le fichier `compose.yaml` à la ligne 3000`:3000`. > > Si vous préférez un autre port comme `3002` par exemple, éditez simplement le fichier yaml et remplacez `3000 > :3000` par `3002:3000`, puis relancez la commande `docker compose up --detach`. > **Chronologie des articles:** > > En cliquant sur le menu `Blog`, vous remarquerez que l'ordre par défaut suit l'ordre chronologique : le dernier article créé (`2024-02-06-my-second-post.md`) est le premier de la liste. Pour ce tutoriel, les images viennent de `unsplash.com` mais avec un thème : les dinosaures. *Si vous rafraîchissez la page, vous obtenez de nouvelles images.* ### Un rendu amélioré {#improved-look--feel} Nous allons créer un fichier sur notre machine, dans notre dossier `blog`. J'utilise VS Code donc je passe dans mon éditeur, je vais dans le dossier `blog` et je crée un nouveau fichier, disons : `2024-02-07-really-better.md` ![Bien mieux](./images/vscode.webp) Pour aller plus vite, rendez-vous sur ce tutoriel : . Copiez/collez l'exemple `2019-09-05-hello-docusaurus.md` dans votre fichier `2024-02-07-really-better.md`. > Screenshot — http\://localhost:3000/blog/2019-09-05-hello-docusaurus.md > > ![Notre nouvel article](./images/vscode-article.webp) Retournez dans votre navigateur et rafraîchissez la page (appuyez sur `F5`). Votre nouvel article est là ! > Screenshot — http\://localhost:3000/blog/2024-02-07-really-better.md > > ![Votre nouvel article est là](./images/with-new-post.webp) ## Sous le capot — le Dockerfile, ligne par ligne (à sauter si vous voulez juste que ça tourne) {#under-the-hood--the-dockerfile-line-by-line-skip-this-if-you-just-want-it-running} * Ligne 1 : nous utilisons Node.js v22 LTS dans sa version alpine, * Ligne 2 : la commande `RUN npx create-docusaurus@latest /app classic && chown -R node:node /app` installe la dernière version de Docusaurus (dans le dossier `/app`) et s'assure que le dossier appartient à notre utilisateur `node`, * Ligne 3 : à partir de maintenant, tout se fait avec l'utilisateur `node`, * Ligne 4 : `/app` sera le répertoire de travail par défaut dans l'image, * Ligne 5 : la commande `cd /app && yarn install` entre dans le dossier et installe les dépendances node, * Ligne 6 : `COPY . .` copie tout le répertoire de votre projet (sur votre host) dans l'image Docker (dans le dossier `/app` puisque c'est le répertoire de travail par défaut) et * Ligne 7 : la commande `CMD ["yarn", "start", "--host", "0.0.0.0"]` exécute `yarn start --host 0.0.0.0`, l'instruction qui lance Docusaurus, fait la conversion *transparente* des pages Markdown en HTML et affiche le site sur le port par défaut (le port `3000`). ## Arrêter et redémarrer {#stop-and-restart} Comme le dossier `blog` est stocké sur votre machine, nous pouvons arrêter le blog et le relancer sans rien perdre. Pour l'illustrer, lançons `docker compose stop && docker compose rm --force` pour arrêter et supprimer le container. Puis `docker compose up --detach --build` pour le reconstruire. En retournant sur `http://localhost : 3000`, vous verrez que votre blog est toujours là avec vos derniers changements. Rien n'a été perdu. ## Et ensuite ? {#whats-next} L'installation ci-dessus est une installation de *développement* : votre contenu reste sur votre host et est monté dans le container. Deux articles de suivi vont plus loin : * [Running your own blog with Docusaurus and Docker](/blog/docusaurus-docker-own-blog) — transformer ce bac à sable en votre vrai blog et * [Encapsulate an entire Docusaurus site in a Docker image](/blog/docker-docusaurus-prod) — construire une image autonome prête à être déployée. --- # 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 `).* 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 && docker run -d -p ` 80 `:80 --name ` blog ` ` `` comme je l'indiquais en préface de cet article. Amusez-vous bien ! --- # Faire tourner Docusaurus avec Docker > Simplifiez le développement de votre blog Docusaurus ! Construisez une seule image Docker pour la production et utilisez les Dev Containers de VS Code pour une édition cohérente, rapide et isolée. --- ![Faire tourner Docusaurus avec Docker](/img/v2/docusaurus_using_docker.webp) **TL;DR** Ce guide explique comment simplifier le développement d'un blog Docusaurus avec Docker. Il montre comment créer une seule image Docker pour les builds de production et comment exploiter les Dev Containers de VS Code pour obtenir un environnement de développement cohérent, rapide et isolé. Le setup enrichit le workflow avec des outils intégrés comme LanguageTool et Code Spell Checker pour améliorer la qualité du contenu. Simplifiez votre workflow Docusaurus avec une configuration propre et reproductible. Dans ce guide mis à jour, vous allez apprendre à maintenir un blog Docusaurus avec une seule image Docker, les DevContainers de VSCode et un workflow qui rend à la fois les builds de production et l'écriture quotidienne rapides et fiables. Il y a dix-huit mois, j'ai publié [Encapsuler un site Docusaurus entier dans une image Docker](/blog/docker-docusaurus-prod) mais, depuis, j'ai affiné l'approche, surtout autour de la séparation propre des environnements de **production** et de **développement** (voir [Une seule image Docker pour la production et les Devcontainers - la méthode propre](/blog/docker-prod-devcontainer)). Avec ce setup : * Vous lancez `make build` une seule fois pour créer une image Docker de base. * Vous démarrez votre environnement d'édition avec `make devcontainer`. * VSCode construit et ouvre automatiquement un container isolé avec tout de configuré. *Si Docusaurus est tout nouveau pour vous, commencez par [Running Docusaurus with Docker](/blog/docusaurus-docker) ; et une fois le site construit, [GitHub - Use Actions to deploy this blog](/blog/github-action) s'occupe de le publier.* Voyons le workflow complet. **Quick Jump:** [Récupérer un blog Docusaurus](#retrieve-a-docusaurus-blog) | [Créer l'image Docker de base](#create-the-base-docker-image) Voici où on va : `make build` une fois, `make up`, et le blog tourne déjà. > Screenshot — https\://localhost/blog > > ![Le blog tourne](./images/running_prod.webp) Quelques commandes plus bas et vous y serez aussi. **Ce que nous allons apprendre ici :** 1. Récupérer un blog Docusaurus, 2. Créer une image Docker pour faire tourner le blog 3. Ouvrir le projet dans VSCode avec les DevContainers 4. Enrichir l'environnement avec des extensions comme LanguageTool, Code Spell Checker ou Markdown lint. ## Récupérer un blog Docusaurus {#retrieve-a-docusaurus-blog} Si vous avez déjà un blog Docusaurus, sautez simplement ce chapitre. Pour la démonstration, nous allons utiliser le blog d'un ami, [Docux](http://docuxlab.com/), qui publie des composants et plugins Docusaurus très utiles. Récupérez une copie de son blog en lançant les commandes ci-dessous : ```bash $ cd /tmp && git clone git@github.com:Juniors017/docux-blog.git && cd docux-blog ``` ## Créer l'image Docker de base {#create-the-base-docker-image} Si vous débutez avec Docusaurus, vous vous demandez peut-être : *comment faire tourner le blog en local, tout simplement ?*. Réponse rapide : en lançant une image Docker autonome, et c'est ce que nous allons voir dans ce chapitre. Ouvrez d'abord VSCode en lançant `code .` : ```bash $ code . ``` Vous devriez maintenant voir la structure du projet dans votre éditeur. ![Le blog ouvert dans VSCode](./images/initial_after_clone.webp) ### Créer les fichiers requis {#create-required-files} À la racine du projet, créez les fichiers suivants : **Project: /tmp/docux-blog** **compose.yaml** ```yaml name: blog services: docusaurus: build: context: . target: ${TARGET:-development} dockerfile: Dockerfile container_name: docusaurus ports: - "443:443" command: sleep infinity ``` **Dockerfile** ```docker # syntax=docker/dockerfile:1.6 # By default, 1000:1000 but can be different like 1002:1002. # There parameters are initialized using the "make build" command. ARG OS_USERID=1000 ARG OS_GROUPID=1000 # Don't change, we'll reuse the standard user (the "node" user is # the one is node Docker image having user 1000:1000 ARG OS_USERNAME="node" # Root folder where Docusaurus will be installed in the Docker image ARG APP_HOME="/opt/docusaurus" # ───────────────────────────────────────────────────────────── # 🧱 Base Image: Devcontainer Node.js environment # ───────────────────────────────────────────────────────────── FROM mcr.microsoft.com/devcontainers/javascript-node:20-bookworm AS base # Install bash and bash-completion (required for Devcontainer shell features) RUN --mount=type=cache,target=/var/cache/apt \ --mount=type=cache,target=/var/lib/apt/lists \ set -eux; \ apt-get update && \ apt-get install -y --no-install-recommends \ bash \ bash-completion; \ rm -rf /var/lib/apt/lists/* # If the host userid/groupid is different from 1000:1000 then update the # existing node user to these IDs. This is not needed here for the production # image but well for devcontainer to remove permissions problems while synchronizing # with the host ARG OS_USERID ARG OS_GROUPID ARG OS_USERNAME RUN set -eux && \ if [ "$OS_USERID" != 1000 ] || [ "$OS_GROUPID" != "1000" ]; then \ groupmod -g "$OS_GROUPID" node; \ usermod -u "$OS_USERID" -g "$OS_GROUPID" "${OS_USERNAME}"; \ fi # Add the node user to the docker group (create it if needed) to allow, # in devcontainer, to run Docker-in-Docker actions if needed RUN set -eux && \ groupadd docker || true && \ usermod -aG docker "${OS_USERNAME}" ARG APP_HOME RUN set -eux && \ # Make sure the home folder exists mkdir -p "${APP_HOME}" && \ # Pre-create the .docusaurus folder to avoid permission issues during build # Note: this folder will be stored in memory (tmpfs) when running devcontainer mkdir -p "${APP_HOME}/.docusaurus" && \ chown -R "${OS_USERNAME}":"${OS_USERNAME}" "${APP_HOME}" # Switch to non-root user for all subsequent stages USER "${OS_USERNAME}" WORKDIR "${APP_HOME}" # ───────────────────────────────────────────────────────────── # 📦 Stage 1: Dependency Installation # ───────────────────────────────────────────────────────────── FROM base AS dependencies ARG APP_HOME ARG OS_USERNAME ARG OS_USERID ARG OS_GROUPID # Configure the cache folder for yarn so we can reuse it in our Devcontainer later on ENV YARN_CACHE_FOLDER=/home/${OS_USERNAME}/.cache/yarn/v6 RUN set -eux && \ mkdir -p "${YARN_CACHE_FOLDER}" && \ chown -R "${OS_USERNAME}":"${OS_USERNAME}" "${YARN_CACHE_FOLDER}" # Copy package manifests and lockfiles for dependency installation COPY --chown="${OS_USERNAME}":"${OS_USERNAME}" package.json package-*.* yarn*.* ./ USER "${OS_USERNAME}" # Install dependencies using Yarn with cache mount RUN --mount=type=cache,target=${YARN_CACHE_FOLDER},uid=${OS_USERID},gid=${OS_GROUPID} \ yarn install --frozen-lockfile --prefer-offline # ───────────────────────────────────────────────────────────── # 🧪 Stage 2: Development Environment Setup # # This is the target to use when building a Devcontainer # ───────────────────────────────────────────────────────────── FROM base AS development ARG APP_HOME # Copy full project source code and installed node_modules from dependencies stage COPY --chown="${OS_USERNAME}":"${OS_USERNAME}" . . COPY --chown="${OS_USERNAME}":"${OS_USERNAME}" --from=dependencies "${APP_HOME}"/node_modules ./node_modules # ───────────────────────────────────────────────────────────── # 🏗️ Stage 3: Static Site Build # # This stage is used to build the static site; when # "TARGET=production make build" is fired. # ───────────────────────────────────────────────────────────── FROM development AS build # Build the Docusaurus site into static HTML/CSS/JS RUN yarn build # ───────────────────────────────────────────────────────────── # 🚀 Stage 4: Production Image (Nginx) # # Minimal Nginx image for serving static files) # ───────────────────────────────────────────────────────────── FROM nginx:stable-alpine AS production # Clean default Nginx content RUN rm -rf /usr/share/nginx/html/* ARG APP_HOME RUN mkdir -p "${APP_HOME}" # Copy built static site from build stage into Nginx's web root COPY --from=build "${APP_HOME}"/build /usr/share/nginx/html # Add TLS certificates (for HTTPS support) RUN mkdir -p /etc/nginx/certs COPY localhost.pem /etc/nginx/certs/ COPY localhost-key.pem /etc/nginx/certs/ # Replace default Nginx config with custom one COPY nginx.conf /etc/nginx/conf.d/default.conf # Expose HTTP and HTTPS ports EXPOSE 80 443 # Start Nginx in foreground CMD ["nginx", "-g", "daemon off;"] ``` **localhost-key.pem** ```pem -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD1oKfgLKVSX43z IOLZ73MAoXISQ58x6xBpJdq84bX9MFTJTR2TI7oVUAxich9r7bithemycKyCbAGr 9qu3oSx8z0j3R/dCNhy/UCHpto5EVOoh98L924SReS+jWTSwBhLTb77602JaTcBC rwuiD+pzYM62+ip2TvCK+luUf6Jfi7XATw16HaEaWlsC9FOwG7tCSe4yxlj9GmY3 60+yyrjg2WEWsLsiDrW689gjIPlIkwWTL3RL9hBE0n+jLSCPqYOhzXyFxmNwHw1/ YkUt6OjQWKpEdFEim/k2fbkeS6t2q+mCzrc063Wqe2sFTiGvMqrfN2238ra3S+dI PRuZ39G9AgMBAAECggEAWjKOsxFx/Be+xqMy6pi1gGvIudA/K1OIiTQvaODxtXIE EI/S5m9eK+ljQttpknaPNJlSQgaCMrp6uq0wTZRQA90SjEvPDm9UiyRf77Tr9UfB Wovi/NmZitoXlkJxyJnSW/ZTCUSKf2LB0Tr6aJ6zl37bKfIfmaZPSpm4+4ULGU46 V1uFbbulG5Lm0ykcjwtaznkPnysxXEVnVO9TgGsbj+wSGcA8RxDj3kpQxnjqgt8J mDMD3yJZ/WstsJtsVcwkSBt4aoWLYGD5JzF7uNikR7/4gDqcAjrwO/+or2bGwdA8 OyvZNSDaXOhkALPV/9jxhT6HhPVK9f1/vYIBc2k5wQKBgQD94xnkifeOn8LhiKSh PL7Wt8Sd1cqAqAaSod22BBpu3ic4fFDE3zbGUKuULXEJUzzJr5m2m5mybBpY8ESR lQkBZAsL7WxIIrLVjedmRCFRxe/gfoQQd1VQfi+odEXTCul/ESOOAY/Ht29AuVCE m2IB2TDTq0pwukjqxxwK5ZUq+QKBgQD3q/U4f3c/e8L/otyf4EUGIsk+UdkuxxTE TqXWnb9UoxHJ6SF0yjldLkOYTle+Ioz4GCCUagYbQkBXQBOLf4wKoaszv8vCA0a3 SumeJizNu+x/erhcce4mnUl71eBAR0M2j59px9Wh2Lj4kOFaZ8xOPeoK9d0DuZDJ MzkztSKp5QKBgA18N47dCSopMDe8g0oRKO3MuwPLLENjKV42fMlDNIg5AopMiBm8 5XLpLAPl2gAtzKfFzPtXDnXcqhWZRXsaGG1be/65MKwwM6GGNQlIiyCFy/T3o1og jIaG/U8MRf0ssjROm73aNWHdS2p7rVwtyKnieGZ1zbFpI6BczQ+DHEY5AoGAe9hl x7mdZIY7bN8AWYWonjfYz6vX4Rc1oC//p0C8QtND+uXzj9XrN1gB/u/12iKmwQw3 hl857HmFeNuWqmO+IDbVSAw5FCvVOaRhC27lan709I3mEHv77R0reJvrBdlcl7Tb ruZ5QfQAEnNH7JAIznsRRCfLO6iX6LQPf3as/a0CgYBg2qKsj9I3NJYpNahC24wN lMLyamiXBNWyoZ5jMXHN3DTCYtFiEc5XepHTTKhvGA5GV/XR8/3oL+sXCi7u5PoB 9e4P2l7mZQ+p3naC52UVpbC02yO6kfdeKysLbu5yZaURVWYRDFl38SznG+opW+vG fwNFiSi7rFvEDd5mezJclw== -----END PRIVATE KEY----- ``` **localhost.pem** ```pem -----BEGIN CERTIFICATE----- MIIEKDCCApCgAwIBAgIRAKY08xOJWP5zy47GwVEvFOgwDQYJKoZIhvcNAQELBQAw cTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSMwIQYDVQQLDBpjaHJp c3RvcGhlQEFWT05UVVJFLVJBQ09VUjEqMCgGA1UEAwwhbWtjZXJ0IGNocmlzdG9w aGVAQVZPTlRVUkUtUkFDT1VSMB4XDTI0MDgxOTEyNTkzNloXDTI2MTExOTEzNTkz NlowTjEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMSMw IQYDVQQLDBpjaHJpc3RvcGhlQEFWT05UVVJFLVJBQ09VUjCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAPWgp+AspVJfjfMg4tnvcwChchJDnzHrEGkl2rzh tf0wVMlNHZMjuhVQDGJyH2vtuK2F6bJwrIJsAav2q7ehLHzPSPdH90I2HL9QIem2 jkRU6iH3wv3bhJF5L6NZNLAGEtNvvvrTYlpNwEKvC6IP6nNgzrb6KnZO8Ir6W5R/ ol+LtcBPDXodoRpaWwL0U7Abu0JJ7jLGWP0aZjfrT7LKuODZYRawuyIOtbrz2CMg +UiTBZMvdEv2EETSf6MtII+pg6HNfIXGY3AfDX9iRS3o6NBYqkR0USKb+TZ9uR5L q3ar6YLOtzTrdap7awVOIa8yqt83bbfytrdL50g9G5nf0b0CAwEAAaNeMFwwDgYD VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFPBV fPspKHOownZkdv5UZq4w9i8hMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG 9w0BAQsFAAOCAYEAPGdW3uHyzKDqBNOXSUNppPGRGGGB+Seai3km//szxUEwe4Nx jFNkOFDAos4zOD1hx977WaDkTN1XlUW0jmC+vi7mljKJYaNLr5YVbQyythabDG0o RsbkAafn04kcIYG6LmjB8uRegJNRcj3dco5odjt1KDQgIOmu4PwMHgdop5ptV4jS L5y2vj89/vdKu6eMroBWDecVcY4cbM1FUZIG/HtJhHwNfJq2NgSm25l13ipf6axf F7gJO2zCooSScLQfTbsKFm+4EmYLuEfJLPFUr3aRsZn4ZPdMkl+D6l7MT0qNhwer 51JB1BsJtEG9RbjC8Lbf34gwdJOkslZfL7YjPfDuqWhjOvvMJvPr0STzFEaPQRJj 0kIU3P9RvYb1xiooZVfpt+hGxqs2IR+YHvwQZW3epTy8LeB+CmxIqdxbYLLmVYXf 3VCbwGF6Qxv8uzmcB+q6fe24vY1U7cbMebXxOCZuxIr5xNXZ5f0NE87VfaKuE7UN Rj/0IWKjfyKtIOcd -----END CERTIFICATE----- ``` **makefile** ``` # cspell:ignore beautifulsoup4,repost,maintag,oyaml SHELL:=bash # Load .env file if there is one (if none, no error will be fired) -include .env DOCKER_UID:=$(shell id -u) DOCKER_GID:=$(shell id -g) # If not defined on the command line, default to development # Use "TARGET=production make build" f.i. to switch to PROD and then build the PROD image TARGET ?= development # region - Helpers COLOR_RED=31 COLOR_GREEN=32 COLOR_YELLOW=33 COLOR_WHITE=37 _RED:="\033[1;${COLOR_RED}m%s\033[0m %s\n" _GREEN:="\033[1;${COLOR_GREEN}m%s\033[0m %s\n" _YELLOW:="\033[1;${COLOR_YELLOW}m%s\033[0m %s\n" _WHITE:="\033[1;${COLOR_WHITE}m%s\033[0m %s\n" # endregion default: help .PHONY: help help: ## Show the help with the list of commands @clear @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[0;33m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) @echo "" ##@ Blog Blog helpers (!! For production, please always add "TARGET=production" before like "TARGET=production make build" !!) .PHONY: bash build down remove up bash: _bash-$(TARGET)  ## Open an interactive shell in the Docker container build: _build-$(TARGET) ## Build dev/prod image. You can pass CLI flags via ARGS like ARGS="--no-cache" down: _down-$(TARGET) ## Stop the container remove: _remove-$(TARGET) ## Remove the image up: _up-$(TARGET) ## Create the Docker container. .PHONY: _bash-development _bash-development: @printf $(_YELLOW) "Start an interactive shell in the Docker DEVELOPMENT container; type exit to quit" DOCKER_UID=${DOCKER_UID} DOCKER_GID=${DOCKER_GID} docker compose run --rm docusaurus /bin/bash .PHONY: _bash-production _bash-production: @printf $(_YELLOW) "Start an interactive shell in the Docker PROD container; type exit to quit" docker run -it my/blog:node /bin/sh # Build the DEV image i.e. using docker compose and with all our environment variables # Pre-create folders to avoid permission problems while building devcontainer .PHONY: _ensure-host-volumes _ensure-host-volumes: @for dir in .docusaurus node_modules; do \ if [ ! -d "$$dir" ]; then \ printf $(_YELLOW) "-> Creating $$dir directory on host..."; \ mkdir -p "$$dir"; \ fi; \ done .PHONY: _build-development _build-development: _ensure-host-volumes @printf $(_YELLOW) "-> Building DEVELOPMENT image using docker compose..." @printf $(_WHITE) "Use 'TARGET=PRODUCTION make build' for the PRODUCTION image." @echo "" DOCKER_UID=${DOCKER_UID} DOCKER_GID=${DOCKER_GID} docker compose build ${ARGS} @echo "" @printf $(_GREEN) "Development build finished. Now, continue by running \"make devcontainer\"." @echo "" # Build the PROD image i.e. a light nginx image .PHONY: _build-production _build-production: @printf $(_YELLOW) "-> Building PRODUCTION stand alone image..." @echo "" docker build --tag my/blog:latest --target production ${ARGS} . @echo "" @printf $(_GREEN) "Production build finished. You can now publish this image." .PHONY: devcontainer devcontainer: ## Open the blog in Visual Studio Code in devcontainer @printf $(_YELLOW) "Open the blog in Visual Studio Code in devcontainer" code . .PHONY: _down-development _down-development: docker run --rm -it --user root -v $${PWD}/:/project -w /project node /bin/bash -c "yarn run clear" DOCKER_UID=${DOCKER_UID} DOCKER_GID=${DOCKER_GID} docker compose down .PHONY: _down-production _down-production: @docker stop blog > /dev/null 2>&1 || true .PHONY: _remove-development _remove-development: #--volumes DOCKER_UID=${DOCKER_UID} DOCKER_GID=${DOCKER_GID} docker compose down --remove-orphans --rmi all #-docker image rmi blog-docusaurus # Remove the old build folder if present @rm -rf .docusaurus # Don't remove these files / folders so the next build won't download anymore dependencies # @rm -rf node_modules *.lock package-lock.json @echo "" .PHONY: _remove-production _remove-production: _down-production @docker rmi --force my/blog:latest .PHONY: up up: _up-$(TARGET) ## Create the Docker container. .PHONY: _up-development _up-development: @printf $(_YELLOW) "-> No need to create the container; just run 'make devcontainer' instead." @printf $(_WHITE) "Use 'TARGET=production make up' to start the PRODUCTION environment." @echo "" .PHONY: _up-production _up-production: @printf $(_YELLOW) "-> Create the PRODUCTION stand alone image..." @echo "" @-docker stop blog > /dev/null 2>&1 || true @-docker rm blog > /dev/null 2>&1 || true docker run -d --publish 443:443 --name blog my/blog:latest @echo "" @printf $(_YELLOW) "Open the PROD blog (https://localhost)" .PHONY: push push: ## Push the image on Docker hub (only when TARGET=production in .env) # Make sure you're already logged in on docker.com, if not run "docker login" and proceed to make an authentication. # docker build --tag my/blog:latest --target final ${ARGS} . docker image push my/blog:latest ``` **nginx.conf** ```conf # nginx.conf # 1. HTTP to HTTPS Redirection (Optional but recommended) server { listen 80; server_name localhost; # Redirect all HTTP traffic to HTTPS, preserving the path and query string return 301 https://$host$request_uri; } # 2. HTTPS Server Block (The main site configuration) server { listen 443 ssl http2; # Root directory where Docusaurus files are located root /usr/share/nginx/html; # Index file index index.html index.htm; server_name localhost; # SSL Configuration (using the copied certificates) ssl_certificate /etc/nginx/certs/localhost.pem; ssl_certificate_key /etc/nginx/certs/localhost-key.pem; # Basic SSL settings (Recommended) ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers HIGH:!aNULL:!MD5; # --- Docusaurus Home Page Redirection --- # When a user hits the root (https://localhost/), redirect them to /blog location = / { return 301 /blog; } # Docusaurus requires rewrites for client-side routing on all other paths location / { try_files $uri $uri/ /index.html; } # Cache control for static assets (optional, but good practice) location ~* \.(?:css|js|jpe?g|gif|png|ico|woff2?|ttf|eot|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; } } ``` ### Construire et lancer l'image de production {#build-and-run-the-production-image} Une fois les fichiers en place, lancez `make` dans une console et vous obtiendrez un écran comme celui-ci : ![Obtenir la liste des targets](./images/make_help.webp) Si `make` n'est pas encore installé sur votre machine, vous obtiendrez une erreur : installez donc d'abord `make` : ```bash $ sudo apt-get update && sudo apt-get install make ``` Nous devons maintenant lancer deux commandes. La première va créer une image Docker pour la production, c'est-à-dire où tout est inclus dans l'image. ```bash $ TARGET=production make build ``` Vous obtiendrez un écran comme celui-ci et il faudra patienter environ 80 secondes le temps que l'image soit construite. ![Lancement de make build pour la PROD](./images/make_build.webp) Cela fait, lancez la seconde commande pour créer une instance en cours d'exécution de l'image, c'est-à-dire un container : ```bash $ TARGET=production make up ``` Ce sera très rapide et vous verrez dans la console un message indiquant `Open the PROD blog (https://localhost)`. Et vous savez quoi ? C'est déjà fait : rendez-vous sur `https://localhost` et laissez-vous surprendre — le blog tourne, exactement le résultat annoncé en haut de cet article. > **Qu'avons-nous fait ?:** > > En lançant `TARGET=production make build`, nous avons créé une version autonome du blog tournant sur nginx. Nous avons encapsulé tous les articles du blog (des fichiers Markdown) et lancé toutes les commandes nécessaires pour générer la version statique du blog. Nous avons *garé* ces fichiers dans un emplacement temporaire. > > Nous avons créé l'image finale en utilisant nginx et en copiant dans l'image les fichiers statiques créés plus tôt. > > En lançant `TARGET=production make up`, nous avons créé une instance en cours d'exécution (appelée `container`) de cette image et demandé à notre système d'exploitation d'accéder au container sur le port `443` (celui du protocole `https`). C'est pourquoi, en accédant à `https://localhost`, nous obtenons le site. ## Utiliser les DevContainers pour le développement {#using-devcontainers-for-development} Construire l'image de production, c'est bien et utile si vous voulez la publier sur Docker Hub, mais il nous faut aussi un workflow d'édition confortable. Avec les DevContainers de VS Code, nous obtenons un environnement totalement isolé, taillé pour Docusaurus. Lancez d'abord `make build` (notez qu'ici, nous ne précisons pas `TARGET=production`). Ce sera extrêmement rapide puisque tout a déjà été créé avant. Ensuite, lancez `make devcontainer` pour ouvrir VSCode et... rien ne se passe pour l'instant : c'est normal, nous devons créer des fichiers supplémentaires. > **Les deux en une seule commande:** > > Sous Linux, vous pouvez lancer `make build && make devcontainer` pour exécuter les deux en une seule commande. ### Créer le dossier .devcontainer {#create-the-devcontainer-folder} Nous devons créer un nouveau dossier `.devcontainer` avec quelques fichiers : **Project: /tmp/docux-blog** **.devcontainer/.env** ``` OS_USERID=1000 OS_GROUPID=1000 ``` **.devcontainer/bootstrap.sh** ```bash #!/usr/bin/env bash set -euo pipefail # Load helper functions source "/opt/docusaurus/.devcontainer/bash_helpers.sh" # Run the Docusaurus watcher HTTPS=true \ SSL_CRT_FILE=localhost.pem \ SSL_KEY_FILE=localhost-key.pem \ yarn start --host 0.0.0.0 ``` **.devcontainer/bash\_helpers.sh** ```bash #!/bin/bash set -euo pipefail BASHRC="/home/${OS_USERNAME:-node}/.bashrc" MARKER="# DEV CONTAINER WELCOME MESSAGE" if ! grep -qxF "$MARKER" "$BASHRC"; then cat <> "$BASHRC" $MARKER BLOG_HOST_DIR='${BLOG_HOST_DIR:-}' alias ls='ls -alh' alias upgrade='yarn upgrade && yarn upgrade @docusaurus/core@latest @docusaurus/plugin-ideal-image@latest @docusaurus/plugin-sitemap@latest @docusaurus/preset-classic@latest @docusaurus/theme-search-algolia@latest @docusaurus/module-type-aliases@latest @docusaurus/types@latest' alias version='npx docusaurus -V' echo -e "🚀 Welcome to your Docusaurus Dev Container!" echo -e "" echo -e "📚 Quick Commands Reference:" echo -e "" echo -e " 📦 \e[33mversion\e[0m Show current Docusaurus version." echo -e " ⬆️ \e[33mupgrade\e[0m Upgrade Docusaurus core and plugins." echo -e "" EOF fi ``` **.devcontainer/compose.yaml** ```yaml # cspell:ignore erikvl87,langtool,ngrams services: docusaurus: build: context: . dockerfile: Dockerfile args: - APP_HOME=${APP_HOME:-/opt/docusaurus} - OS_USERID=${OS_USERID:-1000} - OS_GROUPID=${OS_GROUPID:-1000} user: ${OS_USERID:-1000}:${OS_GROUPID:-1000} env_file: - .env volumes: - ..:${APP_HOME:-/opt/docusaurus} - node_modules:/opt/docusaurus/node_modules command: sleep infinity languagetool: image: erikvl87/languagetool restart: always environment: - langtool_languageModel=/ngrams - Java_Xms=512m - Java_Xmx=1g volumes: - ngrams:/ngrams volumes: ngrams: node_modules: ``` **.devcontainer/devcontainer.json** ```json { "name": "MyBlog", "service": "docusaurus", "dockerComposeFile": ["../.devcontainer/compose.yaml"], "workspaceFolder": "/opt/docusaurus", "mounts": [ "source=../.devcontainer/history/.bash_history,target=/home/node/.bash_history,type=bind", "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind", "source=node_modules,target=/opt/docusaurus/node_modules,type=volume", "source=vscode-extensions,target=/home/node/.vscode-server/extensions,type=volume", "source=yarn-cache,target=/home/node/.cache/yarn/v6,type=volume" ], "portsAttributes": { "3000": { "onAutoForward": "openBrowser", "label": "Docusaurus Blog" }, "8010": { "label": "LanguageTool" } }, "forwardPorts": [3000, 8010], "containerEnv": { "OS_USERNAME": "node", "YARN_CACHE_FOLDER": "/home/node/.cache/yarn/v6" }, "initializeCommand": "mkdir -p .docusaurus .devcontainer/history && touch .devcontainer/history/.bash_history && echo '.bash_history' > .devcontainer/history/.gitignore", "postCreateCommand": "sudo chown -R node:node /opt/docusaurus/node_modules && /opt/docusaurus/.devcontainer/bootstrap.sh", "remoteUser": "node", "containerUser": "${OS_USERID}:${OS_GROUPID}", "customizations": { "vscode": { "settings": { "[markdown]": { "editor.codeActionsOnSave": { "source.fixAll": "always" }, "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", "editor.formatOnSave": true, "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "cspell.language": "en", "editor.bracketPairColorization.enabled": true, "editor.codeActionsOnSave": { "source.fixAll.markdownlint": "always" }, "editor.folding": true, "editor.foldingStrategy": "auto", "editor.formatOnSave": true, "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", "editor.guides.highlightActiveBracketPair": true, "editor.guides.indentation": true, "editor.renderWhitespace": "all", "editor.rulers": [120, 70], "editor.stickyScroll.enabled": true, "editor.tabCompletion": "on", "files.associations": { ".mdx": "Markdown" }, "files.autoSave": "onFocusChange", "files.eol": "\n", "files.defaultLanguage": "markdown", "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "languageToolLinter.languageTool.motherTongue": "en", "languageToolLinter.languageTool.language": "en", "languageToolLinter.external.url": "http://languagetool:8010", "markdownlint.configFile": ".config/.markdownlint.json", "markdownlint.fixOnSave": true, "sonarlint.disableTelemetry": true, "telemetry.telemetryLevel": "off", "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash", "icon": "terminal-bash" } }, "terminal.integrated.defaultProfile.linux": "bash", "window.title": "${dirty}${rootName} ${separator} [DevContainer]", "workbench.colorTheme": "Visual Studio Light", "workbench.startupEditor": "terminal" }, "extensions": [ "DavidAnson.vscode-markdownlint", "davidlday.languagetool-linter", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "GitHub.vscode-github-actions", "ms-vscode.makefile-tools", "shd101wyy.markdown-preview-enhanced", "streetsidesoftware.code-spell-checker", "TomasDahlqvist.markdown-admonitions", "usernamehw.errorlens", "yzhang.markdown-all-in-one" ] } } } ``` **.devcontainer/Dockerfile** ```docker # syntax=docker/dockerfile:1.6 # Use Dockerfile syntax version 1.6 for advanced features like cache mounts # https://docs.docker.com/build/dockerfile/frontend/ # This Dockerfile is designed for use in a Devcontainer setup with VSCode # Define build-time arguments for user and workspace configuration ARG OS_USERID=1002 ARG OS_GROUPID=1002 ARG OS_USERNAME="node" ARG APP_HOME="/opt/docusaurus" # Use a custom base image that includes Node.js and Docusaurus dependencies # This image is built from the `blog-docusaurus` Dockerfile file you'll # find in the root directory of this blog FROM blog-docusaurus:latest # Re-declare APP_HOME to ensure it's available in later stages ARG APP_HOME # Switch to root to install system packages and configure environment USER root # Install essential packages with caching to speed up rebuilds RUN --mount=type=cache,target=/var/lib/apt/lists \ --mount=type=cache,target=/var/cache/apt \ set -eux && \ apt-get update && \ apt-get install -y --no-install-recommends \ git \ bash \ bash-completion \ curl \ ca-certificates \ sudo && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ git config --global --add safe.directory "${APP_HOME}" # Avoid Git safety warnings # Re-declare user-related build arguments ARG OS_USERID ARG OS_GROUPID ARG OS_USERNAME # Grant passwordless sudo access to the node user # Allow our node user to run "sudo xxx" in case of very specific use case RUN set -eux && \ echo "${OS_USERNAME} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/"${OS_USERNAME}" && \ chmod 0440 /etc/sudoers.d/"${OS_USERNAME}" # Create and set permissions for VSCode server folders used for extensions and remote features RUN set -eux && \ mkdir -p "/home/${OS_USERNAME}/.vscode-server/bin" && \ mkdir -p "/home/${OS_USERNAME}/.vscode-server/extensions" && \ mkdir -p "/home/${OS_USERNAME}/.vscode-server/data" && \ chown -R "${OS_USERNAME}:${OS_USERNAME}" "/home/${OS_USERNAME}/.vscode-server" # ---------- Shell and history setup ---------- # Configure bash history behavior for the node user RUN echo "export HISTFILE=/home/${OS_USERNAME}/.bash_history" >> "/home/${OS_USERNAME}/.bashrc" && \ echo 'export HISTSIZE=10000' >> "/home/${OS_USERNAME}/.bashrc" && \ echo 'export HISTFILESIZE=10000' >> "/home/${OS_USERNAME}/.bashrc" && \ echo 'shopt -s histappend' >> "/home/${OS_USERNAME}/.bashrc" # Customize the shell prompt for clarity and style RUN set -eux && \ echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> "/home/${OS_USERNAME}/.bashrc" # ---------- Yarn setup and dependency caching ---------- # Define Yarn cache location to improve install performance ENV YARN_CACHE_FOLDER="/home/${OS_USERNAME}/.cache/yarn/v6" # Create the cache folder and assign correct ownership RUN set -eux && \ mkdir -p "${YARN_CACHE_FOLDER}" && \ chown -R "${OS_USERNAME}:${OS_USERNAME}" "${YARN_CACHE_FOLDER}" # Install project dependencies and the Sharp image plugin required by Docusaurus RUN --mount=type=cache,target=${YARN_CACHE_FOLDER},uid=${OS_USERID},gid=${OS_GROUPID} \ set -eux && \ yarn install --immutable --frozen-lockfile --prefer-offline && \ yarn add --platform=linux --arch=x64 sharp # Switch to the node user for development tasks USER "${OS_USERNAME}" # Set the working directory to the application root WORKDIR "${APP_HOME}" ``` > **Utilisez bien vos propres UID/GID:** > > Éditez le fichier `.devcontainer/.env` et vérifiez que ces valeurs correspondent bien aux vôtres : lancez `id -u` dans votre console et vérifiez que vous obtenez `1000`. Si ce n'est pas le cas, reportez la valeur obtenue (p.ex. `1002`) dans `OS_USERID` et faites de même avec la commande `id -g`, ici pour la variable `OS_GROUPID`. Comme nous avons deux scripts bash, nous devons les rendre exécutables. Lancez cette commande dans un terminal : ```bash $ chmod +x .devcontainer/bootstrap.sh .devcontainer/bash_helpers.sh ``` Il nous faut maintenant ouvrir le devcontainer, alors lancez : ```bash $ make build && make devcontainer ``` Une fois dans VSCode, appuyez sur `F1` pour ouvrir la **Command Palette** et lancez **Dev Containers: Rebuild Without Cache and Reopen in Container**. > **Vous ne l'avez pas ?:** > > Si vous n'avez pas cette commande, installez l'extension VSCode [Dev Container de Microsoft](https://marketplace.visualstudio.com/publishers/Microsoft). ![Devcontainer](./images/devcontainer_rebuild.webp) Cliquez donc pour lancer l'action **Dev Containers: Rebuild and Reopen in Container**. Si vous regardez le terminal (depuis VSCode), vous verrez beaucoup de messages comme ceux ci-dessous : ![Construction du devcontainer](./images/building_devcontainer.webp) À un moment donné, vous verrez ceci : ![Yarn run](./images/yarn_run.webp) Cela signifie que Docusaurus est prêt à servir votre contenu. Rendez-vous sur `https://localhost:3000` et tadaaa. > Screenshot — https\://localhost:3000 > > ![Le site tourne](./images/site_is_running.webp) Jouons un peu. Ouvrez le dossier `blog` et créez un nouveau fichier appelé `index.md` : ![Création d'un nouvel article](./images/new_blog_post.webp) Sauvegardez et, oh ?, votre navigateur reflète immédiatement le changement (le hot reload est bien activé) : > Screenshot — https\://localhost/blog/index/ > > ![Le nouvel article est déjà en ligne](./images/new_blog_post_running.webp) ### Utiliser le terminal depuis le devcontainer {#running-the-terminal-from-the-devcontainer} Un autre point vraiment sympa : l'utilisation du **Terminal**, toujours dans VSCode. En cas de besoin, appuyez sur `CTRL`+`ù` pour ouvrir le Terminal et repérez le bouton `+` en haut à droite de cette fenêtre. Cliquez sur le `+` pour en créer un nouveau : ![Utilisation du terminal dans le devcontainer](./images/devcontainer_terminal.webp) Vous avez vu ? Vous obtenez un écran d'accueil avec quelques actions à lancer. Par exemple, pour mettre à jour Docusaurus vers la dernière version, il suffit de lancer `upgrade` et c'est tout. Pourrait-on faire plus simple ? > **D'où vient cet écran d'accueil ?:** > > Voyez le fichier `.devcontainer/bash_helpers.sh` que vous avez copié/collé précédemment. > > Vous trouverez dans ce script Bash la mise en forme de l'écran d'accueil ainsi que quelques alias comme `version` pour obtenir la version de l'instance Docusaurus installée. ## Quels sont les avantages d'une telle approche ? {#what-are-the-advantages-of-such-approach} Ils sont si nombreux... Commençons par celui-ci : sur mon host, je n'ai que Docker et rien d'autre. Je n'ai pas besoin d'installer `Node` comme l'exige Docusaurus, ni aucune dépendance Linux. Je peux aussi construire mon blog comme une image autonome. Regardez mon image Docker `cavo789/blog` sur . Si vous voulez la faire tourner sur votre propre machine, il suffit de lancer `docker pull cavo789/blog && docker run -d -p 443:443 --name blog cavo789/blog` puis d'aller sur `https://localhost` et c'est tout. Avec un Devcontainer comme illustré dans cet article, vous pouvez aussi affiner votre environnement, c'est-à-dire indiquer quelle extension doit être installée dans VSCode et comment elle doit être configurée. Vous pouvez également définir le look\&feel de l'éditeur (la police, la taille, la couleur, le template, ...). Comme tout est isolé, vous pouvez *jeter* le container et le recréer sans douleur. Vous travaillez en équipe ? Le devcontainer est alors la voie à suivre puisque tout le monde aura exactement le même environnement. ## Utiliser LanguageTool dans VSCode {#using-languagetool-in-vscode} Voyons l'un des nombreux avantages : plutôt que de *simplement utiliser VSCode pour écrire nos articles*, utilisons LanguageTool qui fournit une vérification de base de la grammaire et de l'orthographe de vos articles. Et vous savez quoi ? Vous l'avez déjà. Regardez votre fichier `.devcontainer/compose.yaml`. Vous verrez un service appelé `languagetool` basé sur une image Docker appelée **erikvl87/languagetool**. Et maintenant, regardez votre fichier `.devcontainer/devcontainer.json`. Cherchez **languageToolLinter** et vous verrez une URL locale (basée sur une IP) ; c'est celle exposée par le service. Cherchez ensuite l'extension **davidlday.languagetool-linter**, c'est elle qui fait la magie. Voici un exemple de LanguageTool à l'œuvre : ![LanguageTool en action](./images/language_tool_in_action.webp) Bien sûr, la phrase correcte devrait être *This example illustrates LanguageTool in action*. ## Code Spell Checker {#code-spell-checker} Voici un autre exemple, une erreur qu'un francophone peut faire par inadvertance : ![Le mot example est mal orthographié](./images/example_is_misspelled.webp) Ici, l'extension utilisée est *Code Spell Checker* de **streetsidesoftware**. ## Markdown lint {#markdown-lint} Et peut-être que, pendant que vous écrivez votre contenu Markdown, VSCode vous affichera des erreurs en orange comme `MD047/single-training-newline` pour vous dire que vous avez oublié d'ajouter une ligne vide tout en bas du fichier. Ces avertissements viennent de Markdownlint, une autre extension déjà installée dans le devcontainer. ---