Tutoriels

Déployer une Application Node.js en Production

Mark Toledo

Mark Toledo

28 décembre 2024

Déployer une Application Node.js en Production

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 !