
Mon premier déploiement en production a cassé l'app pendant 3 heures.
J'avais tout testé en local. Le code marchait. Les tests passaient. J'ai cliqué sur déployer, je suis allé chercher un café, et je suis revenu sur un channel Slack en feu.
La chaîne de connexion à la base de données était fausse. Une variable d'environnement. Trois heures de downtime.
C'était la première de nombreuses leçons. Voici ce que j'aurais aimé qu'on me dise.
Leçon 1 : les variables d'environnement vont te trahir
Les variables d'environnement sont la cause #1 de "ça marche sur ma machine."
J'ai vu :
- Des typos dans les noms de variables (
DATABSE_URLau lieu deDATABASE_URL) - Des variables manquantes en prod qui existaient en dev
- Des secrets accidentellement commités sur git
- Des valeurs différentes entre staging et production
La solution ? Valide au démarrage.
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
});
// Ça s'exécute quand ton app démarre
// Si une variable est manquante ou invalide, l'app crash immédiatement
// Mieux vaut crasher au démarrage qu'en production à 3h du mat
export const env = envSchema.parse(process.env);
Échoue vite. Si une variable est fausse, crash immédiatement. N'attends pas qu'un utilisateur tombe sur la feature cassée.
Leçon 2 : le staging doit correspondre à la production
"Ça marche en staging" veut rien dire si staging ne correspond pas à la production.
J'ai debuggé des problèmes causés par :
- Différentes versions de Node.js
- Différentes versions de base de données (PostgreSQL 14 vs 15)
- Différents OS (Ubuntu vs Alpine)
- Différentes limites de mémoire
- Différentes variables d'environnement
La règle : staging devrait être un clone plus petit de la prod, pas un setup différent.
# docker-compose.prod.yml
services:
app:
image: node:20-alpine # Pareil qu'en prod
environment:
- NODE_ENV=production # Pareil qu'en prod
deploy:
resources:
limits:
memory: 512M # Mêmes limites qu'en prod
Si tu peux pas te permettre une infrastructure identique, au moins matche :
- Les versions runtime (Node, Python, etc.)
- Les versions de base de données
- L'image OS de base
- Les variables d'environnement principales
Leçon 3 : ne déploie jamais le vendredi
J'ai appris ça à mes dépens.
Déploiement vendredi 17h. Bug qui apparaît samedi matin. Personne autour. Les clients sont en colère. Je passe mon weekend à réparer au lieu de me reposer.
Maintenant mes règles :
- Pas de déploiement après jeudi 16h
- Pas de déploiement avant un jour férié
- Pas de "quick fix" le vendredi
Si c'est assez urgent pour déployer le vendredi, c'est assez urgent pour avoir l'équipe en standby. Si l'équipe peut pas être en standby, ça peut attendre lundi.
Leçon 4 : chaque déploiement a besoin d'un plan de rollback
"On corrigera en avançant" c'est pas un plan.
Avant chaque déploiement, je demande :
- Comment je sais si ce déploiement a échoué ?
- Comment je rollback vers la version précédente ?
- Combien de temps le rollback prendra ?
Ma méthode : GitHub Releases + Actions.
Chaque release est un tag sur GitHub. Pas besoin de ligne de commande, tu peux tout faire depuis l'interface :
- Va dans Releases → Create new release
- Clique sur Choose a tag → crée un nouveau tag (ex:
v1.2.3) - Ajoute un titre et des notes de release (optionnel mais utile)
- Clique sur Publish release
Le workflow se déclenche automatiquement :
# .github/workflows/deploy.yml
name: Deploy on Release
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy
run: npm run deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Quelque chose a cassé ? Rollback en 3 clics :
- Va dans l'onglet Actions
- Trouve le déploiement de la version précédente qui marchait (ex:
v1.2.2) - Clique sur Re-run all jobs
C'est tout. Pas de commandes git à retenir. Pas de stress à 3h du mat.
Pour les migrations de base de données, c'est plus délicat. Ma règle : les migrations doivent être réversibles ou additives.
-- Dangereux : pas de rollback possible
ALTER TABLE users DROP COLUMN old_field;
-- Safe : ajoute d'abord, supprime après
ALTER TABLE users ADD COLUMN new_field VARCHAR(255);
-- Déploie le nouveau code
-- Vérifie que tout marche
-- Plus tard, dans une autre release : DROP COLUMN old_field
Leçon 5 : les logs sont ton meilleur ami (quand ils existent)
La première fois qu'un bug en prod est arrivé, j'avais aucune idée de ce qui s'était passé. Pas de logs. Pas de traces. Juste une page d'erreur vide.
Maintenant je log tout ce qui compte :
// Avant : pas de contexte
console.log('Error');
// Après : information actionnable
logger.error('Payment failed', {
userId: user.id,
amount: payment.amount,
errorCode: error.code,
errorMessage: error.message,
timestamp: new Date().toISOString(),
});
Quoi logger :
- Chaque appel API externe (requête + réponse + durée)
- Chaque requête base de données qui échoue
- Chaque tentative d'authentification
- Chaque transaction de paiement
- Les actions utilisateur qui modifient des données
Quoi NE PAS logger :
- Les mots de passe
- Les numéros de carte bancaire
- Les données personnelles (RGPD)
- Les corps de requête complets avec des infos sensibles
Leçon 6 : les health checks préviennent les désastres
Mon app a crashé silencieusement une fois. Le processus tournait, mais il ne répondait pas aux requêtes. Le load balancer continuait d'envoyer du trafic vers une instance morte.
Les health checks règlent ça :
// pages/api/health.ts (Next.js)
export default async function handler(req, res) {
try {
// Vérifie la connexion à la base de données
await db.query('SELECT 1');
// Vérifie les services externes si critiques
// await redis.ping();
res.status(200).json({ status: 'healthy' });
} catch (error) {
res.status(500).json({ status: 'unhealthy', error: error.message });
}
}
Ton load balancer/orchestrateur appelle cet endpoint toutes les 30 secondes. S'il échoue, le trafic est routé ailleurs.
Leçon 7 : les secrets appartiennent à un gestionnaire de secrets
J'ai commité des secrets sur git. Plus d'une fois.
Même si tu les supprimes, ils sont dans l'historique git. Des bots scannent GitHub pour les credentials exposés. Ils trouveront les tiens.
Les règles :
- Ajoute
.envà.gitignoredès le jour 1 - Utilise
.env.exampleavec des valeurs placeholder - Utilise un gestionnaire de secrets pour la prod (Vercel env vars, AWS Secrets Manager, Doppler)
- Fais tourner les credentials immédiatement s'ils sont exposés
# .gitignore
.env
.env.local
.env.production
# .env.example (commite ça)
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
STRIPE_SECRET_KEY=sk_test_xxx
Si tu commites accidentellement un secret :
- Fais tourner le credential immédiatement
- Supprime de l'historique git avec
git filter-branchou BFG - Force push (coordonne avec ton équipe)
Leçon 8 : le monitoring n'est pas optionnel
"L'app est lente" c'est pas actionnable. "Le temps de réponse moyen est passé de 200ms à 2s à 14h32" ça l'est.
Monitoring minimum :
- Uptime : Est-ce que l'app répond ?
- Temps de réponse : À quelle vitesse ?
- Taux d'erreur : Combien de 500 ?
- Base de données : Pool de connexions, temps de requête
- Mémoire/CPU : Est-ce qu'on manque de ressources ?
Options gratuites/pas chères qui marchent :
- Vercel Analytics (si sur Vercel)
- Sentry (erreurs + performance)
- Better Stack / Uptime Robot (uptime)
- PlanetScale insights (base de données)
Configure des alertes. Si le taux d'erreur spike, tu devrais le savoir avant que tes utilisateurs te le disent.
Leçon 9 : le CI/CD vaut le temps de setup
Pendant des mois, je déployais manuellement :
- Lance les tests en local
- Build en local
- SSH sur le serveur
- Pull le code
- Redémarre l'app
Chaque déploiement prenait 15 minutes de travail manuel. Et je sautais des étapes quand j'étais pressé.
Maintenant tout est automatisé :
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linter
run: npm run lint
- name: Build
run: npm run build
- name: Deploy
run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
Push sur main = déploiement automatique. Les tests échouent = déploiement bloqué. Pas d'étapes manuelles = pas d'erreur humaine.
Leçon 10 : les backups sont inutiles tant que tu les testes pas
"On a des backups" veut rien dire si t'as jamais essayé d'en restaurer un.
Questions à répondre :
- C'était quand le dernier backup ?
- Combien de temps prend une restauration ?
- T'as vraiment essayé de restaurer ?
- Tu backup aussi les variables d'environnement et les secrets ?
Je planifie un "exercice de reprise après sinistre" tous les trimestres :
- Lance un nouvel environnement
- Restaure depuis le backup
- Vérifie que l'app marche
- Documente les problèmes
Le pire moment pour apprendre que tes backups marchent pas, c'est pendant un vrai désastre.
La cheat sheet
| Leçon | Action |
|---|---|
| Valide les env vars | Utilise Zod, crash au démarrage si invalide |
| Matche staging à la prod | Mêmes versions, même OS, mêmes limites |
| Pas de déploiement le vendredi | Urgences seulement, avec l'équipe en standby |
| Planifie les rollbacks | Tag les releases, migrations réversibles |
| Log tout ce qui est utile | Du contexte, pas juste "error" |
| Ajoute des health checks | Laisse l'infra détecter les échecs |
| Utilise des gestionnaires de secrets | Ne commite jamais .env sur git |
| Monitore proactivement | Alertes avant que les utilisateurs se plaignent |
| Automatise le CI/CD | Pas d'étapes de déploiement manuelles |
| Teste tes backups | Exercices de restauration trimestriels |
La leçon
Le DevOps c'est pas des outils fancy. C'est ne pas se faire réveiller à 3h du mat.
Chaque leçon ici vient de la douleur. Du downtime. Des utilisateurs en colère. Des sessions de debug le weekend. Des conversations inconfortables avec les managers.
L'objectif est simple : déploie avec confiance, dors paisiblement.
C'est le dernier article de ma série "Ce que j'ai appris à mes dépens". Merci d'avoir lu les 6 parties.
Des questions ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.