[{"data":1,"prerenderedAt":3618},["ShallowReactive",2],{"article-fr-\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker":3,"article-sibling-fr-\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker":1959,"surround-fr-\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker":3575,"related-fr-\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker":3582},{"id":4,"title":5,"body":6,"date":1942,"description":1943,"extension":1944,"img":1945,"meta":1946,"navigation":416,"path":1947,"seo":1948,"slug":1949,"stem":1950,"tags":1951,"topics":1956,"__hash__":1958},"content\u002Fdatabases\u002Fmongo\u002F1.set-up-mongodb-replica-with-docker.fr.md","Pourquoi j'ai construit ma propre image MongoDB pour faire tourner un replica set en production",{"type":7,"value":8,"toc":1928},"minimark",[9,18,21,29,61,66,73,80,84,97,104,114,247,253,257,260,275,278,305,308,328,331,334,361,376,386,390,397,509,522,527,593,596,630,635,641,663,685,690,708,769,795,805,809,819,881,887,1214,1227,1231,1242,1249,1346,1349,1355,1493,1499,1513,1624,1637,1665,1668,1672,1681,1684,1692,1717,1724,1728,1738,1768,1793,1835,1841,1845,1851,1861,1867,1874,1878,1924],[10,11,12,13,17],"p",{},"Il y a un moment, dans la vie d'une application, où une seule base de données ne suffit plus. Pas parce qu'elle est lente. Parce qu'elle est ",[14,15,16],"em",{},"seule",".",[10,19,20],{},"Une seule instance MongoDB, c'est un point de défaillance unique. Le jour où ce conteneur tombe — disque plein, kernel qui panique, redémarrage de VM qui se passe mal — votre application ne ralentit pas : elle s'arrête. Pas de bascule, pas de réplique prête à prendre le relais. Juste un écran d'erreur et un téléphone qui sonne.",[10,22,23,24,28],{},"C'est le problème de départ. Et la réponse classique de MongoDB, c'est le ",[25,26,27],"strong",{},"replica set"," : plusieurs nœuds qui détiennent la même donnée, élisent un primaire, et basculent automatiquement quand ce primaire disparaît. Sur le papier, c'est simple. Dans un environnement Docker reproductible, multi-machines et multi-clients, ça l'est beaucoup moins. Cet article raconte où ça coince réellement, et comment j'ai fini par résoudre le problème une bonne fois pour toutes.",[30,31,32],"blockquote",{},[10,33,34,35,39,40,39,43,39,46,49,50,57,58,17],{},"Le lab complet (",[36,37,38],"code",{},"Dockerfile",", ",[36,41,42],{},"init.sh",[36,44,45],{},"init.js",[36,47,48],{},"mongod.conf",") est disponible sur ",[51,52,56],"a",{"href":53,"rel":54},"https:\u002F\u002Fgithub.com\u002Fdenisakp\u002Fdev-life-labs",[55],"nofollow","github.com\u002Fdenisakp\u002Fdev-life-labs",", dans le dossier ",[36,59,60],{},"mongo-replica",[62,63,65],"h2",{"id":64},"le-contexte-une-plateforme-multi-tenant-une-base-par-client","Le contexte : une plateforme multi-tenant, une base par client",[10,67,68,69,72],{},"Avant d'entrer dans le vif : chaque client de la plateforme tourne sur sa propre VM isolée, avec sa propre stack Docker Compose. Cela veut dire que la base de données n'est pas déployée une fois, mais ",[25,70,71],{},"autant de fois qu'il y a de clients",". Tout ce qui est fragile ou manuel dans le déploiement de MongoDB se retrouve donc multiplié par le nombre de VMs.",[10,74,75,76,79],{},"Cette contrainte change tout. Une procédure « qui marche si on fait attention » n'est pas acceptable quand on doit la rejouer sur dix machines différentes, sur macOS en dev et sur AlmaLinux en prod. Il fallait quelque chose qui marche ",[14,77,78],{},"toujours",", sans intervention.",[62,81,83],{"id":82},"pourquoi-un-replica-set-concrètement","Pourquoi un replica set, concrètement",[10,85,86,87,39,90,39,93,96],{},"Un replica set MongoDB, c'est trois nœuds dans mon cas — ",[36,88,89],{},"mongo1",[36,91,92],{},"mongo2",[36,94,95],{},"mongo3",". Un seul est primaire à un instant donné : il reçoit toutes les écritures. Les deux autres sont secondaires et répliquent la donnée en continu.",[10,98,99,100,103],{},"L'intérêt n'est pas le volume ni la performance. C'est la ",[25,101,102],{},"disponibilité",". Si le primaire tombe, les nœuds restants organisent une élection et en désignent un nouveau en quelques secondes. L'application se reconnecte automatiquement, à condition d'utiliser la bonne chaîne de connexion (j'y reviens). Trois nœuds, c'est le minimum pour qu'une élection ait une majorité claire — deux survivants sur trois, ça suffit pour décider ; un seul survivant sur deux, non.",[10,105,106,107,109,110,113],{},"Dans ma config, le nœud ",[36,108,89],{}," reçoit une ",[36,111,112],{},"priority: 2",", les autres restent à 1 :",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","members: [\n  { _id: 0, host: 'mongo1:27017', priority: 2 },\n  { _id: 1, host: 'mongo2:27017' },\n  { _id: 2, host: 'mongo3:27017' }\n]\n","javascript","",[36,122,123,140,187,214,241],{"__ignoreMap":120},[124,125,128,132,136],"span",{"class":126,"line":127},"line",1,[124,129,131],{"class":130},"sBMFI","members",[124,133,135],{"class":134},"sMK4o",":",[124,137,139],{"class":138},"sTEyZ"," [\n",[124,141,143,146,150,152,156,159,162,164,167,171,174,176,179,181,184],{"class":126,"line":142},2,[124,144,145],{"class":134},"  {",[124,147,149],{"class":148},"swJcz"," _id",[124,151,135],{"class":134},[124,153,155],{"class":154},"sbssI"," 0",[124,157,158],{"class":134},",",[124,160,161],{"class":148}," host",[124,163,135],{"class":134},[124,165,166],{"class":134}," '",[124,168,170],{"class":169},"sfazB","mongo1:27017",[124,172,173],{"class":134},"'",[124,175,158],{"class":134},[124,177,178],{"class":148}," priority",[124,180,135],{"class":134},[124,182,183],{"class":154}," 2",[124,185,186],{"class":134}," },\n",[124,188,190,192,194,196,199,201,203,205,207,210,212],{"class":126,"line":189},3,[124,191,145],{"class":134},[124,193,149],{"class":148},[124,195,135],{"class":134},[124,197,198],{"class":154}," 1",[124,200,158],{"class":134},[124,202,161],{"class":148},[124,204,135],{"class":134},[124,206,166],{"class":134},[124,208,209],{"class":169},"mongo2:27017",[124,211,173],{"class":134},[124,213,186],{"class":134},[124,215,217,219,221,223,225,227,229,231,233,236,238],{"class":126,"line":216},4,[124,218,145],{"class":134},[124,220,149],{"class":148},[124,222,135],{"class":134},[124,224,183],{"class":154},[124,226,158],{"class":134},[124,228,161],{"class":148},[124,230,135],{"class":134},[124,232,166],{"class":134},[124,234,235],{"class":169},"mongo3:27017",[124,237,173],{"class":134},[124,239,240],{"class":134}," }\n",[124,242,244],{"class":126,"line":243},5,[124,245,246],{"class":138},"]\n",[10,248,249,250,252],{},"Cette priorité plus élevée dit simplement à MongoDB : « si ",[36,251,89],{}," est en bonne santé, c'est lui le primaire ». Ça rend le comportement prévisible, ce qui simplifie le debug et la supervision.",[62,254,256],{"id":255},"le-vrai-piège-lauthentification-entre-nœuds","Le vrai piège : l'authentification entre nœuds",[10,258,259],{},"Voilà où le tutoriel « 3 conteneurs et c'est parti » s'effondre.",[10,261,262,263,266,267,270,271,274],{},"Dès qu'un replica set est protégé par authentification — et en production, il ",[14,264,265],{},"doit"," l'être — les nœuds ne se contentent pas d'authentifier les clients. Ils doivent aussi ",[25,268,269],{},"s'authentifier entre eux",". MongoDB utilise pour ça un ",[14,272,273],{},"keyfile"," : un secret partagé que tous les nœuds possèdent.",[10,276,277],{},"On le génère une fois :",[115,279,283],{"className":280,"code":281,"language":282,"meta":120,"style":120},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","openssl rand -base64 756 > rs_keyfile\n","bash",[36,284,285],{"__ignoreMap":120},[124,286,287,290,293,296,299,302],{"class":126,"line":127},[124,288,289],{"class":130},"openssl",[124,291,292],{"class":169}," rand",[124,294,295],{"class":169}," -base64",[124,297,298],{"class":154}," 756",[124,300,301],{"class":134}," >",[124,303,304],{"class":169}," rs_keyfile\n",[10,306,307],{},"Et MongoDB est intransigeant sur ce fichier. Il exige :",[309,310,311,322],"ul",{},[312,313,314,315,321],"li",{},"des permissions ",[25,316,317,318],{},"exactement à ",[36,319,320],{},"400"," (lecture seule pour le propriétaire, rien pour les autres),",[312,323,324,325,17],{},"une appartenance à l'utilisateur ",[36,326,327],{},"mongodb",[10,329,330],{},"Un seul bit de trop, et le nœud refuse de démarrer. Pas un warning : un refus net.",[10,332,333],{},"Le réflexe naturel, c'est de monter ce keyfile en volume depuis l'hôte :",[115,335,339],{"className":336,"code":337,"language":338,"meta":120,"style":120},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","volumes:\n  - .\u002Fmongodb\u002Frs_keyfile:\u002Fetc\u002Fmongodb\u002Fkeyfile:ro   # ❌ piège\n","yaml",[36,340,341,349],{"__ignoreMap":120},[124,342,343,346],{"class":126,"line":127},[124,344,345],{"class":148},"volumes",[124,347,348],{"class":134},":\n",[124,350,351,354,357],{"class":126,"line":142},[124,352,353],{"class":134},"  -",[124,355,356],{"class":169}," .\u002Fmongodb\u002Frs_keyfile:\u002Fetc\u002Fmongodb\u002Fkeyfile:ro",[124,358,360],{"class":359},"sHwdD","   # ❌ piège\n",[10,362,363,364,367,368,371,372,375],{},"Sauf que les permissions d'un fichier monté depuis l'hôte ",[25,365,366],{},"dépendent de l'hôte",". Sur ma machine macOS de dev, elles ne ressemblent pas à celles d'une VM AlmaLinux. Le fichier qui démarre parfaitement en local fait planter le conteneur en production. On bricole un ",[36,369,370],{},"chmod",", ça marche aujourd'hui, ça casse au prochain ",[36,373,374],{},"git clone"," sur une autre machine. Le comportement n'est jamais reproductible.",[10,377,378,379,381,382,385],{},"Multipliez maintenant ce problème : trois nœuds, quatre fichiers de config chacun (keyfile, ",[36,380,48],{},", scripts d'init), c'est ",[25,383,384],{},"douze montages de volume"," par déploiement. Douze occasions de se tromper de chemin ou de permission. Sur chaque VM. C'est exactement le genre de friction qui transforme un déploiement en loterie.",[62,387,389],{"id":388},"la-solution-tout-cuire-dans-limage","La solution : tout cuire dans l'image",[10,391,392,393,396],{},"Le déclic a été d'arrêter de monter la configuration au runtime, et de la ",[25,394,395],{},"figer au build",". Au lieu de demander à l'hôte de fournir les bons fichiers avec les bonnes permissions, je construis une image qui les contient déjà, correctement.",[115,398,402],{"className":399,"code":400,"filename":38,"language":401,"meta":120,"style":120},"language-dockerfile shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","FROM mongo:8.2-noble\n\n# Le keyfile reçoit ses permissions 400 ici, une fois pour toutes\nCOPY --chown=mongodb:mongodb --chmod=400 rs_keyfile \u002Fetc\u002Fmongodb\u002Fkeyfile\n\nCOPY --chown=mongodb:mongodb --chmod=644 mongod.conf \u002Fetc\u002Fmongod.conf\nCOPY --chown=mongodb:mongodb --chmod=755 init.sh \u002Finit.sh\nCOPY --chown=mongodb:mongodb --chmod=644 init.js \u002Finit.js\n\n# Le dossier qui contient le keyfile est lui aussi verrouillé\nRUN chown -R mongodb:mongodb \u002Fetc\u002Fmongodb && \\\n    chmod 700 \u002Fetc\u002Fmongodb\n\nCMD [\"--config\", \"\u002Fetc\u002Fmongod.conf\"]\n","dockerfile",[36,403,404,412,418,423,431,435,443,451,459,464,470,479,485,490],{"__ignoreMap":120},[124,405,406,409],{"class":126,"line":127},[124,407,408],{"class":154},"FROM",[124,410,411],{"class":138}," mongo:8.2-noble\n",[124,413,414],{"class":126,"line":142},[124,415,417],{"emptyLinePlaceholder":416},true,"\n",[124,419,420],{"class":126,"line":189},[124,421,422],{"class":359},"# Le keyfile reçoit ses permissions 400 ici, une fois pour toutes\n",[124,424,425,428],{"class":126,"line":216},[124,426,427],{"class":154},"COPY",[124,429,430],{"class":138}," --chown=mongodb:mongodb --chmod=400 rs_keyfile \u002Fetc\u002Fmongodb\u002Fkeyfile\n",[124,432,433],{"class":126,"line":243},[124,434,417],{"emptyLinePlaceholder":416},[124,436,438,440],{"class":126,"line":437},6,[124,439,427],{"class":154},[124,441,442],{"class":138}," --chown=mongodb:mongodb --chmod=644 mongod.conf \u002Fetc\u002Fmongod.conf\n",[124,444,446,448],{"class":126,"line":445},7,[124,447,427],{"class":154},[124,449,450],{"class":138}," --chown=mongodb:mongodb --chmod=755 init.sh \u002Finit.sh\n",[124,452,454,456],{"class":126,"line":453},8,[124,455,427],{"class":154},[124,457,458],{"class":138}," --chown=mongodb:mongodb --chmod=644 init.js \u002Finit.js\n",[124,460,462],{"class":126,"line":461},9,[124,463,417],{"emptyLinePlaceholder":416},[124,465,467],{"class":126,"line":466},10,[124,468,469],{"class":359},"# Le dossier qui contient le keyfile est lui aussi verrouillé\n",[124,471,473,476],{"class":126,"line":472},11,[124,474,475],{"class":154},"RUN",[124,477,478],{"class":138}," chown -R mongodb:mongodb \u002Fetc\u002Fmongodb && \\\n",[124,480,482],{"class":126,"line":481},12,[124,483,484],{"class":138},"    chmod 700 \u002Fetc\u002Fmongodb\n",[124,486,488],{"class":126,"line":487},13,[124,489,417],{"emptyLinePlaceholder":416},[124,491,493,496,499,502,504,507],{"class":126,"line":492},14,[124,494,495],{"class":154},"CMD",[124,497,498],{"class":138}," [",[124,500,501],{"class":169},"\"--config\"",[124,503,39],{"class":138},[124,505,506],{"class":169},"\"\u002Fetc\u002Fmongod.conf\"",[124,508,246],{"class":138},[10,510,511,512,515,516,518,519,521],{},"Le ",[36,513,514],{},"--chmod=400"," du ",[36,517,427],{}," n'est pas une suggestion : c'est gravé dans la couche d'image. Peu importe l'hôte, peu importe l'OS, le keyfile a ",[14,520,78],{}," les bonnes permissions et le bon propriétaire. Le problème de reproductibilité disparaît, parce que l'hôte n'a plus son mot à dire.",[10,523,511,524,526],{},[36,525,48],{}," embarqué est minimal — il pointe vers le keyfile désormais garanti présent :",[115,528,530],{"className":336,"code":529,"filename":48,"language":338,"meta":120,"style":120},"replication:\n  replSetName: okydookReplSet\nsecurity:\n  keyFile: \u002Fetc\u002Fmongodb\u002Fkeyfile\nnet:\n  bindIp: 0.0.0.0\n  port: 27017\n",[36,531,532,539,549,556,566,573,583],{"__ignoreMap":120},[124,533,534,537],{"class":126,"line":127},[124,535,536],{"class":148},"replication",[124,538,348],{"class":134},[124,540,541,544,546],{"class":126,"line":142},[124,542,543],{"class":148},"  replSetName",[124,545,135],{"class":134},[124,547,548],{"class":169}," okydookReplSet\n",[124,550,551,554],{"class":126,"line":189},[124,552,553],{"class":148},"security",[124,555,348],{"class":134},[124,557,558,561,563],{"class":126,"line":216},[124,559,560],{"class":148},"  keyFile",[124,562,135],{"class":134},[124,564,565],{"class":169}," \u002Fetc\u002Fmongodb\u002Fkeyfile\n",[124,567,568,571],{"class":126,"line":243},[124,569,570],{"class":148},"net",[124,572,348],{"class":134},[124,574,575,578,580],{"class":126,"line":437},[124,576,577],{"class":148},"  bindIp",[124,579,135],{"class":134},[124,581,582],{"class":154}," 0.0.0.0\n",[124,584,585,588,590],{"class":126,"line":445},[124,586,587],{"class":148},"  port",[124,589,135],{"class":134},[124,591,592],{"class":154}," 27017\n",[10,594,595],{},"Ce changement de perspective a quatre conséquences directes :",[309,597,598,604,618,624],{},[312,599,600,603],{},[25,601,602],{},"Cohérence"," — même comportement sur macOS en dev et sur les VMs en prod. L'hôte n'intervient plus dans la config.",[312,605,606,609,610,613,614,617],{},[25,607,608],{},"Simplicité"," — fini les douze montages. Le compose ne monte plus que les volumes de ",[14,611,612],{},"données"," (",[36,615,616],{},"\u002Fdata\u002Fdb","), ce qui est leur vrai rôle.",[312,619,620,623],{},[25,621,622],{},"Versionnement"," — un changement de config devient un nouveau tag d'image. On sait exactement quelle configuration tourne sur quel nœud, et on peut revenir en arrière.",[312,625,626,629],{},[25,627,628],{},"Sécurité"," — la configuration est immuable. On ne peut pas la modifier par accident au runtime, et on réduit la surface d'attaque en supprimant les liaisons hôte↔conteneur.",[631,632,634],"h3",{"id":633},"construire-limage","Construire l'image",[10,636,637,638,640],{},"Pour faire tourner le lab sur une seule machine, pas besoin de registre — build l'image localement depuis le ",[36,639,38],{}," :",[115,642,644],{"className":280,"code":643,"language":282,"meta":120,"style":120},"docker build -t mongo-replica:latest .\n",[36,645,646],{"__ignoreMap":120},[124,647,648,651,654,657,660],{"class":126,"line":127},[124,649,650],{"class":130},"docker",[124,652,653],{"class":169}," build",[124,655,656],{"class":169}," -t",[124,658,659],{"class":169}," mongo-replica:latest",[124,661,662],{"class":169}," .\n",[10,664,665,666,669,670,673,674,677,678,681,682,17],{},"C'est exactement ce que fait le ",[36,667,668],{},"docker-compose.yml"," du lab tout seul : chaque nœud déclare ",[36,671,672],{},"build: ."," avec ",[36,675,676],{},"image: mongo-replica:latest",", donc un simple ",[36,679,680],{},"docker compose up"," build l'image une fois et la réutilise pour les trois nœuds. Pas de registre, pas de ",[36,683,684],{},"docker login",[686,687,689],"h4",{"id":688},"passer-en-multi-vm-pousser-sur-un-registre","Passer en multi-VM : pousser sur un registre",[10,691,692,693,613,696,699,700,703,704,707],{},"En prod je ne fais pas tourner les trois nœuds sur une seule machine — ils vivent sur des ",[25,694,695],{},"VMs séparées",[36,697,698],{},"linux\u002Famd64",", AlmaLinux sur Proxmox), alors que mon poste de dev est en ",[36,701,702],{},"arm64",". Builder localement sur chaque VM ne suffit plus : je build une fois pour la plateforme cible et je pousse sur ",[25,705,706],{},"un registre que je contrôle",", puis chaque VM tire la même image :",[115,709,711],{"className":280,"code":710,"language":282,"meta":120,"style":120},"docker buildx build \\\n  --platform linux\u002Famd64 \\\n  -t \u003Cyour-registry>\u002Fmongo-replica:latest \\\n  --push \\\n  .\n",[36,712,713,725,735,757,764],{"__ignoreMap":120},[124,714,715,717,720,722],{"class":126,"line":127},[124,716,650],{"class":130},[124,718,719],{"class":169}," buildx",[124,721,653],{"class":169},[124,723,724],{"class":138}," \\\n",[124,726,727,730,733],{"class":126,"line":142},[124,728,729],{"class":169},"  --platform",[124,731,732],{"class":169}," linux\u002Famd64",[124,734,724],{"class":138},[124,736,737,740,743,746,749,752,755],{"class":126,"line":189},[124,738,739],{"class":169},"  -t",[124,741,742],{"class":134}," \u003C",[124,744,745],{"class":169},"your-registr",[124,747,748],{"class":138},"y",[124,750,751],{"class":134},">",[124,753,754],{"class":169},"\u002Fmongo-replica:latest",[124,756,724],{"class":138},[124,758,759,762],{"class":126,"line":216},[124,760,761],{"class":169},"  --push",[124,763,724],{"class":138},[124,765,766],{"class":126,"line":243},[124,767,768],{"class":169},"  .\n",[10,770,771,772,774,775,778,779,782,783,786,787,790,791,794],{},"Ensuite, dans le compose, remplace ",[36,773,672],{}," par ",[36,776,777],{},"image: \u003Cyour-registry>\u002Fmongo-replica:latest",". Remplace ",[36,780,781],{},"\u003Cyour-registry>"," par le registre où ",[14,784,785],{},"tu"," pousses ton image. Comme cette image embarque ton keyfile et ta config, elle n'a rien à faire sur un registre public : pousse-la sur un registre ",[25,788,789],{},"privé"," — GitHub Container Registry (",[36,792,793],{},"ghcr.io\u002F\u003Cton-user>","), Docker Hub privé, GitLab, ou un registre auto-hébergé. Un registre public ne convient que si l'image ne contient aucun secret (par exemple si tu génères le keyfile au runtime plutôt que de le cuire dans l'image).",[10,796,797,798,800,801,804],{},"Chaque VM se contente ensuite de tirer l'image (après un ",[36,799,684],{}," sur le registre privé). Déployer une base devient : ",[14,802,803],{},"pull, run",". Plus aucun fichier à copier à la main.",[62,806,808],{"id":807},"le-compose-avant-et-après","Le compose, avant et après",[10,810,811,812,814,815,818],{},"Le bénéfice se voit le mieux dans le ",[36,813,668],{},". ",[25,816,817],{},"Avant",", chaque nœud croulait sous les montages :",[115,820,822],{"className":336,"code":821,"filename":668,"language":338,"meta":120,"style":120},"mongo1:\n  image: mongo:8.2-noble\n  volumes:\n    - mongo1_data:\u002Fdata\u002Fdb\n    - mongo1_config:\u002Fdata\u002Fconfigdb\n    - $PWD\u002Fscripts\u002Fmongo\u002Frs_keyfile:\u002Fetc\u002Fmongodb\u002Fkeyfile:ro   # ❌ permissions hôte\n    - $PWD\u002Fscripts\u002Fmongo\u002Fmongod.conf:\u002Fetc\u002Fmongod.conf:ro      # ❌ × 3 nœuds = 12 montages\n",[36,823,824,830,839,846,854,861,871],{"__ignoreMap":120},[124,825,826,828],{"class":126,"line":127},[124,827,89],{"class":148},[124,829,348],{"class":134},[124,831,832,835,837],{"class":126,"line":142},[124,833,834],{"class":148},"  image",[124,836,135],{"class":134},[124,838,411],{"class":169},[124,840,841,844],{"class":126,"line":189},[124,842,843],{"class":148},"  volumes",[124,845,348],{"class":134},[124,847,848,851],{"class":126,"line":216},[124,849,850],{"class":134},"    -",[124,852,853],{"class":169}," mongo1_data:\u002Fdata\u002Fdb\n",[124,855,856,858],{"class":126,"line":243},[124,857,850],{"class":134},[124,859,860],{"class":169}," mongo1_config:\u002Fdata\u002Fconfigdb\n",[124,862,863,865,868],{"class":126,"line":437},[124,864,850],{"class":134},[124,866,867],{"class":169}," $PWD\u002Fscripts\u002Fmongo\u002Frs_keyfile:\u002Fetc\u002Fmongodb\u002Fkeyfile:ro",[124,869,870],{"class":359},"   # ❌ permissions hôte\n",[124,872,873,875,878],{"class":126,"line":445},[124,874,850],{"class":134},[124,876,877],{"class":169}," $PWD\u002Fscripts\u002Fmongo\u002Fmongod.conf:\u002Fetc\u002Fmongod.conf:ro",[124,879,880],{"class":359},"      # ❌ × 3 nœuds = 12 montages\n",[10,882,883,886],{},[25,884,885],{},"Après",", l'image custom porte la config, le compose ne garde que les données :",[115,888,890],{"className":336,"code":889,"filename":668,"language":338,"meta":120,"style":120},"services:\n  mongo1:\n    container_name: mongo1\n    build: .                       # build local → en prod : image: \u003Cyour-registry>\u002Fmongo-replica:latest\n    image: mongo-replica:latest\n    hostname: mongo1\n    networks: [ mongo-cluster ]\n    volumes:\n      - mongo1_data:\u002Fdata\u002Fdb          # ✅ uniquement les données\n      - mongo1_config:\u002Fdata\u002Fconfigdb\n    environment:\n      MONGO_INITDB_ROOT_USERNAME: '${MONGO_ADMIN_USER:-admin}'\n      MONGO_INITDB_ROOT_PASSWORD: '${MONGO_ADMIN_PASSWD:-veryStringPassword}'\n\n  # mongo2 et mongo3 : strictement identiques, seul le suffixe change\n\n  mongo-init:\n    container_name: mongo-init\n    image: mongo-replica:latest\n    restart: \"no\"\n    entrypoint: [\"\u002Fbin\u002Fbash\", \"\u002Finit.sh\"]\n    networks: [ mongo-cluster ]\n    depends_on: [ mongo1, mongo2, mongo3 ]\n    environment:\n      MONGO_ROOT_USERNAME: '${MONGO_ADMIN_USER:-admin}'\n      MONGO_ROOT_PASSWORD: '${MONGO_ADMIN_PASSWD:-veryStringPassword}'\n      MONGO_DB_USER: '${DB_USERNAME:-okydook}'\n      MONGO_DB_PASSWORD: '${DB_PASSWORD:-mypassword}'\n",[36,891,892,899,906,916,929,939,948,963,970,981,987,994,1009,1023,1027,1033,1038,1046,1056,1065,1082,1111,1124,1149,1156,1170,1184,1199],{"__ignoreMap":120},[124,893,894,897],{"class":126,"line":127},[124,895,896],{"class":148},"services",[124,898,348],{"class":134},[124,900,901,904],{"class":126,"line":142},[124,902,903],{"class":148},"  mongo1",[124,905,348],{"class":134},[124,907,908,911,913],{"class":126,"line":189},[124,909,910],{"class":148},"    container_name",[124,912,135],{"class":134},[124,914,915],{"class":169}," mongo1\n",[124,917,918,921,923,926],{"class":126,"line":216},[124,919,920],{"class":148},"    build",[124,922,135],{"class":134},[124,924,925],{"class":154}," .",[124,927,928],{"class":359},"                       # build local → en prod : image: \u003Cyour-registry>\u002Fmongo-replica:latest\n",[124,930,931,934,936],{"class":126,"line":243},[124,932,933],{"class":148},"    image",[124,935,135],{"class":134},[124,937,938],{"class":169}," mongo-replica:latest\n",[124,940,941,944,946],{"class":126,"line":437},[124,942,943],{"class":148},"    hostname",[124,945,135],{"class":134},[124,947,915],{"class":169},[124,949,950,953,955,957,960],{"class":126,"line":445},[124,951,952],{"class":148},"    networks",[124,954,135],{"class":134},[124,956,498],{"class":134},[124,958,959],{"class":169}," mongo-cluster",[124,961,962],{"class":134}," ]\n",[124,964,965,968],{"class":126,"line":453},[124,966,967],{"class":148},"    volumes",[124,969,348],{"class":134},[124,971,972,975,978],{"class":126,"line":461},[124,973,974],{"class":134},"      -",[124,976,977],{"class":169}," mongo1_data:\u002Fdata\u002Fdb",[124,979,980],{"class":359},"          # ✅ uniquement les données\n",[124,982,983,985],{"class":126,"line":466},[124,984,974],{"class":134},[124,986,860],{"class":169},[124,988,989,992],{"class":126,"line":472},[124,990,991],{"class":148},"    environment",[124,993,348],{"class":134},[124,995,996,999,1001,1003,1006],{"class":126,"line":481},[124,997,998],{"class":148},"      MONGO_INITDB_ROOT_USERNAME",[124,1000,135],{"class":134},[124,1002,166],{"class":134},[124,1004,1005],{"class":169},"${MONGO_ADMIN_USER:-admin}",[124,1007,1008],{"class":134},"'\n",[124,1010,1011,1014,1016,1018,1021],{"class":126,"line":487},[124,1012,1013],{"class":148},"      MONGO_INITDB_ROOT_PASSWORD",[124,1015,135],{"class":134},[124,1017,166],{"class":134},[124,1019,1020],{"class":169},"${MONGO_ADMIN_PASSWD:-veryStringPassword}",[124,1022,1008],{"class":134},[124,1024,1025],{"class":126,"line":492},[124,1026,417],{"emptyLinePlaceholder":416},[124,1028,1030],{"class":126,"line":1029},15,[124,1031,1032],{"class":359},"  # mongo2 et mongo3 : strictement identiques, seul le suffixe change\n",[124,1034,1036],{"class":126,"line":1035},16,[124,1037,417],{"emptyLinePlaceholder":416},[124,1039,1041,1044],{"class":126,"line":1040},17,[124,1042,1043],{"class":148},"  mongo-init",[124,1045,348],{"class":134},[124,1047,1049,1051,1053],{"class":126,"line":1048},18,[124,1050,910],{"class":148},[124,1052,135],{"class":134},[124,1054,1055],{"class":169}," mongo-init\n",[124,1057,1059,1061,1063],{"class":126,"line":1058},19,[124,1060,933],{"class":148},[124,1062,135],{"class":134},[124,1064,938],{"class":169},[124,1066,1068,1071,1073,1076,1079],{"class":126,"line":1067},20,[124,1069,1070],{"class":148},"    restart",[124,1072,135],{"class":134},[124,1074,1075],{"class":134}," \"",[124,1077,1078],{"class":169},"no",[124,1080,1081],{"class":134},"\"\n",[124,1083,1085,1088,1090,1092,1095,1098,1100,1102,1104,1107,1109],{"class":126,"line":1084},21,[124,1086,1087],{"class":148},"    entrypoint",[124,1089,135],{"class":134},[124,1091,498],{"class":134},[124,1093,1094],{"class":134},"\"",[124,1096,1097],{"class":169},"\u002Fbin\u002Fbash",[124,1099,1094],{"class":134},[124,1101,158],{"class":134},[124,1103,1075],{"class":134},[124,1105,1106],{"class":169},"\u002Finit.sh",[124,1108,1094],{"class":134},[124,1110,246],{"class":134},[124,1112,1114,1116,1118,1120,1122],{"class":126,"line":1113},22,[124,1115,952],{"class":148},[124,1117,135],{"class":134},[124,1119,498],{"class":134},[124,1121,959],{"class":169},[124,1123,962],{"class":134},[124,1125,1127,1130,1132,1134,1137,1139,1142,1144,1147],{"class":126,"line":1126},23,[124,1128,1129],{"class":148},"    depends_on",[124,1131,135],{"class":134},[124,1133,498],{"class":134},[124,1135,1136],{"class":169}," mongo1",[124,1138,158],{"class":134},[124,1140,1141],{"class":169}," mongo2",[124,1143,158],{"class":134},[124,1145,1146],{"class":169}," mongo3",[124,1148,962],{"class":134},[124,1150,1152,1154],{"class":126,"line":1151},24,[124,1153,991],{"class":148},[124,1155,348],{"class":134},[124,1157,1159,1162,1164,1166,1168],{"class":126,"line":1158},25,[124,1160,1161],{"class":148},"      MONGO_ROOT_USERNAME",[124,1163,135],{"class":134},[124,1165,166],{"class":134},[124,1167,1005],{"class":169},[124,1169,1008],{"class":134},[124,1171,1173,1176,1178,1180,1182],{"class":126,"line":1172},26,[124,1174,1175],{"class":148},"      MONGO_ROOT_PASSWORD",[124,1177,135],{"class":134},[124,1179,166],{"class":134},[124,1181,1020],{"class":169},[124,1183,1008],{"class":134},[124,1185,1187,1190,1192,1194,1197],{"class":126,"line":1186},27,[124,1188,1189],{"class":148},"      MONGO_DB_USER",[124,1191,135],{"class":134},[124,1193,166],{"class":134},[124,1195,1196],{"class":169},"${DB_USERNAME:-okydook}",[124,1198,1008],{"class":134},[124,1200,1202,1205,1207,1209,1212],{"class":126,"line":1201},28,[124,1203,1204],{"class":148},"      MONGO_DB_PASSWORD",[124,1206,135],{"class":134},[124,1208,166],{"class":134},[124,1210,1211],{"class":169},"${DB_PASSWORD:-mypassword}",[124,1213,1008],{"class":134},[10,1215,1216,1217,1219,1220,1222,1223,17],{},"Plus de keyfile, plus de ",[36,1218,48],{},", plus de scripts montés : ils vivent dans l'image. C'est tout l'intérêt. Le ",[36,1221,668],{}," complet — les trois nœuds, les réseaux, les volumes — est dans le ",[51,1224,1226],{"href":53,"rel":1225},[55],"repo du lab",[62,1228,1230],{"id":1229},"orchestrer-lamorçage-le-vrai-casse-tête-caché","Orchestrer l'amorçage : le vrai casse-tête caché",[10,1232,1233,1234,1237,1238,1241],{},"Construire l'image ne suffit pas. Un replica set ne s'initialise pas tout seul : il faut, ",[25,1235,1236],{},"une fois",", lancer la commande ",[36,1239,1240],{},"rs.initiate()"," qui dit aux trois nœuds qu'ils forment un cluster. Et il faut le faire au bon moment — ni avant que les nœuds soient prêts, ni deux fois.",[10,1243,1244,1245,1248],{},"J'ai confié ce travail à un conteneur éphémère dédié, ",[36,1246,1247],{},"mongo-init",", qui ne fait que ça puis s'éteint. Son script attend patiemment que MongoDB réponde avant d'agir :",[115,1250,1252],{"className":280,"code":1251,"filename":42,"language":282,"meta":120,"style":120},"until mongosh --host mongo1:27017 -u \"$MONGO_ROOT_USERNAME\" -p \"$MONGO_ROOT_PASSWORD\" \\\n  --authenticationDatabase admin --eval \"db.adminCommand('ping')\" > \u002Fdev\u002Fnull 2>&1; do\n  echo \"Waiting for MongoDB connection...\"\n  sleep 5\ndone\n",[36,1253,1254,1291,1320,1333,1341],{"__ignoreMap":120},[124,1255,1256,1260,1263,1266,1269,1272,1274,1277,1279,1282,1284,1287,1289],{"class":126,"line":127},[124,1257,1259],{"class":1258},"s7zQu","until",[124,1261,1262],{"class":130}," mongosh",[124,1264,1265],{"class":169}," --host",[124,1267,1268],{"class":169}," mongo1:27017",[124,1270,1271],{"class":169}," -u",[124,1273,1075],{"class":134},[124,1275,1276],{"class":138},"$MONGO_ROOT_USERNAME",[124,1278,1094],{"class":134},[124,1280,1281],{"class":169}," -p",[124,1283,1075],{"class":134},[124,1285,1286],{"class":138},"$MONGO_ROOT_PASSWORD",[124,1288,1094],{"class":134},[124,1290,724],{"class":138},[124,1292,1293,1296,1299,1302,1304,1307,1309,1311,1314,1317],{"class":126,"line":142},[124,1294,1295],{"class":169},"  --authenticationDatabase",[124,1297,1298],{"class":169}," admin",[124,1300,1301],{"class":169}," --eval",[124,1303,1075],{"class":134},[124,1305,1306],{"class":169},"db.adminCommand('ping')",[124,1308,1094],{"class":134},[124,1310,301],{"class":134},[124,1312,1313],{"class":169}," \u002Fdev\u002Fnull",[124,1315,1316],{"class":134}," 2>&1;",[124,1318,1319],{"class":1258}," do\n",[124,1321,1322,1326,1328,1331],{"class":126,"line":189},[124,1323,1325],{"class":1324},"s2Zo4","  echo",[124,1327,1075],{"class":134},[124,1329,1330],{"class":169},"Waiting for MongoDB connection...",[124,1332,1081],{"class":134},[124,1334,1335,1338],{"class":126,"line":216},[124,1336,1337],{"class":130},"  sleep",[124,1339,1340],{"class":154}," 5\n",[124,1342,1343],{"class":126,"line":243},[124,1344,1345],{"class":1258},"done\n",[10,1347,1348],{},"Deux détails comptent énormément ici, parce qu'ils font la différence entre un déploiement automatisé et un déploiement à surveiller :",[10,1350,1351,1354],{},[25,1352,1353],{},"L'initialisation est idempotente."," Le script vérifie d'abord si le replica set existe déjà avant de tenter de le créer :",[115,1356,1358],{"className":117,"code":1357,"filename":45,"language":119,"meta":120,"style":120},"try {\n  var status = rs.status();\n  if (status.ok === 1) {\n    isInitialized = true;\n    print(\"ReplicaSet already initiated\");\n  }\n} catch(e) {\n  print(\"ReplicaSet not yet initiated, proceeding...\");\n}\n",[36,1359,1360,1368,1394,1419,1432,1452,1457,1470,1488],{"__ignoreMap":120},[124,1361,1362,1365],{"class":126,"line":127},[124,1363,1364],{"class":1258},"try",[124,1366,1367],{"class":134}," {\n",[124,1369,1370,1374,1377,1380,1383,1385,1388,1391],{"class":126,"line":142},[124,1371,1373],{"class":1372},"spNyl","  var",[124,1375,1376],{"class":138}," status",[124,1378,1379],{"class":134}," =",[124,1381,1382],{"class":138}," rs",[124,1384,17],{"class":134},[124,1386,1387],{"class":1324},"status",[124,1389,1390],{"class":148},"()",[124,1392,1393],{"class":134},";\n",[124,1395,1396,1399,1401,1403,1405,1408,1411,1413,1416],{"class":126,"line":189},[124,1397,1398],{"class":1258},"  if",[124,1400,613],{"class":148},[124,1402,1387],{"class":138},[124,1404,17],{"class":134},[124,1406,1407],{"class":138},"ok",[124,1409,1410],{"class":134}," ===",[124,1412,198],{"class":154},[124,1414,1415],{"class":148},") ",[124,1417,1418],{"class":134},"{\n",[124,1420,1421,1424,1426,1430],{"class":126,"line":216},[124,1422,1423],{"class":138},"    isInitialized",[124,1425,1379],{"class":134},[124,1427,1429],{"class":1428},"sfNiH"," true",[124,1431,1393],{"class":134},[124,1433,1434,1437,1440,1442,1445,1447,1450],{"class":126,"line":243},[124,1435,1436],{"class":1324},"    print",[124,1438,1439],{"class":148},"(",[124,1441,1094],{"class":134},[124,1443,1444],{"class":169},"ReplicaSet already initiated",[124,1446,1094],{"class":134},[124,1448,1449],{"class":148},")",[124,1451,1393],{"class":134},[124,1453,1454],{"class":126,"line":437},[124,1455,1456],{"class":134},"  }\n",[124,1458,1459,1462,1465,1468],{"class":126,"line":445},[124,1460,1461],{"class":134},"}",[124,1463,1464],{"class":1258}," catch",[124,1466,1467],{"class":138},"(e) ",[124,1469,1418],{"class":134},[124,1471,1472,1475,1477,1479,1482,1484,1486],{"class":126,"line":453},[124,1473,1474],{"class":1324},"  print",[124,1476,1439],{"class":148},[124,1478,1094],{"class":134},[124,1480,1481],{"class":169},"ReplicaSet not yet initiated, proceeding...",[124,1483,1094],{"class":134},[124,1485,1449],{"class":148},[124,1487,1393],{"class":134},[124,1489,1490],{"class":126,"line":461},[124,1491,1492],{"class":134},"}\n",[10,1494,1495,1496,1498],{},"Sans cette garde, relancer la stack tenterait de réinitialiser un cluster déjà vivant — et le ferait planter. Avec elle, on peut relancer ",[36,1497,680],{}," autant de fois qu'on veut : si le cluster existe, le script le constate et passe son chemin. C'est ce qui rend le déploiement rejouable sans crainte.",[10,1500,1501,1504,1505,1508,1509,1512],{},[25,1502,1503],{},"L'utilisateur applicatif est créé dans la foulée",", avec les droits stricts dont il a besoin et rien de plus — ",[36,1506,1507],{},"readWrite"," sur la seule base ",[36,1510,1511],{},"okydook",", pas un accès admin :",[115,1514,1516],{"className":117,"code":1515,"filename":45,"language":119,"meta":120,"style":120},"db.createUser({\n  user: process.env.MONGO_DB_USER,\n  pwd: process.env.MONGO_DB_PASSWORD,\n  roles: [{ role: 'readWrite', db: 'okydook' }]\n});\n",[36,1517,1518,1532,1555,1575,1616],{"__ignoreMap":120},[124,1519,1520,1523,1525,1528,1530],{"class":126,"line":127},[124,1521,1522],{"class":138},"db",[124,1524,17],{"class":134},[124,1526,1527],{"class":1324},"createUser",[124,1529,1439],{"class":138},[124,1531,1418],{"class":134},[124,1533,1534,1537,1539,1542,1544,1547,1549,1552],{"class":126,"line":142},[124,1535,1536],{"class":148},"  user",[124,1538,135],{"class":134},[124,1540,1541],{"class":138}," process",[124,1543,17],{"class":134},[124,1545,1546],{"class":138},"env",[124,1548,17],{"class":134},[124,1550,1551],{"class":138},"MONGO_DB_USER",[124,1553,1554],{"class":134},",\n",[124,1556,1557,1560,1562,1564,1566,1568,1570,1573],{"class":126,"line":189},[124,1558,1559],{"class":148},"  pwd",[124,1561,135],{"class":134},[124,1563,1541],{"class":138},[124,1565,17],{"class":134},[124,1567,1546],{"class":138},[124,1569,17],{"class":134},[124,1571,1572],{"class":138},"MONGO_DB_PASSWORD",[124,1574,1554],{"class":134},[124,1576,1577,1580,1582,1584,1587,1590,1592,1594,1596,1598,1600,1603,1605,1607,1609,1611,1614],{"class":126,"line":216},[124,1578,1579],{"class":148},"  roles",[124,1581,135],{"class":134},[124,1583,498],{"class":138},[124,1585,1586],{"class":134},"{",[124,1588,1589],{"class":148}," role",[124,1591,135],{"class":134},[124,1593,166],{"class":134},[124,1595,1507],{"class":169},[124,1597,173],{"class":134},[124,1599,158],{"class":134},[124,1601,1602],{"class":148}," db",[124,1604,135],{"class":134},[124,1606,166],{"class":134},[124,1608,1511],{"class":169},[124,1610,173],{"class":134},[124,1612,1613],{"class":134}," }",[124,1615,246],{"class":138},[124,1617,1618,1620,1622],{"class":126,"line":243},[124,1619,1461],{"class":134},[124,1621,1449],{"class":138},[124,1623,1393],{"class":134},[10,1625,1626,1627,1629,1630,1633,1634,640],{},"Enfin, Compose enchaîne les dépendances proprement : l'UI d'admin n'attend pas seulement que ",[36,1628,1247],{}," ait ",[14,1631,1632],{},"démarré",", mais qu'il se soit terminé ",[25,1635,1636],{},"avec succès",[115,1638,1640],{"className":336,"code":1639,"filename":668,"language":338,"meta":120,"style":120},"depends_on:\n  mongo-init:\n    condition: service_completed_successfully\n",[36,1641,1642,1649,1655],{"__ignoreMap":120},[124,1643,1644,1647],{"class":126,"line":127},[124,1645,1646],{"class":148},"depends_on",[124,1648,348],{"class":134},[124,1650,1651,1653],{"class":126,"line":142},[124,1652,1043],{"class":148},[124,1654,348],{"class":134},[124,1656,1657,1660,1662],{"class":126,"line":189},[124,1658,1659],{"class":148},"    condition",[124,1661,135],{"class":134},[124,1663,1664],{"class":169}," service_completed_successfully\n",[10,1666,1667],{},"Cette condition est subtile mais essentielle : elle garantit qu'on n'expose jamais une interface sur un cluster à moitié initialisé.",[62,1669,1671],{"id":1670},"la-pièce-finale-se-connecter-au-cluster-pas-à-un-nœud","La pièce finale : se connecter au cluster, pas à un nœud",[10,1673,1674,1675,1677,1678,1680],{},"Tout ce travail serait gâché si l'application pointait vers un seul nœud. Car si elle ne connaît que ",[36,1676,89],{}," et que ",[36,1679,89],{}," tombe, elle est aveugle au nouveau primaire fraîchement élu — la haute disponibilité ne sert plus à rien.",[10,1682,1683],{},"La chaîne de connexion liste donc les trois nœuds et nomme explicitement le replica set :",[115,1685,1690],{"className":1686,"code":1688,"language":1689},[1687],"language-text","mongodb:\u002F\u002FUSER:PASS@mongo1:27017,mongo2:27017,mongo3:27017\u002Fokydook?replicaSet=okydookReplSet&authSource=okydook\n","text",[36,1691,1688],{"__ignoreMap":120},[30,1693,1694],{},[10,1695,1696,1697,1700,1701,1704,1705,1708,1709,1712,1713,1716],{},"Les trois nœuds répondent sur le port ",[36,1698,1699],{},"27017"," ",[14,1702,1703],{},"à l'intérieur"," du réseau Docker — ce sont les hostnames du réseau ",[36,1706,1707],{},"mongo-cluster",", pas des ports mappés sur l'hôte. Lister ",[36,1710,1711],{},"mongo2:27018"," ou ",[36,1714,1715],{},"mongo3:27019"," ici est une erreur fréquente : ces ports n'existent qu'en dehors du réseau.",[10,1718,1719,1720,1723],{},"Le paramètre ",[36,1721,1722],{},"replicaSet=okydookReplSet"," est ce qui change tout. Avec lui, le driver MongoDB ne traite pas ces trois adresses comme trois bases indépendantes : il comprend que c'est un cluster, découvre sa topologie, repère qui est primaire à chaque instant, et redirige automatiquement les écritures vers le bon nœud — y compris après une bascule. L'application ne sait même pas qu'une élection a eu lieu. C'est précisément l'effet recherché.",[62,1725,1727],{"id":1726},"vérifier-que-tout-tourne","Vérifier que tout tourne",[10,1729,1730,1731,1734,1735,1737],{},"Une fois la stack démarrée (",[36,1732,1733],{},"docker compose up -d","), deux vérifications suffisent. D'abord le keyfile, le point qui faisait planter le setup monté en volume — il doit être à ",[36,1736,320],{}," dans le conteneur :",[115,1739,1741],{"className":280,"code":1740,"language":282,"meta":120,"style":120},"docker exec -it mongo1 ls -la \u002Fetc\u002Fmongodb\u002Fkeyfile\n# -r-------- 1 mongodb mongodb ...\n",[36,1742,1743,1763],{"__ignoreMap":120},[124,1744,1745,1747,1750,1753,1755,1758,1761],{"class":126,"line":127},[124,1746,650],{"class":130},[124,1748,1749],{"class":169}," exec",[124,1751,1752],{"class":169}," -it",[124,1754,1136],{"class":169},[124,1756,1757],{"class":169}," ls",[124,1759,1760],{"class":169}," -la",[124,1762,565],{"class":169},[124,1764,1765],{"class":126,"line":142},[124,1766,1767],{"class":359},"# -r-------- 1 mongodb mongodb ...\n",[10,1769,1770,1771,1774,1775,1777,1778,39,1781,1783,1784,1777,1786,1789,1790,17],{},"Ensuite l'état du cluster : ",[36,1772,1773],{},"rs.status()"," doit montrer ",[36,1776,89],{}," en ",[36,1779,1780],{},"PRIMARY",[36,1782,92],{},"\u002F",[36,1785,95],{},[36,1787,1788],{},"SECONDARY",", tous avec ",[36,1791,1792],{},"health: 1",[115,1794,1796],{"className":280,"code":1795,"language":282,"meta":120,"style":120},"docker exec -it mongo1 mongosh -u admin -p PASSWORD \\\n  --authenticationDatabase admin --eval \"rs.status()\"\n",[36,1797,1798,1821],{"__ignoreMap":120},[124,1799,1800,1802,1804,1806,1808,1810,1812,1814,1816,1819],{"class":126,"line":127},[124,1801,650],{"class":130},[124,1803,1749],{"class":169},[124,1805,1752],{"class":169},[124,1807,1136],{"class":169},[124,1809,1262],{"class":169},[124,1811,1271],{"class":169},[124,1813,1298],{"class":169},[124,1815,1281],{"class":169},[124,1817,1818],{"class":169}," PASSWORD",[124,1820,724],{"class":138},[124,1822,1823,1825,1827,1829,1831,1833],{"class":126,"line":142},[124,1824,1295],{"class":169},[124,1826,1298],{"class":169},[124,1828,1301],{"class":169},[124,1830,1075],{"class":134},[124,1832,1773],{"class":169},[124,1834,1081],{"class":134},[10,1836,1837,1838,17],{},"Les autres commandes de diagnostic (auth de l'utilisateur applicatif, ping) sont listées dans le README du ",[51,1839,1226],{"href":53,"rel":1840},[55],[62,1842,1844],{"id":1843},"ce-que-ce-parcours-ma-appris","Ce que ce parcours m'a appris",[10,1846,1847,1848],{},"Le problème de départ semblait être « MongoDB doit être hautement disponible ». Le vrai problème, celui qui m'a coûté du temps, était ailleurs : ",[25,1849,1850],{},"rendre un setup distribué reproductible à travers des environnements qui ne se ressemblent pas.",[10,1852,1853,1854,1856,1857,1860],{},"La leçon que je retiens : quand une configuration est fragile à cause de l'environnement — permissions, chemins, état de l'hôte — la bonne réponse n'est pas de la documenter mieux ni d'ajouter des ",[36,1855,370],{}," défensifs. C'est de ",[25,1858,1859],{},"retirer l'environnement de l'équation",". Cuire la config dans l'image, rendre l'init idempotent, déclarer les dépendances explicitement : chacune de ces décisions transforme une étape « il faut faire attention » en une étape « ça marche, point ».",[10,1862,1863,1864,17],{},"Et c'est peut-être ça le vrai sens de l'infrastructure as code : pas seulement écrire l'infra dans des fichiers, mais éliminer méthodiquement chaque endroit où un humain doit ",[14,1865,1866],{},"faire attention",[10,1868,1869,1870,1873],{},"Dans un prochain article, je m'attaque à la suite logique : ",[25,1871,1872],{},"comment sauvegarder et restaurer ce cluster MongoDB dans un environnement conteneurisé",". Parce qu'un cluster hautement disponible sans stratégie de backup, c'est juste une façon plus élaborée de perdre ses données.",[62,1875,1877],{"id":1876},"références","Références",[309,1879,1880,1889,1896,1903,1910,1917],{},[312,1881,1882],{},[51,1883,1885,1886],{"href":53,"rel":1884},[55],"Lab complet : ",[36,1887,1888],{},"dev-life-labs\u002Fmongo-replica",[312,1890,1891],{},[51,1892,1895],{"href":1893,"rel":1894},"https:\u002F\u002Fwww.mongodb.com\u002Fdocs\u002Fmanual\u002Ftutorial\u002Fdeploy-replica-set-with-keyfile-access-control\u002F",[55],"Authentification keyfile d'un replica set MongoDB",[312,1897,1898],{},[51,1899,1902],{"href":1900,"rel":1901},"https:\u002F\u002Fdocs.mongodb.com\u002Fmanual\u002Freplication\u002F",[55],"Documentation MongoDB Replica Set",[312,1904,1905],{},[51,1906,1909],{"href":1907,"rel":1908},"https:\u002F\u002Fdocs.docker.com\u002Fbuild\u002Fbuilding\u002Fmulti-platform\u002F",[55],"Builds multi-plateformes Docker",[312,1911,1912],{},[51,1913,1916],{"href":1914,"rel":1915},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fpackages\u002Fworking-with-a-github-packages-registry\u002Fworking-with-the-container-registry",[55],"GitHub Container Registry",[312,1918,1919],{},[51,1920,1923],{"href":1921,"rel":1922},"https:\u002F\u002Fdocs.docker.com\u002Fcompose\u002F",[55],"Documentation Docker Compose",[1925,1926,1927],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":120,"searchDepth":142,"depth":142,"links":1929},[1930,1931,1932,1933,1936,1937,1938,1939,1940,1941],{"id":64,"depth":142,"text":65},{"id":82,"depth":142,"text":83},{"id":255,"depth":142,"text":256},{"id":388,"depth":142,"text":389,"children":1934},[1935],{"id":633,"depth":189,"text":634},{"id":807,"depth":142,"text":808},{"id":1229,"depth":142,"text":1230},{"id":1670,"depth":142,"text":1671},{"id":1726,"depth":142,"text":1727},{"id":1843,"depth":142,"text":1844},{"id":1876,"depth":142,"text":1877},"2025-12-13","Retour d'expérience : déployer un cluster MongoDB replica set à 3 nœuds avec Docker, et pourquoi figer la config dans une image custom au lieu de monter des volumes rend le déploiement reproductible sur des environnements qui ne se ressemblent pas.","md","https:\u002F\u002Fres.cloudinary.com\u002Fdpdwhd6ka\u002Fimage\u002Fupload\u002Ff_auto,q_auto\u002Fv1\u002FBlog\u002Farticles\u002Fmongodb-replicaset",{},"\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker.fr",{"title":5,"description":1943},"set-up-mongodb-replica-with-docker","databases\u002Fmongo\u002F1.set-up-mongodb-replica-with-docker.fr",[1952,1953,1954,1955],"MongoDB","Replica-Set","Docker","Docker-compose",[1957,650,327],"databases","PzRn-itn0vJYKfgE7sfD42s_7P8UmBrXMPQEvk11tYU",{"id":1960,"title":1961,"body":1962,"date":1942,"description":3567,"extension":1944,"img":1945,"meta":3568,"navigation":416,"path":3569,"seo":3570,"slug":1949,"stem":3571,"tags":3572,"topics":3573,"__hash__":3574},"content\u002Fdatabases\u002Fmongo\u002F1.set-up-mongodb-replica-with-docker.md","Why I built my own MongoDB image to run a replica set in production",{"type":7,"value":1963,"toc":3553},[1964,1970,1973,1979,2000,2004,2011,2018,2022,2032,2039,2048,2144,2150,2154,2157,2171,2174,2192,2195,2212,2215,2218,2238,2251,2261,2265,2272,2357,2370,2376,2430,2433,2466,2470,2475,2491,2508,2512,2528,2576,2600,2609,2613,2622,2677,2683,2948,2961,2965,2975,2981,3055,3058,3064,3172,3178,3190,3286,3299,3323,3326,3330,3339,3342,3347,3367,3372,3376,3385,3409,3429,3469,3475,3479,3485,3495,3501,3508,3512,3551],[10,1965,1966,1967,17],{},"There comes a moment in an application's life when a single database is no longer enough. Not because it's slow. Because it's ",[14,1968,1969],{},"alone",[10,1971,1972],{},"A single MongoDB instance is a single point of failure. The day that container goes down — full disk, kernel panic, a VM reboot gone wrong — your app doesn't slow down: it stops. No failover, no replica ready to take over. Just an error screen and a ringing phone.",[10,1974,1975,1976,1978],{},"That's the starting problem. MongoDB's classic answer is the ",[25,1977,27],{},": several nodes holding the same data, electing a primary, and failing over automatically when that primary disappears. On paper, it's simple. In a reproducible, multi-machine, multi-tenant Docker environment, it's a lot less so. This article tells where it actually breaks, and how I ended up solving the problem once and for all.",[30,1980,1981],{},[10,1982,1983,1984,39,1986,39,1988,39,1990,1992,1993,1996,1997,1999],{},"The full lab (",[36,1985,38],{},[36,1987,42],{},[36,1989,45],{},[36,1991,48],{},") is available on ",[51,1994,56],{"href":53,"rel":1995},[55],", in the ",[36,1998,60],{}," folder.",[62,2001,2003],{"id":2002},"the-context-a-multi-tenant-platform-one-database-per-client","The context: a multi-tenant platform, one database per client",[10,2005,2006,2007,2010],{},"Before getting into it: every client on the platform runs on its own isolated VM, with its own Docker Compose stack. That means the database isn't deployed once, but ",[25,2008,2009],{},"as many times as there are clients",". Anything fragile or manual in deploying MongoDB gets multiplied by the number of VMs.",[10,2012,2013,2014,2017],{},"That constraint changes everything. A procedure that \"works if you're careful\" is not acceptable when you have to replay it on ten different machines, on macOS in dev and AlmaLinux in prod. I needed something that works ",[14,2015,2016],{},"every time",", with no intervention.",[62,2019,2021],{"id":2020},"why-a-replica-set-concretely","Why a replica set, concretely",[10,2023,2024,2025,39,2027,39,2029,2031],{},"A MongoDB replica set is three nodes in my case — ",[36,2026,89],{},[36,2028,92],{},[36,2030,95],{},". Only one is primary at any moment: it receives all writes. The other two are secondaries that replicate the data continuously.",[10,2033,2034,2035,2038],{},"The point isn't volume or performance. It's ",[25,2036,2037],{},"availability",". If the primary goes down, the surviving nodes hold an election and pick a new one within seconds. The application reconnects automatically, provided it uses the right connection string (more on that). Three nodes is the minimum for an election to have a clear majority — two survivors out of three is enough to decide; a single survivor out of two is not.",[10,2040,2041,2042,2044,2045,2047],{},"In my config, ",[36,2043,89],{}," gets ",[36,2046,112],{},", the others stay at 1:",[115,2049,2050],{"className":117,"code":118,"language":119,"meta":120,"style":120},[36,2051,2052,2060,2092,2116,2140],{"__ignoreMap":120},[124,2053,2054,2056,2058],{"class":126,"line":127},[124,2055,131],{"class":130},[124,2057,135],{"class":134},[124,2059,139],{"class":138},[124,2061,2062,2064,2066,2068,2070,2072,2074,2076,2078,2080,2082,2084,2086,2088,2090],{"class":126,"line":142},[124,2063,145],{"class":134},[124,2065,149],{"class":148},[124,2067,135],{"class":134},[124,2069,155],{"class":154},[124,2071,158],{"class":134},[124,2073,161],{"class":148},[124,2075,135],{"class":134},[124,2077,166],{"class":134},[124,2079,170],{"class":169},[124,2081,173],{"class":134},[124,2083,158],{"class":134},[124,2085,178],{"class":148},[124,2087,135],{"class":134},[124,2089,183],{"class":154},[124,2091,186],{"class":134},[124,2093,2094,2096,2098,2100,2102,2104,2106,2108,2110,2112,2114],{"class":126,"line":189},[124,2095,145],{"class":134},[124,2097,149],{"class":148},[124,2099,135],{"class":134},[124,2101,198],{"class":154},[124,2103,158],{"class":134},[124,2105,161],{"class":148},[124,2107,135],{"class":134},[124,2109,166],{"class":134},[124,2111,209],{"class":169},[124,2113,173],{"class":134},[124,2115,186],{"class":134},[124,2117,2118,2120,2122,2124,2126,2128,2130,2132,2134,2136,2138],{"class":126,"line":216},[124,2119,145],{"class":134},[124,2121,149],{"class":148},[124,2123,135],{"class":134},[124,2125,183],{"class":154},[124,2127,158],{"class":134},[124,2129,161],{"class":148},[124,2131,135],{"class":134},[124,2133,166],{"class":134},[124,2135,235],{"class":169},[124,2137,173],{"class":134},[124,2139,240],{"class":134},[124,2141,2142],{"class":126,"line":243},[124,2143,246],{"class":138},[10,2145,2146,2147,2149],{},"That higher priority simply tells MongoDB: \"if ",[36,2148,89],{}," is healthy, it's the primary.\" It makes behavior predictable, which simplifies debugging and monitoring.",[62,2151,2153],{"id":2152},"the-real-trap-node-to-node-authentication","The real trap: node-to-node authentication",[10,2155,2156],{},"Here's where the \"3 containers and you're done\" tutorial falls apart.",[10,2158,2159,2160,2163,2164,2167,2168,2170],{},"As soon as a replica set is protected by authentication — and in production it ",[14,2161,2162],{},"must"," be — nodes don't just authenticate clients. They must also ",[25,2165,2166],{},"authenticate each other",". MongoDB uses a ",[14,2169,273],{}," for this: a shared secret every node holds.",[10,2172,2173],{},"You generate it once:",[115,2175,2176],{"className":280,"code":281,"language":282,"meta":120,"style":120},[36,2177,2178],{"__ignoreMap":120},[124,2179,2180,2182,2184,2186,2188,2190],{"class":126,"line":127},[124,2181,289],{"class":130},[124,2183,292],{"class":169},[124,2185,295],{"class":169},[124,2187,298],{"class":154},[124,2189,301],{"class":134},[124,2191,304],{"class":169},[10,2193,2194],{},"And MongoDB is uncompromising about this file. It requires:",[309,2196,2197,2206],{},[312,2198,2199,2200,2205],{},"permissions ",[25,2201,2202,2203],{},"exactly ",[36,2204,320],{}," (read-only for the owner, nothing for others),",[312,2207,2208,2209,2211],{},"ownership by the ",[36,2210,327],{}," user.",[10,2213,2214],{},"One bit too many, and the node refuses to start. Not a warning: a flat refusal.",[10,2216,2217],{},"The natural reflex is to mount this keyfile as a volume from the host:",[115,2219,2221],{"className":336,"code":2220,"language":338,"meta":120,"style":120},"volumes:\n  - .\u002Fmongodb\u002Frs_keyfile:\u002Fetc\u002Fmongodb\u002Fkeyfile:ro   # ❌ the trap\n",[36,2222,2223,2229],{"__ignoreMap":120},[124,2224,2225,2227],{"class":126,"line":127},[124,2226,345],{"class":148},[124,2228,348],{"class":134},[124,2230,2231,2233,2235],{"class":126,"line":142},[124,2232,353],{"class":134},[124,2234,356],{"class":169},[124,2236,2237],{"class":359},"   # ❌ the trap\n",[10,2239,2240,2241,2244,2245,2247,2248,2250],{},"Except the permissions of a host-mounted file ",[25,2242,2243],{},"depend on the host",". On my macOS dev machine they don't look like those on an AlmaLinux VM. The file that starts perfectly locally crashes the container in production. You patch it with a ",[36,2246,370],{},", it works today, it breaks on the next ",[36,2249,374],{}," on another machine. The behavior is never reproducible.",[10,2252,2253,2254,2256,2257,2260],{},"Now multiply the problem: three nodes, four config files each (keyfile, ",[36,2255,48],{},", init scripts) — that's ",[25,2258,2259],{},"twelve volume mounts"," per deployment. Twelve chances to get a path or a permission wrong. On every VM. Exactly the kind of friction that turns a deployment into a lottery.",[62,2262,2264],{"id":2263},"the-solution-bake-everything-into-the-image","The solution: bake everything into the image",[10,2266,2267,2268,2271],{},"The breakthrough was to stop mounting configuration at runtime and ",[25,2269,2270],{},"freeze it at build time",". Instead of asking the host to provide the right files with the right permissions, I build an image that already contains them, correctly.",[115,2273,2275],{"className":399,"code":2274,"filename":38,"language":401,"meta":120,"style":120},"FROM mongo:8.2-noble\n\n# The keyfile gets its 400 permissions here, once and for all\nCOPY --chown=mongodb:mongodb --chmod=400 rs_keyfile \u002Fetc\u002Fmongodb\u002Fkeyfile\n\nCOPY --chown=mongodb:mongodb --chmod=644 mongod.conf \u002Fetc\u002Fmongod.conf\nCOPY --chown=mongodb:mongodb --chmod=755 init.sh \u002Finit.sh\nCOPY --chown=mongodb:mongodb --chmod=644 init.js \u002Finit.js\n\n# The directory holding the keyfile is locked down too\nRUN chown -R mongodb:mongodb \u002Fetc\u002Fmongodb && \\\n    chmod 700 \u002Fetc\u002Fmongodb\n\nCMD [\"--config\", \"\u002Fetc\u002Fmongod.conf\"]\n",[36,2276,2277,2283,2287,2292,2298,2302,2308,2314,2320,2324,2329,2335,2339,2343],{"__ignoreMap":120},[124,2278,2279,2281],{"class":126,"line":127},[124,2280,408],{"class":154},[124,2282,411],{"class":138},[124,2284,2285],{"class":126,"line":142},[124,2286,417],{"emptyLinePlaceholder":416},[124,2288,2289],{"class":126,"line":189},[124,2290,2291],{"class":359},"# The keyfile gets its 400 permissions here, once and for all\n",[124,2293,2294,2296],{"class":126,"line":216},[124,2295,427],{"class":154},[124,2297,430],{"class":138},[124,2299,2300],{"class":126,"line":243},[124,2301,417],{"emptyLinePlaceholder":416},[124,2303,2304,2306],{"class":126,"line":437},[124,2305,427],{"class":154},[124,2307,442],{"class":138},[124,2309,2310,2312],{"class":126,"line":445},[124,2311,427],{"class":154},[124,2313,450],{"class":138},[124,2315,2316,2318],{"class":126,"line":453},[124,2317,427],{"class":154},[124,2319,458],{"class":138},[124,2321,2322],{"class":126,"line":461},[124,2323,417],{"emptyLinePlaceholder":416},[124,2325,2326],{"class":126,"line":466},[124,2327,2328],{"class":359},"# The directory holding the keyfile is locked down too\n",[124,2330,2331,2333],{"class":126,"line":472},[124,2332,475],{"class":154},[124,2334,478],{"class":138},[124,2336,2337],{"class":126,"line":481},[124,2338,484],{"class":138},[124,2340,2341],{"class":126,"line":487},[124,2342,417],{"emptyLinePlaceholder":416},[124,2344,2345,2347,2349,2351,2353,2355],{"class":126,"line":492},[124,2346,495],{"class":154},[124,2348,498],{"class":138},[124,2350,501],{"class":169},[124,2352,39],{"class":138},[124,2354,506],{"class":169},[124,2356,246],{"class":138},[10,2358,2359,2360,2362,2363,2365,2366,2369],{},"The ",[36,2361,514],{}," on ",[36,2364,427],{}," isn't a suggestion: it's etched into the image layer. Whatever the host, whatever the OS, the keyfile ",[14,2367,2368],{},"always"," has the right permissions and owner. The reproducibility problem disappears, because the host no longer has a say.",[10,2371,2372,2373,2375],{},"The embedded ",[36,2374,48],{}," is minimal — it points at the now-guaranteed keyfile:",[115,2377,2378],{"className":336,"code":529,"filename":48,"language":338,"meta":120,"style":120},[36,2379,2380,2386,2394,2400,2408,2414,2422],{"__ignoreMap":120},[124,2381,2382,2384],{"class":126,"line":127},[124,2383,536],{"class":148},[124,2385,348],{"class":134},[124,2387,2388,2390,2392],{"class":126,"line":142},[124,2389,543],{"class":148},[124,2391,135],{"class":134},[124,2393,548],{"class":169},[124,2395,2396,2398],{"class":126,"line":189},[124,2397,553],{"class":148},[124,2399,348],{"class":134},[124,2401,2402,2404,2406],{"class":126,"line":216},[124,2403,560],{"class":148},[124,2405,135],{"class":134},[124,2407,565],{"class":169},[124,2409,2410,2412],{"class":126,"line":243},[124,2411,570],{"class":148},[124,2413,348],{"class":134},[124,2415,2416,2418,2420],{"class":126,"line":437},[124,2417,577],{"class":148},[124,2419,135],{"class":134},[124,2421,582],{"class":154},[124,2423,2424,2426,2428],{"class":126,"line":445},[124,2425,587],{"class":148},[124,2427,135],{"class":134},[124,2429,592],{"class":154},[10,2431,2432],{},"This shift in perspective has four direct consequences:",[309,2434,2435,2441,2454,2460],{},[312,2436,2437,2440],{},[25,2438,2439],{},"Consistency"," — same behavior on macOS in dev and on the prod VMs. The host no longer touches the config.",[312,2442,2443,2446,2447,2450,2451,2453],{},[25,2444,2445],{},"Simplicity"," — no more twelve mounts. Compose only mounts the ",[14,2448,2449],{},"data"," volumes (",[36,2452,616],{},"), which is their real job.",[312,2455,2456,2459],{},[25,2457,2458],{},"Versioning"," — a config change becomes a new image tag. You know exactly which configuration runs on which node, and you can roll back.",[312,2461,2462,2465],{},[25,2463,2464],{},"Security"," — the configuration is immutable. It can't be accidentally modified at runtime, and you shrink the attack surface by removing host↔container bindings.",[631,2467,2469],{"id":2468},"build-the-image","Build the image",[10,2471,2472,2473,135],{},"To run the lab on a single machine, you don't need a registry at all — build the image locally from the ",[36,2474,38],{},[115,2476,2477],{"className":280,"code":643,"language":282,"meta":120,"style":120},[36,2478,2479],{"__ignoreMap":120},[124,2480,2481,2483,2485,2487,2489],{"class":126,"line":127},[124,2482,650],{"class":130},[124,2484,653],{"class":169},[124,2486,656],{"class":169},[124,2488,659],{"class":169},[124,2490,662],{"class":169},[10,2492,2493,2494,2496,2497,2499,2500,2502,2503,2505,2506,17],{},"That's exactly what the lab's ",[36,2495,668],{}," does on its own: each node declares ",[36,2498,672],{}," with ",[36,2501,676],{},", so a plain ",[36,2504,680],{}," builds the image once and reuses it for the three nodes. No registry, no ",[36,2507,684],{},[686,2509,2511],{"id":2510},"going-multi-vm-push-to-a-registry","Going multi-VM: push to a registry",[10,2513,2514,2515,613,2518,2520,2521,2523,2524,2527],{},"In production I don't run the three nodes on one machine — they live on ",[25,2516,2517],{},"separate VMs",[36,2519,698],{},", AlmaLinux on Proxmox), while my dev machine is ",[36,2522,702],{},". Building locally on each VM no longer cuts it: I build once for the target platform and push to ",[25,2525,2526],{},"a registry I control",", then every VM pulls the same image:",[115,2529,2530],{"className":280,"code":710,"language":282,"meta":120,"style":120},[36,2531,2532,2542,2550,2566,2572],{"__ignoreMap":120},[124,2533,2534,2536,2538,2540],{"class":126,"line":127},[124,2535,650],{"class":130},[124,2537,719],{"class":169},[124,2539,653],{"class":169},[124,2541,724],{"class":138},[124,2543,2544,2546,2548],{"class":126,"line":142},[124,2545,729],{"class":169},[124,2547,732],{"class":169},[124,2549,724],{"class":138},[124,2551,2552,2554,2556,2558,2560,2562,2564],{"class":126,"line":189},[124,2553,739],{"class":169},[124,2555,742],{"class":134},[124,2557,745],{"class":169},[124,2559,748],{"class":138},[124,2561,751],{"class":134},[124,2563,754],{"class":169},[124,2565,724],{"class":138},[124,2567,2568,2570],{"class":126,"line":216},[124,2569,761],{"class":169},[124,2571,724],{"class":138},[124,2573,2574],{"class":126,"line":243},[124,2575,768],{"class":169},[10,2577,2578,2579,2581,2582,2584,2585,2587,2588,2591,2592,2595,2596,2599],{},"Then, in the compose file, swap ",[36,2580,672],{}," for ",[36,2583,777],{},". Replace ",[36,2586,781],{}," with the registry ",[14,2589,2590],{},"you"," push to. Since this image bakes in your keyfile and config, it has no business on a public registry: push it to a ",[25,2593,2594],{},"private"," one — GitHub Container Registry (",[36,2597,2598],{},"ghcr.io\u002F\u003Cyour-user>","), a private Docker Hub repo, GitLab, or a self-hosted registry. A public registry is only fine if the image holds no secrets (e.g. you generate the keyfile at runtime instead of baking it in).",[10,2601,2602,2603,2605,2606,2608],{},"Each VM then just pulls the image (after a ",[36,2604,684],{}," to the private registry). Deploying a database becomes: ",[14,2607,803],{},". No more files to copy by hand.",[62,2610,2612],{"id":2611},"the-compose-file-before-and-after","The compose file, before and after",[10,2614,2615,2616,814,2618,2621],{},"The benefit shows up best in ",[36,2617,668],{},[25,2619,2620],{},"Before",", each node drowned in mounts:",[115,2623,2625],{"className":336,"code":2624,"filename":668,"language":338,"meta":120,"style":120},"mongo1:\n  image: mongo:8.2-noble\n  volumes:\n    - mongo1_data:\u002Fdata\u002Fdb\n    - mongo1_config:\u002Fdata\u002Fconfigdb\n    - $PWD\u002Fscripts\u002Fmongo\u002Frs_keyfile:\u002Fetc\u002Fmongodb\u002Fkeyfile:ro   # ❌ host permissions\n    - $PWD\u002Fscripts\u002Fmongo\u002Fmongod.conf:\u002Fetc\u002Fmongod.conf:ro      # ❌ × 3 nodes = 12 mounts\n",[36,2626,2627,2633,2641,2647,2653,2659,2668],{"__ignoreMap":120},[124,2628,2629,2631],{"class":126,"line":127},[124,2630,89],{"class":148},[124,2632,348],{"class":134},[124,2634,2635,2637,2639],{"class":126,"line":142},[124,2636,834],{"class":148},[124,2638,135],{"class":134},[124,2640,411],{"class":169},[124,2642,2643,2645],{"class":126,"line":189},[124,2644,843],{"class":148},[124,2646,348],{"class":134},[124,2648,2649,2651],{"class":126,"line":216},[124,2650,850],{"class":134},[124,2652,853],{"class":169},[124,2654,2655,2657],{"class":126,"line":243},[124,2656,850],{"class":134},[124,2658,860],{"class":169},[124,2660,2661,2663,2665],{"class":126,"line":437},[124,2662,850],{"class":134},[124,2664,867],{"class":169},[124,2666,2667],{"class":359},"   # ❌ host permissions\n",[124,2669,2670,2672,2674],{"class":126,"line":445},[124,2671,850],{"class":134},[124,2673,877],{"class":169},[124,2675,2676],{"class":359},"      # ❌ × 3 nodes = 12 mounts\n",[10,2678,2679,2682],{},[25,2680,2681],{},"After",", the custom image carries the config, compose keeps only the data:",[115,2684,2686],{"className":336,"code":2685,"filename":668,"language":338,"meta":120,"style":120},"services:\n  mongo1:\n    container_name: mongo1\n    build: .                       # local build → in prod: image: \u003Cyour-registry>\u002Fmongo-replica:latest\n    image: mongo-replica:latest\n    hostname: mongo1\n    networks: [ mongo-cluster ]\n    volumes:\n      - mongo1_data:\u002Fdata\u002Fdb\n      - mongo1_config:\u002Fdata\u002Fconfigdb\n    environment:\n      MONGO_INITDB_ROOT_USERNAME: '${MONGO_ADMIN_USER:-admin}'\n      MONGO_INITDB_ROOT_PASSWORD: '${MONGO_ADMIN_PASSWD:-veryStringPassword}'\n\n  # mongo2 and mongo3: strictly identical, only the suffix changes\n  mongo-init:\n    container_name: mongo-init\n    image: mongo-replica:latest\n    restart: \"no\"\n    entrypoint: [\"\u002Fbin\u002Fbash\", \"\u002Finit.sh\"]\n    networks: [ mongo-cluster ]\n    depends_on: [ mongo1, mongo2, mongo3 ]\n    environment:\n      MONGO_ROOT_USERNAME: '${MONGO_ADMIN_USER:-admin}'\n      MONGO_ROOT_PASSWORD: '${MONGO_ADMIN_PASSWD:-veryStringPassword}'\n      MONGO_DB_USER: '${DB_USERNAME:-okydook}'\n      MONGO_DB_PASSWORD: '${DB_PASSWORD:-mypassword}'\n",[36,2687,2688,2694,2700,2708,2719,2727,2735,2747,2753,2759,2765,2771,2783,2795,2799,2804,2810,2818,2826,2838,2862,2874,2894,2900,2912,2924,2936],{"__ignoreMap":120},[124,2689,2690,2692],{"class":126,"line":127},[124,2691,896],{"class":148},[124,2693,348],{"class":134},[124,2695,2696,2698],{"class":126,"line":142},[124,2697,903],{"class":148},[124,2699,348],{"class":134},[124,2701,2702,2704,2706],{"class":126,"line":189},[124,2703,910],{"class":148},[124,2705,135],{"class":134},[124,2707,915],{"class":169},[124,2709,2710,2712,2714,2716],{"class":126,"line":216},[124,2711,920],{"class":148},[124,2713,135],{"class":134},[124,2715,925],{"class":154},[124,2717,2718],{"class":359},"                       # local build → in prod: image: \u003Cyour-registry>\u002Fmongo-replica:latest\n",[124,2720,2721,2723,2725],{"class":126,"line":243},[124,2722,933],{"class":148},[124,2724,135],{"class":134},[124,2726,938],{"class":169},[124,2728,2729,2731,2733],{"class":126,"line":437},[124,2730,943],{"class":148},[124,2732,135],{"class":134},[124,2734,915],{"class":169},[124,2736,2737,2739,2741,2743,2745],{"class":126,"line":445},[124,2738,952],{"class":148},[124,2740,135],{"class":134},[124,2742,498],{"class":134},[124,2744,959],{"class":169},[124,2746,962],{"class":134},[124,2748,2749,2751],{"class":126,"line":453},[124,2750,967],{"class":148},[124,2752,348],{"class":134},[124,2754,2755,2757],{"class":126,"line":461},[124,2756,974],{"class":134},[124,2758,853],{"class":169},[124,2760,2761,2763],{"class":126,"line":466},[124,2762,974],{"class":134},[124,2764,860],{"class":169},[124,2766,2767,2769],{"class":126,"line":472},[124,2768,991],{"class":148},[124,2770,348],{"class":134},[124,2772,2773,2775,2777,2779,2781],{"class":126,"line":481},[124,2774,998],{"class":148},[124,2776,135],{"class":134},[124,2778,166],{"class":134},[124,2780,1005],{"class":169},[124,2782,1008],{"class":134},[124,2784,2785,2787,2789,2791,2793],{"class":126,"line":487},[124,2786,1013],{"class":148},[124,2788,135],{"class":134},[124,2790,166],{"class":134},[124,2792,1020],{"class":169},[124,2794,1008],{"class":134},[124,2796,2797],{"class":126,"line":492},[124,2798,417],{"emptyLinePlaceholder":416},[124,2800,2801],{"class":126,"line":1029},[124,2802,2803],{"class":359},"  # mongo2 and mongo3: strictly identical, only the suffix changes\n",[124,2805,2806,2808],{"class":126,"line":1035},[124,2807,1043],{"class":148},[124,2809,348],{"class":134},[124,2811,2812,2814,2816],{"class":126,"line":1040},[124,2813,910],{"class":148},[124,2815,135],{"class":134},[124,2817,1055],{"class":169},[124,2819,2820,2822,2824],{"class":126,"line":1048},[124,2821,933],{"class":148},[124,2823,135],{"class":134},[124,2825,938],{"class":169},[124,2827,2828,2830,2832,2834,2836],{"class":126,"line":1058},[124,2829,1070],{"class":148},[124,2831,135],{"class":134},[124,2833,1075],{"class":134},[124,2835,1078],{"class":169},[124,2837,1081],{"class":134},[124,2839,2840,2842,2844,2846,2848,2850,2852,2854,2856,2858,2860],{"class":126,"line":1067},[124,2841,1087],{"class":148},[124,2843,135],{"class":134},[124,2845,498],{"class":134},[124,2847,1094],{"class":134},[124,2849,1097],{"class":169},[124,2851,1094],{"class":134},[124,2853,158],{"class":134},[124,2855,1075],{"class":134},[124,2857,1106],{"class":169},[124,2859,1094],{"class":134},[124,2861,246],{"class":134},[124,2863,2864,2866,2868,2870,2872],{"class":126,"line":1084},[124,2865,952],{"class":148},[124,2867,135],{"class":134},[124,2869,498],{"class":134},[124,2871,959],{"class":169},[124,2873,962],{"class":134},[124,2875,2876,2878,2880,2882,2884,2886,2888,2890,2892],{"class":126,"line":1113},[124,2877,1129],{"class":148},[124,2879,135],{"class":134},[124,2881,498],{"class":134},[124,2883,1136],{"class":169},[124,2885,158],{"class":134},[124,2887,1141],{"class":169},[124,2889,158],{"class":134},[124,2891,1146],{"class":169},[124,2893,962],{"class":134},[124,2895,2896,2898],{"class":126,"line":1126},[124,2897,991],{"class":148},[124,2899,348],{"class":134},[124,2901,2902,2904,2906,2908,2910],{"class":126,"line":1151},[124,2903,1161],{"class":148},[124,2905,135],{"class":134},[124,2907,166],{"class":134},[124,2909,1005],{"class":169},[124,2911,1008],{"class":134},[124,2913,2914,2916,2918,2920,2922],{"class":126,"line":1158},[124,2915,1175],{"class":148},[124,2917,135],{"class":134},[124,2919,166],{"class":134},[124,2921,1020],{"class":169},[124,2923,1008],{"class":134},[124,2925,2926,2928,2930,2932,2934],{"class":126,"line":1172},[124,2927,1189],{"class":148},[124,2929,135],{"class":134},[124,2931,166],{"class":134},[124,2933,1196],{"class":169},[124,2935,1008],{"class":134},[124,2937,2938,2940,2942,2944,2946],{"class":126,"line":1186},[124,2939,1204],{"class":148},[124,2941,135],{"class":134},[124,2943,166],{"class":134},[124,2945,1211],{"class":169},[124,2947,1008],{"class":134},[10,2949,2950,2951,2953,2954,2956,2957,17],{},"No keyfile, no ",[36,2952,48],{},", no mounted scripts: they live in the image. That's the whole point. The full ",[36,2955,668],{}," — all three nodes, networks, volumes — is in the ",[51,2958,2960],{"href":53,"rel":2959},[55],"lab repo",[62,2962,2964],{"id":2963},"orchestrating-the-bootstrap-the-real-hidden-headache","Orchestrating the bootstrap: the real hidden headache",[10,2966,2967,2968,2971,2972,2974],{},"Building the image isn't enough. A replica set doesn't initialize itself: you have to run, ",[25,2969,2970],{},"once",", the ",[36,2973,1240],{}," command that tells the three nodes they form a cluster. And you have to do it at the right moment — neither before the nodes are ready, nor twice.",[10,2976,2977,2978,2980],{},"I handed that job to a dedicated ephemeral container, ",[36,2979,1247],{},", which does only that then shuts down. Its script patiently waits for MongoDB to respond before acting:",[115,2982,2983],{"className":280,"code":1251,"filename":42,"language":282,"meta":120,"style":120},[36,2984,2985,3013,3035,3045,3051],{"__ignoreMap":120},[124,2986,2987,2989,2991,2993,2995,2997,2999,3001,3003,3005,3007,3009,3011],{"class":126,"line":127},[124,2988,1259],{"class":1258},[124,2990,1262],{"class":130},[124,2992,1265],{"class":169},[124,2994,1268],{"class":169},[124,2996,1271],{"class":169},[124,2998,1075],{"class":134},[124,3000,1276],{"class":138},[124,3002,1094],{"class":134},[124,3004,1281],{"class":169},[124,3006,1075],{"class":134},[124,3008,1286],{"class":138},[124,3010,1094],{"class":134},[124,3012,724],{"class":138},[124,3014,3015,3017,3019,3021,3023,3025,3027,3029,3031,3033],{"class":126,"line":142},[124,3016,1295],{"class":169},[124,3018,1298],{"class":169},[124,3020,1301],{"class":169},[124,3022,1075],{"class":134},[124,3024,1306],{"class":169},[124,3026,1094],{"class":134},[124,3028,301],{"class":134},[124,3030,1313],{"class":169},[124,3032,1316],{"class":134},[124,3034,1319],{"class":1258},[124,3036,3037,3039,3041,3043],{"class":126,"line":189},[124,3038,1325],{"class":1324},[124,3040,1075],{"class":134},[124,3042,1330],{"class":169},[124,3044,1081],{"class":134},[124,3046,3047,3049],{"class":126,"line":216},[124,3048,1337],{"class":130},[124,3050,1340],{"class":154},[124,3052,3053],{"class":126,"line":243},[124,3054,1345],{"class":1258},[10,3056,3057],{},"Two details matter enormously here, because they make the difference between an automated deployment and one you have to babysit:",[10,3059,3060,3063],{},[25,3061,3062],{},"Initialization is idempotent."," The script first checks whether the replica set already exists before trying to create it:",[115,3065,3066],{"className":117,"code":1357,"filename":45,"language":119,"meta":120,"style":120},[36,3067,3068,3074,3092,3112,3122,3138,3142,3152,3168],{"__ignoreMap":120},[124,3069,3070,3072],{"class":126,"line":127},[124,3071,1364],{"class":1258},[124,3073,1367],{"class":134},[124,3075,3076,3078,3080,3082,3084,3086,3088,3090],{"class":126,"line":142},[124,3077,1373],{"class":1372},[124,3079,1376],{"class":138},[124,3081,1379],{"class":134},[124,3083,1382],{"class":138},[124,3085,17],{"class":134},[124,3087,1387],{"class":1324},[124,3089,1390],{"class":148},[124,3091,1393],{"class":134},[124,3093,3094,3096,3098,3100,3102,3104,3106,3108,3110],{"class":126,"line":189},[124,3095,1398],{"class":1258},[124,3097,613],{"class":148},[124,3099,1387],{"class":138},[124,3101,17],{"class":134},[124,3103,1407],{"class":138},[124,3105,1410],{"class":134},[124,3107,198],{"class":154},[124,3109,1415],{"class":148},[124,3111,1418],{"class":134},[124,3113,3114,3116,3118,3120],{"class":126,"line":216},[124,3115,1423],{"class":138},[124,3117,1379],{"class":134},[124,3119,1429],{"class":1428},[124,3121,1393],{"class":134},[124,3123,3124,3126,3128,3130,3132,3134,3136],{"class":126,"line":243},[124,3125,1436],{"class":1324},[124,3127,1439],{"class":148},[124,3129,1094],{"class":134},[124,3131,1444],{"class":169},[124,3133,1094],{"class":134},[124,3135,1449],{"class":148},[124,3137,1393],{"class":134},[124,3139,3140],{"class":126,"line":437},[124,3141,1456],{"class":134},[124,3143,3144,3146,3148,3150],{"class":126,"line":445},[124,3145,1461],{"class":134},[124,3147,1464],{"class":1258},[124,3149,1467],{"class":138},[124,3151,1418],{"class":134},[124,3153,3154,3156,3158,3160,3162,3164,3166],{"class":126,"line":453},[124,3155,1474],{"class":1324},[124,3157,1439],{"class":148},[124,3159,1094],{"class":134},[124,3161,1481],{"class":169},[124,3163,1094],{"class":134},[124,3165,1449],{"class":148},[124,3167,1393],{"class":134},[124,3169,3170],{"class":126,"line":461},[124,3171,1492],{"class":134},[10,3173,3174,3175,3177],{},"Without that guard, restarting the stack would try to re-initialize a live cluster — and crash it. With it, you can run ",[36,3176,680],{}," as many times as you want: if the cluster exists, the script notices and moves on. That's what makes the deployment safely replayable.",[10,3179,3180,3183,3184,3186,3187,3189],{},[25,3181,3182],{},"The application user is created right after",", with the strict rights it needs and nothing more — ",[36,3185,1507],{}," on the single ",[36,3188,1511],{}," database, not admin access:",[115,3191,3192],{"className":117,"code":1515,"filename":45,"language":119,"meta":120,"style":120},[36,3193,3194,3206,3224,3242,3278],{"__ignoreMap":120},[124,3195,3196,3198,3200,3202,3204],{"class":126,"line":127},[124,3197,1522],{"class":138},[124,3199,17],{"class":134},[124,3201,1527],{"class":1324},[124,3203,1439],{"class":138},[124,3205,1418],{"class":134},[124,3207,3208,3210,3212,3214,3216,3218,3220,3222],{"class":126,"line":142},[124,3209,1536],{"class":148},[124,3211,135],{"class":134},[124,3213,1541],{"class":138},[124,3215,17],{"class":134},[124,3217,1546],{"class":138},[124,3219,17],{"class":134},[124,3221,1551],{"class":138},[124,3223,1554],{"class":134},[124,3225,3226,3228,3230,3232,3234,3236,3238,3240],{"class":126,"line":189},[124,3227,1559],{"class":148},[124,3229,135],{"class":134},[124,3231,1541],{"class":138},[124,3233,17],{"class":134},[124,3235,1546],{"class":138},[124,3237,17],{"class":134},[124,3239,1572],{"class":138},[124,3241,1554],{"class":134},[124,3243,3244,3246,3248,3250,3252,3254,3256,3258,3260,3262,3264,3266,3268,3270,3272,3274,3276],{"class":126,"line":216},[124,3245,1579],{"class":148},[124,3247,135],{"class":134},[124,3249,498],{"class":138},[124,3251,1586],{"class":134},[124,3253,1589],{"class":148},[124,3255,135],{"class":134},[124,3257,166],{"class":134},[124,3259,1507],{"class":169},[124,3261,173],{"class":134},[124,3263,158],{"class":134},[124,3265,1602],{"class":148},[124,3267,135],{"class":134},[124,3269,166],{"class":134},[124,3271,1511],{"class":169},[124,3273,173],{"class":134},[124,3275,1613],{"class":134},[124,3277,246],{"class":138},[124,3279,3280,3282,3284],{"class":126,"line":243},[124,3281,1461],{"class":134},[124,3283,1449],{"class":138},[124,3285,1393],{"class":134},[10,3287,3288,3289,3291,3292,3295,3296,135],{},"Finally, Compose chains dependencies cleanly: the admin UI doesn't just wait for ",[36,3290,1247],{}," to ",[14,3293,3294],{},"start",", but to finish ",[25,3297,3298],{},"successfully",[115,3300,3301],{"className":336,"code":1639,"filename":668,"language":338,"meta":120,"style":120},[36,3302,3303,3309,3315],{"__ignoreMap":120},[124,3304,3305,3307],{"class":126,"line":127},[124,3306,1646],{"class":148},[124,3308,348],{"class":134},[124,3310,3311,3313],{"class":126,"line":142},[124,3312,1043],{"class":148},[124,3314,348],{"class":134},[124,3316,3317,3319,3321],{"class":126,"line":189},[124,3318,1659],{"class":148},[124,3320,135],{"class":134},[124,3322,1664],{"class":169},[10,3324,3325],{},"That condition is subtle but essential: it guarantees you never expose an interface on a half-initialized cluster.",[62,3327,3329],{"id":3328},"the-final-piece-connect-to-the-cluster-not-a-node","The final piece: connect to the cluster, not a node",[10,3331,3332,3333,3335,3336,3338],{},"All this work would be wasted if the application pointed at a single node. Because if it only knows ",[36,3334,89],{}," and ",[36,3337,89],{}," goes down, it's blind to the freshly elected new primary — high availability becomes useless.",[10,3340,3341],{},"So the connection string lists all three nodes and explicitly names the replica set:",[115,3343,3345],{"className":3344,"code":1688,"language":1689},[1687],[36,3346,1688],{"__ignoreMap":120},[30,3348,3349],{},[10,3350,3351,3352,1700,3354,3357,3358,3360,3361,3363,3364,3366],{},"The three nodes answer on port ",[36,3353,1699],{},[14,3355,3356],{},"inside"," the Docker network — these are the ",[36,3359,1707],{}," network hostnames, not host-mapped ports. Listing ",[36,3362,1711],{}," or ",[36,3365,1715],{}," here is a common mistake: those ports only exist outside the network.",[10,3368,2359,3369,3371],{},[36,3370,1722],{}," parameter is what changes everything. With it, the MongoDB driver doesn't treat the three addresses as three independent databases: it understands it's a cluster, discovers its topology, tracks who's primary at any instant, and automatically routes writes to the right node — including after a failover. The application never even knows an election happened. That's exactly the intended effect.",[62,3373,3375],{"id":3374},"verifying-it-all-runs","Verifying it all runs",[10,3377,3378,3379,3381,3382,3384],{},"Once the stack is up (",[36,3380,1733],{},"), two checks are enough. First the keyfile — the thing that crashed the volume-mounted setup — which must be ",[36,3383,320],{}," inside the container:",[115,3386,3387],{"className":280,"code":1740,"language":282,"meta":120,"style":120},[36,3388,3389,3405],{"__ignoreMap":120},[124,3390,3391,3393,3395,3397,3399,3401,3403],{"class":126,"line":127},[124,3392,650],{"class":130},[124,3394,1749],{"class":169},[124,3396,1752],{"class":169},[124,3398,1136],{"class":169},[124,3400,1757],{"class":169},[124,3402,1760],{"class":169},[124,3404,565],{"class":169},[124,3406,3407],{"class":126,"line":142},[124,3408,1767],{"class":359},[10,3410,3411,3412,3414,3415,3417,3418,39,3420,1783,3422,3417,3424,3426,3427,17],{},"Then cluster state: ",[36,3413,1773],{}," should show ",[36,3416,89],{}," as ",[36,3419,1780],{},[36,3421,92],{},[36,3423,95],{},[36,3425,1788],{},", all with ",[36,3428,1792],{},[115,3430,3431],{"className":280,"code":1795,"language":282,"meta":120,"style":120},[36,3432,3433,3455],{"__ignoreMap":120},[124,3434,3435,3437,3439,3441,3443,3445,3447,3449,3451,3453],{"class":126,"line":127},[124,3436,650],{"class":130},[124,3438,1749],{"class":169},[124,3440,1752],{"class":169},[124,3442,1136],{"class":169},[124,3444,1262],{"class":169},[124,3446,1271],{"class":169},[124,3448,1298],{"class":169},[124,3450,1281],{"class":169},[124,3452,1818],{"class":169},[124,3454,724],{"class":138},[124,3456,3457,3459,3461,3463,3465,3467],{"class":126,"line":142},[124,3458,1295],{"class":169},[124,3460,1298],{"class":169},[124,3462,1301],{"class":169},[124,3464,1075],{"class":134},[124,3466,1773],{"class":169},[124,3468,1081],{"class":134},[10,3470,3471,3472,17],{},"The other diagnostic commands (app-user auth, ping) are listed in the README of the ",[51,3473,2960],{"href":53,"rel":3474},[55],[62,3476,3478],{"id":3477},"what-this-journey-taught-me","What this journey taught me",[10,3480,3481,3482],{},"The starting problem looked like \"MongoDB must be highly available.\" The real problem, the one that cost me time, was elsewhere: ",[25,3483,3484],{},"making a distributed setup reproducible across environments that don't look alike.",[10,3486,3487,3488,3490,3491,3494],{},"The lesson I keep: when a configuration is fragile because of the environment — permissions, paths, host state — the right answer isn't to document it better or add defensive ",[36,3489,370],{},"s. It's to ",[25,3492,3493],{},"take the environment out of the equation",". Bake the config into the image, make init idempotent, declare dependencies explicitly: each of those decisions turns a \"be careful\" step into an \"it just works\" step.",[10,3496,3497,3498,17],{},"And maybe that's the real meaning of infrastructure as code: not just writing infra into files, but methodically eliminating every place where a human has to ",[14,3499,3500],{},"be careful",[10,3502,3503,3504,3507],{},"In a next article, I'll take on the logical sequel: ",[25,3505,3506],{},"how to back up and restore this MongoDB cluster in a containerized environment",". Because a highly available cluster with no backup strategy is just a fancier way to lose your data.",[62,3509,3511],{"id":3510},"references","References",[309,3513,3514,3522,3528,3534,3540,3545],{},[312,3515,3516],{},[51,3517,3519,3520],{"href":53,"rel":3518},[55],"Full lab: ",[36,3521,1888],{},[312,3523,3524],{},[51,3525,3527],{"href":1893,"rel":3526},[55],"MongoDB replica set keyfile authentication",[312,3529,3530],{},[51,3531,3533],{"href":1900,"rel":3532},[55],"MongoDB Replica Set documentation",[312,3535,3536],{},[51,3537,3539],{"href":1907,"rel":3538},[55],"Docker multi-platform builds",[312,3541,3542],{},[51,3543,1916],{"href":1914,"rel":3544},[55],[312,3546,3547],{},[51,3548,3550],{"href":1921,"rel":3549},[55],"Docker Compose documentation",[1925,3552,1927],{},{"title":120,"searchDepth":142,"depth":142,"links":3554},[3555,3556,3557,3558,3561,3562,3563,3564,3565,3566],{"id":2002,"depth":142,"text":2003},{"id":2020,"depth":142,"text":2021},{"id":2152,"depth":142,"text":2153},{"id":2263,"depth":142,"text":2264,"children":3559},[3560],{"id":2468,"depth":189,"text":2469},{"id":2611,"depth":142,"text":2612},{"id":2963,"depth":142,"text":2964},{"id":3328,"depth":142,"text":3329},{"id":3374,"depth":142,"text":3375},{"id":3477,"depth":142,"text":3478},{"id":3510,"depth":142,"text":3511},"A field report: deploying a 3-node MongoDB replica set with Docker, and why baking the config into a custom image instead of mounting volumes makes deployments reproducible across environments that don't look alike.",{},"\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker",{"title":1961,"description":3567},"databases\u002Fmongo\u002F1.set-up-mongodb-replica-with-docker",[1952,1953,1954,1955],[1957,650,327],"pyYWlzSZkSan2YqkkxzKE_lqHdrhKnBjtZ_9AVItqe0",[3576,3579],{"title":3577,"path":3578},"Mon parcours vers la certification AWS Cloud Practitioner","\u002Fcloud\u002Faws\u002Fhow-i-passed-clf-exam.fr",{"title":3580,"path":3581},"Sauvegarder et restaurer MongoDB dans un environnement Docker","\u002Fdatabases\u002Fmongo\u002Fbackup-and-restore-mongodb-in-docker-environment.fr",[3583,3590,3604],{"path":3581,"title":3580,"description":3584,"date":3585,"tags":3586,"topics":3589},"Comment créer une sauvegarde complète d'une base MongoDB qui tourne dans un conteneur Docker et restaurer la sauvegarde dans un nouveau conteneur MongoDB.","2024-07-18",[1952,1954,1955,3587,3588],"Backup","Restore",[1957,650,327],{"path":3591,"title":3592,"description":3593,"date":3594,"tags":3595,"topics":3601},"\u002Fdocker\u002Fdeploy-expressjs-api-using-docker.fr","Déployer une API Express.js avec Docker","Comment déployer une API REST Express.js avec Docker, avec un focus sur les performances MongoDB, les builds Docker multi-stage, Jest pour les tests, et Nginx comme reverse proxy.","2024-04-11",[3596,3597,3598,3599,3600,1954],"Express.JS","API","Node.js","Mongo","CI\u002FCD",[650,3602,3603],"backend","devops",{"path":3605,"title":3606,"description":3607,"date":3608,"tags":3609,"topics":3616},"\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster.fr","Mettre en place un cluster PostgreSQL haute disponibilité à 3 nœuds avec Patroni et etcd","Comment construire un cluster PostgreSQL à 3 nœuds en haute disponibilité avec bascule automatique grâce à Patroni, etcd, HAProxy et Keepalived. L'architecture, les prérequis et le déploiement pas à pas pour qu'une panne de nœud soit gérée sans rien changer côté applications.","2026-06-25",[3610,3611,3612,3613,3614,3615],"PostgreSQL","High-Availability","Patroni","etcd","HAProxy","Keepalived",[1957,3617],"postgresql",1783419618812]