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→ sertbuild/api/auth/index.html - Route
/guide/permissions→ sertbuild/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
| Option | Commande | Usage |
|---|---|---|
| Docker Compose | docker compose up -d | Développement / petite production |
| Docker run | docker run -p 80:8080 caffette-docs | Production simple |
| Node.js direct | npm run build && node server.js | Sans Docker |
| Docusaurus serve | npm run serve | Dé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ème | Cause | Solution |
|---|---|---|
| Page blanche au refresh | Pas de fallback SPA | Vérifier que server.js est bien utilisé (pas npx serve) |
| 404 sur les assets versionnés | Cache stale | Redémarrer le container : docker compose restart |
| Les liens anglais renvoient du français | Détection de locale incorrecte | Vérifier que le chemin commence par /en/ |