Migrer votre base de données en production sans downtime : la manœuvre technique que vous improvisez au pire moment

Une migration de schéma mal jouée peut couper votre produit en pleine heure de pointe. Pourquoi le pattern expand/contract change tout, et comment le mettre en place sans équipe DBA.

Published: September 2, 2026

Un vendredi soir, un développeur pousse une migration qui renomme une colonne email en email_address. La migration passe en trois secondes. Le déploiement aussi. Et pourtant, pendant les quatre minutes qui séparent l’application du schéma de la fin du rollout, chaque requête de l’ancienne version du code cherche une colonne qui n’existe plus. Les inscriptions échouent, le support s’affole, et personne ne comprend pourquoi « une simple migration » a mis le produit à genoux.

Ce scénario n’a rien d’exotique. C’est l’un des incidents de production les plus fréquents en early-stage, et l’un des plus mal anticipés. Parce que tant que votre base fait quelques milliers de lignes et que vous êtes trois utilisateurs à la tester, une migration destructive passe inaperçue. Le jour où vous avez du trafic réel, des données réelles et des clients qui paient, la même opération devient une manœuvre à haut risque — que vous improvisez au pire moment, sans process et sans filet.

Le vrai problème n’est pas la migration, c’est la fenêtre d’incohérence

La plupart des fondateurs pensent qu’une migration est risquée parce qu’elle « touche la base ». C’est une intuition à moitié fausse. Le danger réel se cache dans un intervalle de temps que presque personne ne modélise : le moment où votre schéma de base et le code de votre application ne sont plus d’accord sur la réalité.

Un déploiement moderne n’est jamais atomique. Pendant un rollout, plusieurs instances de votre application tournent en parallèle : certaines exécutent l’ancien code, d’autres le nouveau. Si votre migration change le schéma de manière incompatible avec l’ancien code — supprimer une colonne, la renommer, la rendre NOT NULL — vous créez une fenêtre où la moitié de vos serveurs plantent sur chaque requête. Cette fenêtre dure quelques secondes à quelques minutes. Suffisant pour perdre des transactions, corrompre des données, et déclencher un incident.

Le piège, c’est que cette fenêtre est invisible en développement. En local, vous avez une seule instance, vous migrez et vous relancez : jamais de désaccord entre schéma et code. C’est précisément pour ça que le problème n’explose qu’en production, sous charge, quand le coût est maximal.

Les opérations qui vous piègent (et celles qui sont sûres)

Toutes les migrations ne se valent pas. Certaines sont additives et réversibles ; d’autres sont destructives et bloquantes. Savoir les distinguer est la moitié du travail.

Ajouter une nouvelle colonne nullable, créer une nouvelle table, ajouter un index (avec la bonne option de votre moteur) : ces opérations sont compatibles avec l’ancien code, qui les ignore simplement. Elles sont sûres, tant que vous ne les rendez pas obligatoires dans le même déploiement.

À l’inverse, renommer une colonne, en supprimer une encore utilisée, ajouter une contrainte NOT NULL sur une colonne existante, ou changer un type de données : ce sont des opérations qui cassent l’ancien code ou verrouillent la table. Sur PostgreSQL, ajouter une colonne avec une valeur par défaut sur une grosse table peut poser un ACCESS EXCLUSIVE lock et geler toutes les écritures pendant que la table est réécrite. Sur une table de plusieurs millions de lignes, cela signifie plusieurs minutes de produit figé — un downtime que vous n’aviez pas prévu et que vous découvrez en le vivant.

La règle mentale à retenir : une migration ne doit jamais casser la version du code actuellement en production. Si vous ne pouvez pas déployer votre migration sans déployer votre nouveau code en même temps, vous avez déjà un problème de conception.

Expand / contract : la seule méthode qui tient la route

La solution n’est pas un outil, c’est un pattern. On l’appelle expand/contract (ou parallel change), et il consiste à décomposer toute migration risquée en plusieurs étapes réversibles, déployées séparément.

Reprenons le renommage de email en email_address, en apparence trivial. En expand/contract, il devient une séquence en trois temps.

D’abord, expand : vous ajoutez la nouvelle colonne email_address sans toucher à l’ancienne. Vous déployez un code qui écrit dans les deux colonnes à la fois, mais lit encore l’ancienne. À ce stade, rien ne casse : l’ancien schéma existe toujours, l’ancien code fonctionne, le nouveau aussi.

Ensuite, migrate : vous copiez les données de email vers email_address par un script de fond, en petits lots, sans bloquer la table. Une fois la copie terminée et vérifiée, vous déployez un code qui lit désormais email_address. Vos deux colonnes sont synchronisées, la bascule de lecture est transparente.

Enfin, contract : une fois que vous êtes certain que plus aucune version du code ne référence l’ancienne colonne — souvent après quelques jours d’observation — vous supprimez email. C’est la seule étape destructive, et elle intervient quand plus rien ne dépend de la colonne.

Trois déploiements au lieu d’un. Plus lent, oui. Mais chaque étape est individuellement réversible, et à aucun moment votre production n’est dans un état incohérent. C’est exactement le genre de discipline qui sépare une équipe qui déploie sereinement d’une équipe qui prie à chaque git push.

Ce que ça change côté business (et pourquoi ça vous concerne, fondateur non-tech)

Il est tentant de ranger ce sujet dans la case « détail d’ingénierie ». Ce serait une erreur de pilotage. Votre capacité à faire évoluer votre schéma de données sans downtime détermine directement votre vélocité produit et votre exposition au risque commercial.

Une équipe qui ne maîtrise pas les migrations sûres finit par avoir peur de sa propre base. Elle repousse les évolutions de modèle de données, empile des colonnes _v2, _temp, _new, et accumule une dette qui ralentit chaque nouvelle feature. À l’inverse, une équipe qui applique expand/contract par défaut peut faire évoluer son modèle en continu, sans jamais négocier une « fenêtre de maintenance » avec ses clients — ce qui, en B2B SaaS avec des SLA à la clé, n’est pas un luxe mais une condition de vente.

Et le jour d’une due diligence technique en levée, la manière dont vous gérez vos migrations est un signal lisible. Un investisseur ou son CTO advisor regardera si vos déploiements sont réversibles, si vous avez des incidents de migration dans votre historique, si vous savez faire évoluer votre schéma sous charge. Une prod qui tombe à chaque changement de colonne raconte une histoire — celle d’une équipe qui n’a pas industrialisé sa livraison.

Ce qu’il faut vraiment faire, selon votre stade

Au stade pre-seed, ne sur-ingénierez pas. Vous n’avez probablement pas encore de trafic qui rende une fenêtre d’incohérence dangereuse. La seule règle non négociable : ne poussez jamais une migration destructive et un déploiement de code dans le même temps sans y réfléchir. Prenez l’habitude d’ajouter avant de supprimer. Cette hygiène ne coûte rien et vous évite l’incident bête qui vous fait perdre une journée.

Au stade seed, vous avez du trafic réel et probablement vos premiers clients payants. C’est le moment d’adopter expand/contract comme pattern par défaut pour toute opération destructive, et de mettre en place les fondamentaux : des migrations versionnées et rejouables (via l’outil de migration natif de votre framework — Prisma, Alembic, Flyway, ActiveRecord, peu importe), une revue systématique de chaque migration en pull request, et une règle simple documentée dans votre CONTRIBUTING : « toute suppression de colonne se fait en deux déploiements ». Vous n’avez pas besoin d’un DBA. Vous avez besoin d’une checklist et d’un peu de discipline.

Au stade série A, avec une équipe qui grossit et plusieurs déploiements par jour, la discipline manuelle ne suffit plus. Automatisez la détection des migrations dangereuses dans votre CI (des outils comme squawk sur PostgreSQL ou les linters de migration intégrés savent bloquer un DROP COLUMN ou un lock exclusif avant qu’il n’atteigne la prod). Documentez le pattern expand/contract dans votre onboarding technique. À ce stade, la migration sûre n’est plus une compétence individuelle : c’est une propriété de votre système de livraison.

En résumé

Une migration de schéma n’est pas une opération technique isolée. C’est un moment où votre code et vos données doivent rester d’accord pendant que tout bouge, sous charge, avec de vrais clients. Le pattern expand/contract ne rend pas la migration plus facile : il la rend sûre, en la décomposant en étapes qui ne cassent jamais la production. Trois déploiements prudents valent mieux qu’un déploiement héroïque qui coupe votre produit un vendredi soir.

La vraie question à vous poser n’est pas « comment migrer plus vite », mais : combien d’évolutions de votre modèle de données repoussez-vous en ce moment, simplement parce que votre équipe a peur de toucher à la base ? Cette peur a un coût — et il se mesure en features non livrées.

base de données, migration, déploiement, architecture, runway