<!--
  canonical-url: https://www.avonture.be/fr/blog/running-docusaurus-with-docker
  published:     2025-11-11
  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.
-->

# 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<target>\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 <<EOF >> "$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 <https://hub.docker.com/r/cavo789/blog>. 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.
