Tutoriels

Déployer une Application Node.js en Production
Mark Toledo
28 décembre 2024

Déployer une application Node.js en production nécessite plus qu'un simple npm start. Ce guide couvre les meilleures pratiques pour un déploiement robuste, sécurisé et maintenable.
Préparation de l'application
Variables d'environnement
Ne jamais hardcoder les secrets. Utilisez des variables d'environnement :
// config.js
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE) || 10
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
}
};
# .env.example (à commiter)
PORT=3000
NODE_ENV=development
DATABASE_URL=
JWT_SECRET=
# .env (NE JAMAIS commiter)
PORT=3000
NODE_ENV=production
DATABASE_URL=mongodb://...
JWT_SECRET=votre-secret-super-securise
Scripts package.json
{
"scripts": {
"start": "node dist/app.js",
"dev": "nodemon src/app.js",
"build": "tsc",
"test": "jest",
"lint": "eslint src/",
"start:prod": "NODE_ENV=production node dist/app.js"
}
}
Health check endpoint
// routes/health.js
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage()
});
});
// Health check avancé avec dépendances
app.get('/health/ready', async (req, res) => {
try {
// Vérifier la base de données
await db.ping();
// Vérifier Redis
await redis.ping();
res.json({ status: 'ready' });
} catch (err) {
res.status(503).json({
status: 'not ready',
error: err.message
});
}
});
PM2 : Process Manager
PM2 maintient votre application en vie et gère le clustering.
Installation et usage
npm install -g pm2
# Démarrer l'application
pm2 start app.js --name "mon-api"
# Avec clustering (utilise tous les CPU)
pm2 start app.js -i max --name "mon-api"
# Commandes utiles
pm2 list # Liste des processus
pm2 logs # Voir les logs
pm2 monit # Monitoring temps réel
pm2 restart mon-api # Redémarrer
pm2 stop mon-api # Arrêter
pm2 delete mon-api # Supprimer
Fichier ecosystem
// ecosystem.config.js
module.exports = {
apps: [{
name: 'mon-api',
script: 'dist/app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
},
// Restart automatique si mémoire > 500MB
max_memory_restart: '500M',
// Logs
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
// Graceful restart
kill_timeout: 5000,
wait_ready: true,
listen_timeout: 10000
}]
};
pm2 start ecosystem.config.js --env production
pm2 save # Sauvegarder la configuration
pm2 startup # Démarrer au boot du système
Docker
Dockerfile optimisé
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Image de production légère
FROM node:20-alpine
WORKDIR /app
# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Copier les fichiers nécessaires
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
# Permissions
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -q --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/app.js"]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=mongodb://mongo:27017/mydb
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
mongo:
image: mongo:7
volumes:
- mongo_data:/data/db
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
mongo_data:
docker-compose up -d
docker-compose logs -f api
CI/CD avec GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t mon-api:${{ github.sha }} .
- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker tag mon-api:${{ github.sha }} ${{ secrets.DOCKER_REGISTRY }}/mon-api:latest
docker push ${{ secrets.DOCKER_REGISTRY }}/mon-api:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /app
docker-compose pull
docker-compose up -d
docker system prune -f
Nginx comme reverse proxy
# /etc/nginx/sites-available/mon-api
upstream nodejs {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
keepalive 64;
}
server {
listen 80;
server_name api.monsite.fr;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.monsite.fr;
ssl_certificate /etc/letsencrypt/live/api.monsite.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.monsite.fr/privkey.pem;
# Sécurité
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Compression
gzip on;
gzip_types application/json text/plain application/javascript;
location / {
proxy_pass http://nodejs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
}
Monitoring et logging
Winston pour les logs
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;
Sentry pour le tracking d'erreurs
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1 // 10% des requêtes
});
// Middleware Express
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());
Checklist de déploiement
- Variables d'environnement configurées
-
NODE_ENV=production - Logs structurés activés
- Health check endpoint disponible
- HTTPS configuré
- Headers de sécurité (Helmet)
- Rate limiting activé
- Compression activée
- Process manager (PM2) configuré
- Monitoring et alertes
- Backups de base de données
- CI/CD fonctionnel
Conclusion
Un déploiement en production réussi nécessite une approche méthodique. Avec PM2 ou Docker, un reverse proxy Nginx, du CI/CD et un bon monitoring, votre application Node.js sera prête pour la production. N'oubliez pas les mises à jour de sécurité régulières et les sauvegardes !
