La configuration est l'un des leviers à plus fort impact dans l'exploitation de Node.js. De petites erreurs — comme laisser NODE_ENV non défini, mal saisir un délai d'attente ou mélanger les réglages de proxy de confiance — peuvent créer des problèmes de fiabilité, de sécurité ou de performance disproportionnés. Ce guide montre comment implémenter des changements sûrs et réversibles grâce au chargement centralisé de la configuration, à la validation stricte au démarrage, aux points de terminaison de santé observables et à des procédures concrètes de restauration. Vous obtiendrez des exemples, des commandes et des listes de contrôle qui s'alignent sur les API Express et les magasins de données typiques comme MongoDB et Redis.
Prérequis et hypothèses
Ce guide suppose la base suivante :
- Node.js 18+ pour fetch natif, AbortController et le lanceur de tests intégré
- Express 4.x comme cadre HTTP
- Proxies inverses courants : nginx, AWS ALB, Cloudflare ou terminateurs TLS similaires
- Cibles de déploiement conteneur (Docker/Kubernetes) ou bare-metal/systemd/PM2
- Gestion des secrets via AWS Secrets Manager, Doppler, dotenv-vault ou variables d'environnement injectées — jamais commises dans le contrôle de source
- Familiarité avec curl, jq et les scripts shell de base pour la vérification
Tous les exemples construits sont clairement identifiés. Remplacez les valeurs de remplacement (chaînes de connexion, ports, versions) par les détails de votre infrastructure réelle avant utilisation.
Vue d'ensemble de l'architecture
Le diagramme ASCII suivant illustre le flux de configuration et de requête. La frontière du proxy de confiance est la démarcation de sécurité critique : n'activez TRUST_PROXY que lorsque le proxy inverse est de confiance et supprime les en-têtes X-Forwarded-* entrants.
┌──────────────┐ ┌──────────────────┐ ┌─────────────────────────┐
│ Variables │────▶│ config.js │────▶│ server.js │
│ d'environnement│ │ (validation Zod)│ │ (Express + serveur HTTP)│
└──────────────┘ └──────────────────┘ └───────────┬─────────────┘
│
┌─────────────────────────────────┼─────────────────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ /healthz │ │ /configz │ │ /metrics │
│ (vivacité) │ │ (vue expurgée) │ │ (Prometheus) │
└─────────────┘ └─────────────────┘ └──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Dépendances externes │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ MongoDB │ │ Redis │ │ API HTTP │ │ Proxy inverse │ │
│ │ (mongoose) │ │ (ioredis) │ │ (fetch) │ │ (nginx/ALB) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
Schéma de configuration complet
Définissez toute la surface de configuration comme une interface TypeScript avec validation Zod. Cela sert à la fois de garde-fous à l'exécution et de documentation pour l'EDI.
// config.ts
import { z } from 'zod';
const SessionSecureSchema = z.union([
z.literal('auto'),
z.literal('true'),
z.literal('false'),
]);
const ConfigSchema = z.object({
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),
LOG_LEVEL: z.enum(['error', 'warn', 'info', 'debug', 'trace']).default('info'),
REQUEST_TIMEOUT_MS: z.coerce.number().int().min(100).max(120000).default(10000),
MONGODB_URI: z.string().url().startsWith('mongodb://').or(z.string().url().startsWith('mongodb+srv://')),
REDIS_URL: z.string().url().startsWith('redis://').or(z.string().url().startsWith('rediss://')),
DB_POOL_MIN: z.coerce.number().int().min(0).max(1000).default(2),
DB_POOL_MAX: z.coerce.number().int().min(1).max(1000).default(10),
KEEP_ALIVE: z.coerce.boolean().default(true),
TRUST_PROXY: z.coerce.boolean().default(false),
SESSION_SECURE: SessionSecureSchema.default('auto'),
CONFIG_VERSION: z.string().regex(/^\d{4}-\d{2}-\d{2}\.\d+$/).default('2026-08-08.1'),
TZ: z.string().default('UTC'),
});
export type Config = z.infer<typeof ConfigSchema>;
export function loadConfig(env: Record<string, string | undefined> = process.env): Config {
const parsed = ConfigSchema.safeParse(env);
if (!parsed.success) {
const issues = parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ');
throw new Error(`Échec de la validation de configuration : ${issues}`);
}
return parsed.data;
}
.env.example — commettez ce fichier (sans secrets) dans le contrôle de version :
# Requis : Application
PORT=3000
NODE_ENV=production
LOG_LEVEL=info
REQUEST_TIMEOUT_MS=10000
CONFIG_VERSION=2026-08-08.1
TZ=UTC
# Requis : Magasins de données (utilisez un gestionnaire de secrets en production)
MONGODB_URI=mongodb://user:pass@localhost:27017/app
REDIS_URL=redis://localhost:6379/0
# Pools de connexion
DB_POOL_MIN=2
DB_POOL_MAX=10
# Comportement HTTP
KEEP_ALIVE=true
TRUST_PROXY=false
SESSION_SECURE=auto
# Optionnel : Limite mémoire (ajustez selon la limite du conteneur)
# NODE_OPTIONS=--max-old-space-size=2048
Intégration des pools de base de données
Connectez les réglages de pool validés aux clients réels. Ce qui suit montre l'initialisation minimale pour Mongoose et ioredis en utilisant les valeurs de configuration.
// db.ts
import mongoose from 'mongoose';
import Redis from 'ioredis';
import { Config } from './config';
export function createMongoClient(cfg: Config) {
return mongoose.connect(cfg.MONGODB_URI, {
maxPoolSize: cfg.DB_POOL_MAX,
minPoolSize: cfg.DB_POOL_MIN,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
family: 4, // IPv4 en priorité
});
}
export function createRedisClient(cfg: Config) {
const client = new Redis(cfg.REDIS_URL, {
maxRetriesPerRequest: 3,
retryStrategy: (times) => {
if (times > 3) return null; // arrêter les tentatives
return Math.min(times * 200, 2000);
},
connectionName: `app-${process.pid}`,
lazyConnect: true,
});
client.on('error', (err) => console.error('Erreur de connexion Redis', { error: err.message }));
return client;
}
Conseil : Dimensionnez DB_POOL_MAX avec la formule (cœurs_cpu * 2) + nombre_effectif_broches. Pour un conteneur 4 cœurs avec stockage SSD, commencez à 10. Surveillez db.serverStatus().connections dans MongoDB et connected_clients dans Redis pour ajuster.
Sécurité des sessions et cookies
Implémentez la logique SESSION_SECURE='auto' : cookies sécurisés uniquement quand TRUST_PROXY=true et que la requête est arrivée en HTTPS (req.secure). Cela évite les échecs de cookies sécurisés quand le TLS se termine au proxy inverse.
// session.ts
import session from 'express-session';
import cookieParser from 'cookie-parser';
import { Request } from 'express';
import { Config } from './config';
export function createSessionMiddleware(cfg: Config) {
return [
cookieParser(),
session({
name: 'sid',
secret: process.env.SESSION_SECRET ?? 'dev-secret-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
secure: (req: Request) => {
if (cfg.SESSION_SECURE === 'true') return true;
if (cfg.SESSION_SECURE === 'false') return false;
// 'auto' : proxy de confiance + req.secure (défini par Express quand trust proxy est activé)
return cfg.TRUST_PROXY && req.secure;
},
maxAge: 24 * 60 * 60 * 1000, // 24 heures
},
}),
];
}
Sécurité : Générez SESSION_SECRET avec openssl rand -base64 32 et stockez-le dans votre gestionnaire de secrets. N'utilisez jamais la valeur par défaut en production.
Arrêt gracieux
Gérez SIGTERM pour cesser d'accepter de nouvelles connexions, vider les requêtes en cours, fermer les pools de base de données et quitter proprement. Alignez la période de grâce sur le terminationGracePeriodSeconds de votre orchestrateur (défaut Kubernetes 30s).
// shutdown.ts
import { Server } from 'http';
import mongoose from 'mongoose';
import Redis from 'ioredis';
import { Config } from './config';
export function setupGracefulShutdown(
server: Server,
mongo: typeof mongoose,
redis: Redis,
cfg: Config
) {
const GRACE_MS = 25000; // laisse 5s de marge avant SIGKILL K8s
let shuttingDown = false;
async function shutdown(signal: string) {
if (shuttingDown) return;
shuttingDown = true;
console.log(`Signal ${signal} reçu, début de l'arrêt gracieux`);
// 1. Cesser d'accepter de nouvelles connexions
server.close(() => console.log('Serveur HTTP fermé'));
// 2. Attendre les requêtes en cours (simplifié : timeout)
await new Promise(resolve => setTimeout(resolve, GRACE_MS));
// 3. Fermer les pools de BD
await Promise.allSettled([
mongo.connection.close(false).then(() => console.log('Pool MongoDB fermé')),
redis.quit().then(() => console.log('Connexion Redis fermée')),
]);
console.log('Arrêt gracieux terminé');
process.exit(0);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Windows : SIGTERM non émis sur `taskkill /PID`, utilisez équivalent SIGINT
if (process.platform === 'win32') {
process.on('message', (msg) => {
if (msg === 'shutdown') shutdown('windows-shutdown');
});
}
}
Crochet preStop Kubernetes (ajoutez à la spec du pod) :
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"] # chevauche GRACE_MS
Application du délai de requête
req.setTimeout ne définit que le délai d'inactivité du socket — il n'annule pas les gestionnaires de route. Utilisez AbortController (Node 18+) pour propager l'annulation à travers les middlewares asynchrones et les gestionnaires de route.
// timeout.ts
import { Request, Response, NextFunction } from 'express';
import { Config } from './config';
export function requestTimeoutMiddleware(cfg: Config) {
return (req: Request, res: Response, next: NextFunction) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), cfg.REQUEST_TIMEOUT_MS);
// Attacher le signal à la requête pour utilisation en aval
(req as any).abortSignal = controller.signal;
res.on('finish', () => clearTimeout(timeoutId));
res.on('close', () => clearTimeout(timeoutId));
controller.signal.addEventListener('abort', () => {
if (!res.headersSent) {
res.status(503).json({ error: 'Délai de requête dépassé', timeoutMs: cfg.REQUEST_TIMEOUT_MS });
}
});
next();
};
}
Utilisation dans les routes :
app.get('/slow', requestTimeoutMiddleware(cfg), async (req, res) => {
await doWork(req.abortSignal); // passer le signal aux opérations annulables
res.json({ ok: true });
});
Point de terminaison /configz avec expurgation des secrets
Exposez une vue de configuration à l'exécution qui expurge les secrets et inclut un hachage SHA-256 pour la détection de dérive.
// configz.ts
import { Request, Response } from 'express';
import { Config } from './config';
import { createHash } from 'crypto';
const SECRET_KEY_PATTERN = /(SECRET|KEY|TOKEN|PASSWORD|PASS)/i;
const URI_PASSWORD_PATTERN = /:\/\/([^:]+):([^@]+)@/;
function redactConfig(cfg: Config): Record<string, unknown> {
const clone = JSON.parse(JSON.stringify(cfg));
for (const key of Object.keys(clone)) {
if (SECRET_KEY_PATTERN.test(key)) {
clone[key] = '***EXPURGÉ***';
}
}
// Expurger les mots de passe dans les chaînes de connexion
if (clone.MONGODB_URI && typeof clone.MONGODB_URI === 'string') {
clone.MONGODB_URI = clone.MONGODB_URI.replace(URI_PASSWORD_PATTERN, '://$1:***@');
}
if (clone.REDIS_URL && typeof clone.REDIS_URL === 'string') {
clone.REDIS_URL = clone.REDIS_URL.replace(URI_PASSWORD_PATTERN, '://$1:***@');
}
return clone;
}
export function createConfigzEndpoint(cfg: Config) {
return (req: Request, res: Response) => {
const redacted = redactConfig(cfg);
const hash = createHash('sha256').update(JSON.stringify(redacted)).digest('hex').slice(0, 16);
res.json({ config: redacted, configHash: hash, timestamp: new Date().toISOString() });
};
}
Sortie /configz attendue (exemple construit) :
{
"config": {
"PORT": 3000,
"NODE_ENV": "production",
"LOG_LEVEL": "info",
"REQUEST_TIMEOUT_MS": 10000,
"MONGODB_URI": "mongodb://user:***@localhost:27017/app",
"REDIS_URL": "redis://:***@localhost:6379/0",
"DB_POOL_MIN": 2,
"DB_POOL_MAX": 10,
"KEEP_ALIVE": true,
"TRUST_PROXY": true,
"SESSION_SECURE": "auto",
"CONFIG_VERSION": "2026-08-08.1",
"TZ": "UTC"
},
"configHash": "a1b2c3d4e5f67890",
"timestamp": "2026-08-08T12:34:56.789Z"
}
Points d'observabilité
Intégrez la journalisation structurée avec Pino, la corrélation par identifiant de requête et un point de terminaison de métriques Prometheus.
// observability.ts
import pino from 'pino';
import { Request, Response, NextFunction } from 'express';
import { Config } from './config';
import client from 'prom-client';
export function createLogger(cfg: Config) {
return pino({
level: cfg.LOG_LEVEL,
base: { pid: process.pid, hostname: require('os').hostname() },
timestamp: () => `,"time":"${new Date().toISOString()}"`,
});
}
export function requestIdMiddleware(req: Request, res: Response, next: NextFunction) {
const id = req.headers['x-request-id'] as string || crypto.randomUUID();
req.id = id;
res.setHeader('X-Request-Id', id);
next();
}
export function createMetricsEndpoint() {
const register = new client.Registry();
client.collectDefaultMetrics({ register, prefix: 'nodejs_' });
const httpRequests = new client.Counter({
name: 'http_requests_total',
help: 'Total des requêtes HTTP',
labelNames: ['method', 'route', 'status'],
registers: [register],
});
return (req: Request, res: Response) => {
res.set('Content-Type', register.contentType);
res.send(register.metrics());
};
}
Utilisation dans server.ts :
app.use(requestIdMiddleware);
app.use((req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
logger.info({ reqId: req.id, method: req.method, url: req.url, status: res.statusCode, durationMs });
httpRequests.inc({ method: req.method, route: req.route?.path || req.path, status: res.statusCode });
});
next();
});
app.get('/metrics', createMetricsEndpoint());
Validation CI/CD
Workflow GitHub Actions qui valide le schéma de configuration, exécute les vérifications de type et teste /healthz en staging.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
- name: Validation configuration
env:
MONGODB_URI: mongodb://dummy:dummy@localhost:27017/test
REDIS_URL: redis://dummy@localhost:6379/0
run: node -e "require('./dist/config').loadConfig(process.env)"
deploy-staging:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Déploiement vers staging
run: ./deploy.sh staging
- name: Test de fumée /healthz
run: |
sleep 10
curl -sf https://staging.example.com/healthz | jq -e '.ok == true and .env == "production"'
Exemples Docker et gestionnaire de processus
Dockerfile (Multi-étapes, non-root, dumb-init)
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
RUN apk add --no-cache dumb-init
ENV NODE_ENV=production \
NODE_OPTIONS=--max-old-space-size=2048 \
TZ=UTC
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --from=builder --chown=node:node /app/package.json ./
USER node
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
Avertissement : Définissez --max-old-space-size à ≤ 75% de votre limite de mémoire conteneur (docs Node.js). Pour une limite de 2 GiB, utilisez 1536 ou 2048. L'exemple de 1024 Mo dans les brouillons antérieurs est dangereusement bas pour les charges de production.
docker-compose.yml (Pile de développement locale)
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports: ["3000:3000"]
environment:
- NODE_ENV=development
- MONGODB_URI=mongodb://mongo:27017/app
- REDIS_URL=redis://redis:6379/0
- TRUST_PROXY=true
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/healthz"]
interval: 10s
timeout: 5s
retries: 5
mongo:
image: mongo:6
ports: ["27017:27017"]
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports: ["6379:6379"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
ports: ["80:80", "443:443"]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on: [app]
nginx.conf (Proxy inverse avec en-têtes de proxy de confiance)
# nginx.conf
events { worker_connections 1024; }
http {
upstream app {
server app:3000;
keepalive 32;
}
server {
listen 80;
listen 443 ssl http2;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 10s; # correspond à REQUEST_TIMEOUT_MS
proxy_send_timeout 10s;
}
location /healthz {
proxy_pass http://app;
access_log off;
health_check interval=10s fails=3 passes=2 uri=/healthz;
}
}
}
Unité systemd (Production bare-metal)
# /etc/systemd/system/node-app.service
[Unit]
Description=Application Node.js
After=network.target mongod.service redis.service
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/nodeapp
EnvironmentFile=/opt/nodeapp/.env
ExecStart=/usr/bin/node --max-old-space-size=2048 dist/server.js
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodeapp
[Install]
WantedBy=multi-user.target
PM2 ecosystem.config.js
// ecosystem.config.js
module.exports = {
apps: [{
name: 'nodeapp',
script: 'dist/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
NODE_OPTIONS: '--max-old-space-size=2048',
},
env_file: '.env',
error_file: '/var/log/nodeapp/error.log',
out_file: '/var/log/nodeapp/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
kill_timeout: 25000,
wait_ready: true,
listen_timeout: 10000,
}],
};
Arbre de décision de dépannage
Suivez ce flux quand /healthz échoue ou se comporte de façon inattendue.
Échec de la vérification de santé ?
│
├─▶ Vérifier les logs pour erreurs de validation au démarrage
│ └─▶ Exécuter : journalctl -u nodeapp -n 100 --no-pager
│ └─▶ Chercher : "Échec de la validation de configuration", "Booléen invalide", "Entier invalide"
│
├─▶ Vérifier la liaison du PORT
│ ├─▶ Linux/macOS : lsof -iTCP:3000 -sTCP:LISTEN
│ ├─▶ Windows : Get-NetTCPConnection -LocalPort 3000 -State Listen
│ └─▶ Si EADDRINUSE : tuer le processus conflictuel ou changer PORT
│
├─▶ Vérifier la connectivité BD
│ ├─▶ MongoDB : mongosh "mongodb://user:pass@host:27017/app" --eval "db.runCommand({ping:1})"
│ ├─▶ Redis : redis-cli -u redis://host:6379 ping
│ └─▶ Vérifier les groupes de sécurité / règles pare-feu autorisent le trafic sortant
│
├─▶ Vérifier la connectivité proxy inverse → application
│ ├─▶ curl -v http://localhost:3000/healthz (depuis l'hôte proxy)
│ ├─▶ Vérifier TRUST_PROXY=true et en-têtes X-Forwarded-Proto=https arrivent
│ └─▶ Vérifier nginx error.log pour timeouts upstream
│
└─▶ Vérifier l'épuisement des ressources
├─▶ Mémoire : process.memoryUsage() dans les logs ou `docker stats`
├─▶ CPU : top -p $(pgrep -f node)
└─▶ Descripteurs de fichier : lsof -p $(pgrep -f node) | wc -l
Guide d'optimisation des performances
| Paramètre | Quand ajuster | Orientations |
|---|---|---|
| DB_POOL_MAX | Saturation BD, timeouts de connexion | Formule : (cœurs_cpu * 2) + nombre_effectif_broches. Pour 4 cœurs SSD : 10. Surveiller db.serverStatus().connections.current. |
| keepAliveTimeout / headersTimeout | Latence élevée, rotation connexions | Définir keepAliveTimeout = proxy_read_timeout + 1000-2000ms. headersTimeout = keepAliveTimeout + 1000ms. |
| --max-old-space-size | OOM kills, pauses GC élevées | ≤ 75% de la limite mémoire conteneur. Pour limite cgroup 2 GiB : 1536-2048. |
| REQUEST_TIMEOUT_MS | Pics de latence amont | Définir à latence p99 + 20% tampon. Ne jamais dépasser proxy_read_timeout du proxy inverse. |
| LOG_LEVEL | Coûts volume de logs | info pour production. debug seulement pendant investigation d'incident. |
Durcissement de sécurité
Superposez les défenses au niveau application et infrastructure.
// security.ts
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import cors from 'cors';
import { Config } from './config';
export function createSecurityMiddleware(cfg: Config) {
return [
helmet({
contentSecurityPolicy: false, // configurer par application
hsts: cfg.TRUST_PROXY, // laisser le proxy inverse gérer HSTS
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}),
cors({
origin: process.env.CORS_ORIGIN?.split(',') || false, // false = refléter l'origine de la requête
credentials: true,
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
}),
rateLimit({
windowMs: 60 * 1000,
max: 1000,
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.ip,
skip: (req) => req.path === '/healthz' || req.path === '/metrics',
}),
];
}
TLS pour MongoDB/Redis : Utilisez mongodb+srv:// (force TLS) et URLs rediss://. Vérifiez les certificats en production :
// Options TLS mongoose (quand n'utilisez pas mongodb+srv)
mongoose.connect(uri, {
tls: true,
tlsCAFile: '/etc/ssl/certs/ca-certificates.crt',
// tlsCertificateKeyFile: '/path/to/client.pem', // si TLS mutuel
});
Script de test de charge
Vérifiez l'application des timeouts, le comportement keep-alive et la dégradation gracieuse sous charge.
// load-test.js (exécuter avec : node load-test.js)
import autocannon from 'autocannon';
const BASE = process.env.TARGET_URL || 'http://localhost:3000';
async function run() {
// 1. Vérification de santé de base
console.log('=== Vérification de santé ===');
await autocannon({ url: `${BASE}/healthz`, connections: 10, duration: 10 });
// 2. Vérification timeout route lente (suppose /slow dort 15s, timeout=10s)
console.log('=== Vérification timeout (attendu 503) ===');
const timeoutResult = await autocannon({
url: `${BASE}/slow`,
connections: 20,
duration: 15,
timeout: 20000,
});
console.log('Non-2xx:', timeoutResult.non2xx);
// 3. Réutilisation connexions keep-alive
console.log('=== Réutilisation keep-alive ===');
await autocannon({
url: `${BASE}/healthz`,
connections: 50,
duration: 30,
pipelining: 10,
});
}
run().catch(console.error);
Attendu : /slow retourne 503 après ~10s (REQUEST_TIMEOUT_MS). Le nombre de connexions reste bas grâce à la réutilisation keep-alive.
Script de vérification de restauration
Automatisez la validation de restauration en comparant CONFIG_VERSION de /healthz aux étiquettes git.
#!/usr/bin/env bash
# rollback-verify.sh
set -euo pipefail
CURRENT_VERSION=$(curl -sf http://localhost:3000/healthz | jq -r .version)
echo "CONFIG_VERSION actuelle : $CURRENT_VERSION"
# Trouver l'étiquette git précédente correspondant au motif
PREV_TAG=$(git tag --list 'config-*' --sort=-v:refname | head -n 2 | tail -n 1)
if [ -z "$PREV_TAG" ]; then
echo "Aucune étiquette de configuration précédente trouvée"
exit 1
fi
echo "Étiquette de configuration précédente : $PREV_TAG"
# Extraire la version de l'étiquette (suppose format : config-YYYY-MM-DD.N)
PREV_VERSION=${PREV_TAG#config-}
echo "Restauration vers la version : $PREV_VERSION"
# Restaurer l'env depuis l'étiquette (suppose stockage .env par étiquette ou git show)
git show "$PREV_TAG:.env" > .env.rollback
source .env.rollback
export CONFIG_VERSION="$PREV_VERSION"
# Redémarrer (exemple systemd)
sudo systemctl restart nodeapp
# Vérifier
sleep 5
NEW_VERSION=$(curl -sf http://localhost:3000/healthz | jq -r .version)
if [ "$NEW_VERSION" == "$PREV_VERSION" ]; then
echo "✅ Restauration vérifiée : $NEW_VERSION"
exit 0
else
echo "❌ Restauration échouée : attendu $PREV_VERSION, obtenu $NEW_VERSION"
exit 1
fi
Liste de contrôle des opérations
Utilisez ceci comme manuel répétable pour chaque changement de configuration.
Avant le changement
- Capturer les versions Node, npm et dépendances (
node -v,npm ls --depth=0) - Confirmer la topologie : type de proxy inverse, point de terminaison TLS, réglage TRUST_PROXY actuel
- Définir une portée pilote étroite (instance unique, route canary ou drapeau de fonctionnalité)
- Spécifier la commande de vérification et la sortie attendue
Implémenter
- Centraliser la configuration dans config.ts avec validation de schéma Zod
- Définir des valeurs par défaut sûres : NODE_ENV=production, LOG_LEVEL=info, KEEP_ALIVE=true, TRUST_PROXY selon topologie
- Expurger les secrets dans les logs et /configz ; ne jamais journaliser les chaînes de connexion
- Étiqueter CONFIG_VERSION avec un libellé monotone (YYYY-MM-DD.N)
Vérifier
- Démarrer le service et confirmer l'écoute sur le PORT attendu (
lsof -iTCP:$PORT -sTCP:LISTEN) - Sonder /healthz et valider les champs env et CONFIG_VERSION
- Confirmer les timeouts : atteindre la route lente, attendre 503 dans REQUEST_TIMEOUT_MS
- Confirmer keep-alive :
curl -vmontre Connection: keep-alive - Confirmer la sémantique proxy : req.ip correspond à l'IP client quand TRUST_PROXY=true
- Vérifier les avertissements :
NODE_OPTIONS="--trace-warnings" node server.js
Exploiter
- Surveiller la latence (p50/p95/p99), le taux d'erreur et l'utilisation des ressources (CPU, RSS, heap)
- Surveiller les compteurs de connexions BD : db.serverStatus().connections (Mongo), connected_clients (Redis)
- Vérifier le volume de logs par rapport à la référence quand on change LOG_LEVEL
Restaurer
- Si une vérification échoue, restaurer le CONFIG_VERSION précédent et ses valeurs d'env
- Redémarrer le processus et reverifier /healthz affiche la version précédente
- Documenter la cause racine et ajouter une règle de validation pour prévenir la récurrence
Revoir (hebdomadaire ou par version)
- Réexécuter l'inventaire et différencier par rapport à la config déployée
- Élaguer les clés inutilisées et supprimer les drapeaux de fonctionnalité morts
- Rafraîchir les valeurs par défaut pour correspondre aux valeurs sûres observées
Conclusion
Les erreurs de configuration dans Node.js sont fréquentes mais hautement évitables. Centralisez et validez les entrées au démarrage avec un schéma comme Zod, étiquetez et observez votre configuration à l'exécution via /healthz et /configz, et gardez les changements petits et réversibles avec l'étiquetage CONFIG_VERSION et les drapeaux de fonctionnalité basés sur des commutateurs. Commencez par un pilote étroit : imposez NODE_ENV=production, validez les variables d'environnement numériques, activez le keep-alive HTTP avec des timeouts explicites dérivés de REQUEST_TIMEOUT_MS, et réglez TRUST_PROXY correctement quand un proxy inverse est en amont. Liez chaque changement à une étape de vérification claire — commandes curl, assertions de logs, vérifications de métriques — et gardez un chemin de restauration prêt avec vérification scriptée. Avec ces habitudes en place, vous livrerez des changements plus sûrs, diagnostiquerez les problèmes plus vite et réduirez le risque opérationnel de vos services Node.js.