Déployer une app générée par l'IA avec Docker — builds sûrs et reproductibles
Déployez une application générée par l'IA avec Docker en toute sécurité : builds reproductibles, versions verrouillées, gestion des secrets et vérifications pour éviter les images dépendantes du builder.

Pourquoi les apps générées par l'IA échouent souvent au déploiement
"Fonctionne seulement sur la machine de build" signifie que l'application tourne sur la machine qui a construit l'image, puis casse dès qu'on lance cette même image en CI, en staging ou en production. La compilation semble réussir, mais le conteneur dépend de quelque chose qui n'est pas réellement dans l'image.
Les apps générées par l'IA rencontrent ce problème plus souvent parce que le code est assemblé à partir de modèles qui supposent un environnement conciliant. Il peut dépendre d'outils installés globalement sur l'ordinateur de l'auteur, d'une base de données locale sur l'hôte, ou d'un fichier qui n'a jamais été commité. Parfois le Dockerfile "passe" en copiant trop de choses depuis l'espace de travail, incluant par erreur des caches, des artefacts compilés, ou même des secrets.
Vous voyez généralement les symptômes dès que vous essayez de déployer dans un environnement propre :
- Les builds réussissent en local mais échouent en CI avec "commande introuvable" ou bibliothèques système manquantes
- L'app démarre puis plante parce qu'une variable d'environnement manque (ou parce qu'un secret a été intégré dans l'image)
- L'authentification fonctionne en localhost mais échoue en staging à cause des URL de callback ou des réglages de cookies
- Les assets statiques ou les migrations manquent parce que l'étape de build n'a jamais été lancée dans le conteneur
- Comportements aléatoires "ça marche parfois" causés par des versions non verrouillées et des dépendances qui évoluent
Le but est simple : une image, même comportement, partout. Si le conteneur a besoin d'une chose pour fonctionner, elle doit être déclarée, installée et configurée dans les étapes de build et d'exécution, pas empruntée depuis la machine qui build.
Choisir d'abord les images de base et verrouiller les versions
Commencez par figer sur quoi votre conteneur est construit. La plupart des échecs "ça marchait sur mon laptop" arrivent parce que l'image de base ou le runtime a changé sans que vous le remarquiez.
Le plus grand risque est d'utiliser :latest. C'est pratique, mais mouvant. Une petite mise à jour de l'image de base peut changer OpenSSL, libc, Python, Node ou même le comportement du shell par défaut. Votre build peut réussir aujourd'hui et échouer la semaine prochaine, ou pire, se comporter différemment en production.
Choisissez une image de base stable (et évitez les changements cachés)
Choisissez une image de base que vous pouvez garder stable pendant des mois. Verrouillez-la à une version précise, et lorsque c'est possible, à un digest. Les pins par digest vous donnent exactement les mêmes octets d'image à chaque fois, pas seulement "Node 20" à la date d'aujourd'hui.
Verrouillez aussi la version du runtime de votre langage. "Node 20" n'est pas la même chose que "Node 20.11.1". Même des mises à jour mineures peuvent casser des modules natifs, la cryptographie ou des scripts de build.
Décidez tôt de la plateforme cible (amd64 vs arm64)
Indiquez explicitement où l'image devra tourner. Beaucoup de développeurs utilisent Apple Silicon (arm64), tandis que beaucoup de serveurs tournent en amd64. Les dépendances natives peuvent se compiler différemment, et certains paquets n'ont pas de binaires arm64.
Exemple : une app Node installe une librairie de traitement d'images. Sur arm64 elle se compile depuis la source et passe. Sur amd64 elle télécharge un binaire précompilé d'une version différente et plante à l'exécution.
Avant d'écrire le reste du Dockerfile, verrouillez :
- La version de l'image de base (et le digest si possible)
- La version du runtime (Node, Python, Java)
- La plateforme cible (amd64 ou arm64)
Si vous héritez d'un repo généré par une IA et que les versions dérivent, commencez par verrouiller l'image de base et le runtime. Cela élimine toute une catégorie de pannes de déploiement mystérieuses.
Étape par étape : un Dockerfile qui construit pareil à chaque fois
Les images reproductibles viennent de choix ennuyeux : pin de l'image de base, usage d'un lockfile, et étapes de build prévisibles.
Voici un squelette simple de Dockerfile pour une app Node typique (API ou full-stack) qui garde le cache efficace et installe de façon déterministe :
# Pin the exact base image version
FROM node:20.11.1-alpine3.19
WORKDIR /app
# Copy only dependency files first (better caching)
COPY package.json package-lock.json ./
# Deterministic install based on the lockfile
RUN npm ci --omit=dev
# Now copy the rest of the source
COPY . .
# Build only if your app has a build step
RUN npm run build
EXPOSE 3000
# Clear, explicit start command
CMD ["node", "server.js"]
# Add a simple healthcheck only if you can keep it stable
# HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:3000/health').then(r=\u003eprocess.exit(r.ok?0:1)).catch(()=\u003eprocess.exit(1))"
Trois détails comptent plus que les gens ne l'attendent :
- Copier d'abord les fichiers de dépendances, installer, puis copier le reste. Vous évitez de réinstaller les paquets à chaque fois que vous changez un fichier source.
- Utiliser la commande qui respecte le lockfile (comme
npm ci) pour obtenir les mêmes versions de dépendances à chaque build. - Garder la commande de démarrage explicite. Évitez les scripts "magiques" qui se comportent différemment selon l'environnement.
Un cas d'échec courant est une app qui tourne en local parce qu'elle lit silencieusement un fichier .env et dépend d'outils installés globalement. Dans Docker, les deux manquent, donc le build échoue ou le conteneur démarre puis plante immédiatement. Ce pattern vous force à déclarer ce dont vous avez besoin, ce qui est tout l'intérêt.
Utilisez les builds multi-étapes pour éviter des images runtime gonflées
Les builds multi-étapes séparent "build" et "run". Vous compilez l'app dans une image (avec des outils lourds), puis vous copiez uniquement le résultat final dans une seconde image, plus propre, que vous exécutez réellement.
C'est important pour les apps générées par l'IA car elles importent souvent sans le vouloir des compilateurs, des CLI et des caches. Si ceux-ci se retrouvent en production, l'image devient énorme, plus lente à transférer et plus difficile à comprendre.
Étape de build vs étape runtime (en clair)
Dans l'étape de build vous installez les outils de compilation et les dépendances de dev : compilateurs TypeScript, bundlers, et tout ce qui sert seulement à produire l'app finale.
Dans l'étape runtime vous ne conservez que ce dont l'app a besoin pour tourner. Cela rend l'image finale plus petite et plus prévisible. Cela empêche aussi une dépendance de "fonctionner" seulement parce qu'un outil de build était présent.
Une question utile : si l'app est déjà buildée, ai-je encore besoin de ce paquet ? Si non, il appartient à l'étape de build.
Le piège le plus courant
Les gens buildent avec succès, puis oublient de copier quelque chose dans l'étape runtime. Les éléments manquants sont généralement des assets buildés (comme dist/), des templates/configs runtime, ou des modules installés. Les permissions posent aussi problème : l'app tourne en local, puis échoue dans le conteneur car elle ne peut pas lire ou écrire dans un répertoire.
Après la construction de l'image, exécutez-la comme si c'était la production. Confirmez qu'elle peut démarrer avec seulement des variables d'environnement et une vraie connexion à la base. Si elle a besoin d'autre chose, cela a probablement été laissé dans l'étape de build.
Rendre les installations de dépendances déterministes
Les installations de dépendances sont là où les builds dérivent en premier. Les projets générés par l'IA fonctionnent souvent sur la machine du créateur parce que des paquets plus récents ont été installés par erreur, des caches réutilisés, ou une branche Git mouvante a été tirée.
Les lockfiles sont votre filet de sécurité. Commitez-les, et faites en sorte que votre build Docker les utilise volontairement. Pour Node, cela signifie typiquement package-lock.json avec npm ci, ou pnpm-lock.yaml avec une installation figée. Pour Python, utilisez poetry.lock (Poetry) ou des requirements entièrement épinglés et évitez latest.
Une règle sauve beaucoup de douleur : n'installez jamais à partir de références flottantes comme main, master ou un tag non épinglé. Si vous devez tirer depuis Git, fixez sur un commit SHA précis.
Les paquets privés sont une autre trappe. N'intégrez pas de tokens dans l'image. Utilisez des secrets de build afin que le build puisse accéder aux registres privés sans laisser des identifiants dans les couches. Après l'étape d'installation, l'image finale ne doit pas contenir .npmrc, config pip, ou tout fichier d'auth.
Rendez aussi les scripts de build explicites. Certains projets générés comptent sur un comportement postinstall caché qui télécharge des binaires ou lance de la génération de code. Si quelque chose doit s'exécuter, faites-le en tant qu'étape de build claire et échouez bruyamment en cas de problème.
Gérer les secrets en sécurité (sans casser le dev local)
Si une app fonctionne en local mais échoue, ou se retrouve exposée, les secrets sont souvent en cause. Une image Docker est faite pour être partagée, mise en cache et stockée dans des registres. Tout ce qu'elle contient peut fuir.
Ne jamais intégrer de valeurs sensibles dans l'image, même "juste pour tester". Cela inclut les clés API, les secrets clients OAuth, les mots de passe de base de données, les clés de signature JWT et les certificats privés.
Une règle simple : passez les secrets à l'exécution, pas pendant docker build. Les valeurs de build peuvent se coincer dans des couches d'image et des logs, surtout si un Dockerfile généré utilise ARG puis l'affiche, l'écrit dans un fichier de config, ou l'intègre dans du code frontend bundlé.
Laissez plutôt votre plateforme de déploiement injecter les secrets au démarrage du conteneur (son gestionnaire de secrets, les variables d'env configurées, ou un coffre chiffré). Gardez le Dockerfile focalisé sur l'installation des dépendances et la copie du code, pas le câblage des identifiants.
Pour le dev local, restez pratique sans committer de secrets :
- Utilisez un fichier
.envlocal et ajoutez-le à.gitignore - Commettez un
.env.exampleavec des noms d'emplacements (pas de vraies valeurs) - Échouez vite avec une erreur claire lorsqu'une variable d'environnement requise manque
Exemple : localement vous utilisez DATABASE_URL depuis .env. En production, vous définissez le même DATABASE_URL dans les settings secrets de l'hôte. Même chemin de code, valeurs différentes, rien de sensible dans l'image.
Builds reproductibles : marquer et tracer ce qui a changé
Traitez chaque build d'image comme un reçu horodaté. La plus grosse erreur est de réutiliser un tag comme latest (ou même v1) pour des contenus différents. C'est ainsi que vous obtenez des déploiements "ça marchait hier".
Un tag doit signifier un ensemble d'octets précis. Utilisez des tags qui indiquent ce que vous avez déployé et qui sont difficiles à changer discrètement.
Un schéma pratique est une version lisible pour les humains (comme v1.4.2) plus un SHA de commit pour la précision (comme sha-3f2c1a9). Si vous utilisez un tag d'environnement comme prod, faites-en un pointeur vers une version immuable.
Pour rendre les builds audités, enregistrez les entrées qui affectent l'image finale. Conservez-les à un endroit que votre équipe lira vraiment (un court fichier BUILDINFO, des notes de release, ou des labels d'image) :
- Digest de l'image de base (pas seulement
node:20, le digest exact) - Hash du lockfile (
package-lock.json,yarn.lock,poetry.lock, etc.) - Commande de build et principaux build args (par exemple
NODE_ENV=production) - Version des migrations (si votre app modifie la base de données)
Un guide utile pour décider quand rebuild :
- Changement de code seulement : rebuild de la couche app, dépendances inchangées si le lockfile n'a pas bougé
- Changement du lockfile : rebuild des dépendances et de l'app, relancez les tests
- Changement du digest de l'image de base : rebuild complet et retestez
- Changement de secret : rotation au déploiement (ne pas rebuild l'image)
Hardening basique du conteneur pour non-experts en sécurité
Si vous savez empaqueter une app dans un conteneur, vous pouvez aussi la rendre plus résistante. Vous n'avez pas besoin de compétences avancées en sécurité. Quelques réglages par défaut couvrent la plupart des problèmes réels dans des images pressées.
Commencez par éviter root quand c'est possible. Beaucoup de Dockerfiles générés exécutent tout en root parce que "ça marche simplement". En production, cela transforme un petit bug en incident plus grave. Créez un utilisateur, donnez la propriété du dossier app et exécutez le processus avec cet utilisateur.
Les permissions comptent aussi. Quand un conteneur ne peut pas écrire un fichier, la réparation rapide courante est chmod -R 777. Cela crée souvent plus de problèmes. Décidez quels dossiers doivent être inscriptibles (logs, uploads, fichiers temporaires), et donnez uniquement ces dossiers en écriture.
Si vous gardez des outils de build dans l'image finale, vous fournissez aussi plus d'outils à un attaquant potentiel. Les builds multi-étapes aident parce que compilateurs et gestionnaires de paquets restent dans l'étape de build.
Pièges courants qui causent des images dépendantes du builder
Une image dépendante du builder fonctionne pour la personne qui l'a buildée, puis casse en CI ou en production. Cela arrive généralement parce que le Dockerfile dépend de votre configuration locale.
Les coupables habituels :
- Un
.dockerignorequi cache quelque chose dont vous avez réellement besoin. Les gens ignorent parfoisdist/,prisma/,migrations/, ou même un lockfile. - Des outils globaux requis accidentellement. Si le build suppose que
tsc,vite,pnpmou Poetry est installé globalement, il peut "fonctionner par accident" sur une machine et échouer ailleurs. - Modules natifs qui se comportent différemment selon les plateformes. Tout ce qui compile du code natif peut casser si le builder est macOS/Windows mais la production est Linux.
- Variables d'environnement de build utilisées comme config runtime. Intégrer
API_URL, les réglages d'auth ou des feature flags au build peut faire paraître l'image correcte dans un environnement et cassée dans un autre.
Un contrôle de réalité rapide : rebuild sans cache et avec un contexte propre, puis lancez le conteneur avec seulement les variables d'env que vous comptez définir en production.
Vérifications rapides pré-déploiement réalisables en 10 minutes
Avant d'envoyer, faites une passe qui imite la production. Ces vérifications prennent le plus souvent en défaut.
- Rebuild depuis zéro une fois (cache désactivé) pour ne pas compter sur d'anciennes couches.
- Démarrez le conteneur avec seulement les variables d'env requises et confirmez que la config manquante échoue clairement.
- Exécutez sans bind mounts. L'image doit contenir tout ce dont elle a besoin.
- Vérifiez l'ordre de démarrage : migrations avant le processus web, et scripts de seed (si présents) sûrs.
- Analysez les logs pour config manquante, erreurs de permission et erreurs de connexion à la base.
Un échec typique : localement l'app semble correcte parce que vous aviez des clés en plus dans .env, plus un bind mount qui masquait des artefacts de build manquants. En production, elle démarre, tente d'écrire des uploads dans un dossier inexistant, les migrations ne s'exécutent pas, et vous voyez seulement un vague "500". Un build sans cache plus un run avec les variables d'env minimales exposent souvent cela en quelques minutes.
Exemple : du succès local à une image Docker sûre pour la production
Une histoire courante est de générer une petite app web avec un outil IA, la faire fonctionner sur votre laptop, puis la voir échouer sur un VPS ou un service managé. En local vous avez la bonne version de Node, un .env complet, et un cache chaud. En production, le conteneur démarre à froid, sans fichiers cachés et sans configuration interactive.
Pour diagnostiquer rapidement, comparez d'abord les versions runtime. Si votre image utilise node:latest ou python:3, vous acceptez des changements silencieux. Ensuite, vérifiez les variables d'environnement manquantes : clés d'auth, URLs de base de données et callbacks OAuth existent souvent sur votre machine mais pas dans l'environnement de déploiement. Enfin, confirmez que la sortie du build existe. Beaucoup de projets reposent sur une étape de build locale, mais l'image Docker ne copie que le source, donc il n'y a rien à servir.
Un chemin de correction pratique est souvent :
- Verrouiller l'image de base et les versions runtime.
- Commettre et appliquer une installation basée sur le lockfile.
- Déplacer les secrets vers des variables d'environnement à l'exécution (pas intégrés dans l'image, pas copiés depuis
.env). - Utiliser les builds multi-étapes pour que l'image runtime contienne seulement la sortie de production et les dépendances nécessaires.
Le succès ressemble à la même étiquette d'image qui tourne localement, en CI et en production sans étapes "il suffit de copier ce fichier".
Prochaines étapes si votre app générée par l'IA ne se déploie toujours pas
Si vous avez essayé le minimum et que cela échoue encore, arrêtez de deviner et rédigez une définition simple de ce qu'est un déploiement réussi. Gardez-la courte :
- Versions runtime exactes (image de base, runtime, gestionnaire de paquets)
- Comment les secrets sont fournis à l'exécution (et ce qui ne doit jamais être intégré dans l'image)
- Un smoke test et un health check à lancer après le déploiement
- Règles de tagging de release (une étiquette par build, liée à un commit)
- Où consulter les logs en premier
Ensuite, décidez s'il faut réparer ce que vous avez ou reconstruire proprement. Corrigez la base de code actuelle quand le flux principal fonctionne et que les échecs sont majoritairement d'emballage et de config. Reconstruisez quand les flux basiques continuent de casser, les modèles de données sont flous, ou chaque changement cause de nouvelles erreurs.
Si vous voyez des pannes uniquement en production comme une authentification cassée, des secrets exposés, une architecture spaghetti, ou des problèmes de sécurité évidents (y compris des risques d'injection SQL), il est généralement plus rapide d'avoir un deuxième regard tôt. FixMyMess (fixmymess.ai) se concentre sur le diagnostic et la réparation des bases de code générées par l'IA pour qu'elles se comportent de manière cohérente en production, en commençant par un audit de code gratuit pour identifier ce qui casse réellement.