Aller au contenu principal

Déploiement de la documentation

Ce guide couvre le déploiement du site de documentation La Cafête en utilisant un serveur Node.js minimal, sans dépendance à nginx ou Apache.

Architecture​

┌──────────────────────────────────────────────┐
│ Docker │
│ ┌──────────────────────────────────────────┐ │
│ │ node:22-alpine │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌──────────────┐ │ │
│ │ │ server.js │ │ build/ │ │ │
│ │ │ (Node.js http) │◄─┤ (static │ │ │
│ │ │ Port 8080 │ │ HTML) │ │ │
│ │ └──────────────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘

Le serveur server.js résout les problèmes de rafraîchissement de page :

  • Route /api/auth → sert build/api/auth/index.html
  • Route /guide/permissions → sert build/guide/permissions/index.html
  • Route inconnue → sert index.html (fallback, pas de 404)

Fichiers de déploiement​

server.js​

import { createServer } from 'node:http';
import { readFile, stat } from 'node:fs/promises';
import { createReadStream } from 'node:fs';
import { extname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = fileURLToPath(new URL('.', import.meta.url));

const PORT = parseInt(process.env.PORT || '8080', 10);
const BUILD_DIR = join(__dirname, 'build');

const MIME_TYPES = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.ico': 'image/x-icon',
'.xml': 'application/xml',
'.txt': 'text/plain; charset=utf-8',
'.webmanifest': 'application/manifest+json',
'.map': 'application/octet-stream',
};

async function tryFile(filePath) {
try {
const info = await stat(filePath);
// Only regular files are servable; directories resolve via index.html.
return info.isFile() ? filePath : null;
} catch {
return null;
}
}

function guessLocaleFromPath(pathname) {
const segments = pathname.split('/').filter(Boolean);
if (segments.length > 0 && segments[0] === 'en') return 'en';
return 'fr';
}

async function resolvePath(pathname) {
if (pathname === '/') {
return join(BUILD_DIR, 'index.html');
}

const decoded = decodeURIComponent(pathname);
const cleanPath = decoded.endsWith('/') ? decoded.slice(0, -1) : decoded;

for (const candidate of [
join(BUILD_DIR, cleanPath),
join(BUILD_DIR, cleanPath + '.html'),
join(BUILD_DIR, cleanPath, 'index.html'),
]) {
const found = await tryFile(candidate);
if (found) return found;
}

return null;
}

function sendFile(res, filePath, status = 200) {
const ext = extname(filePath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';

const cacheable = ['.css', '.js', '.png', '.jpg', '.svg', '.ico', '.webmanifest', '.map'];
const isCacheable = cacheable.includes(ext);

res.writeHead(status, {
'Content-Type': contentType,
'Cache-Control': isCacheable
? 'public, max-age=31536000, immutable'
: 'no-cache',
});

createReadStream(filePath).pipe(res);
}

async function sendFallback(res, locale) {
const fallback = join(
BUILD_DIR,
locale === 'en' ? 'en' : '',
'index.html'
);
const found = await tryFile(fallback);
if (found) {
sendFile(res, found);
} else {
sendFile(res, join(BUILD_DIR, 'index.html'));
}
}

const server = createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${req.headers.host}`);
let { pathname } = url;

if (pathname !== '/' && pathname.endsWith('/')) {
pathname = pathname.slice(0, -1);
}

const filePath = await resolvePath(pathname);

if (filePath) {
sendFile(res, filePath);
} else {
const locale = guessLocaleFromPath(pathname);
await sendFallback(res, locale);
}
} catch (err) {
console.error('Request error:', err);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
}
});

server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});

Dockerfile​

FROM node:22-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:22-alpine

RUN apk add --no-cache tini

WORKDIR /app
COPY --from=builder /app/build ./build
COPY server.js ./

ENV PORT=8080
EXPOSE 8080

USER node
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

docker-compose.yml​

services:
docs:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- PORT=8080
restart: unless-stopped

Déploiement​

En local​

cd docs

# Construction
docker compose build

# Lancement
docker compose up -d

# Vérification
curl http://localhost:8080

En production​

# Construction avec tag
docker build -t caffette-docs:latest .

# Lancement sur le port souhaité
docker run -d \
--name caffette-docs \
-p 80:8080 \
--restart unless-stopped \
caffette-docs:latest

Avec un reverse proxy (Caddy)​

docs.caffette.app {
reverse_proxy localhost:8080
}

Variantes de déploiement​

OptionCommandeUsage
Docker Composedocker compose up -dDéveloppement / petite production
Docker rundocker run -p 80:8080 caffette-docsProduction simple
Node.js directnpm run build && node server.jsSans Docker
Docusaurus servenpm run serveDéveloppement uniquement
info

npm run serve (Docusaurus) n'est pas recommandé en production — il charge le bundler webpack et consomme plus de mémoire que nécessaire. server.js sert les fichiers statiques directement sans surcharge.

Dépannage​

ProblèmeCauseSolution
Page blanche au refreshPas de fallback SPAVérifier que server.js est bien utilisé (pas npx serve)
404 sur les assets versionnésCache staleRedémarrer le container : docker compose restart
Les liens anglais renvoient du françaisDétection de locale incorrecteVérifier que le chemin commence par /en/