[{"data":1,"prerenderedAt":3053},["ShallowReactive",2],{"article-en-\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster":3,"article-sibling-en-\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster":1628,"surround-en-\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster":3025,"related-en-\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster":3032},{"id":4,"title":5,"body":6,"date":1612,"description":1613,"extension":1614,"img":1615,"meta":1616,"navigation":544,"path":1617,"seo":1618,"slug":1619,"stem":1620,"tags":1621,"topics":1623,"__hash__":1627},"content\u002Fdatabases\u002Fpostgres\u002F1.setup-postgres-ha-cluster.md","Setting Up a PostgreSQL HA Cluster with Patroni and etcd",{"type":7,"value":8,"toc":1588},"minimark",[9,14,27,35,42,45,50,55,58,86,89,93,181,185,200,205,209,220,238,242,245,272,274,278,281,312,319,352,355,359,362,376,397,417,433,440,444,447,476,479,757,760,778,782,792,796,803,819,830,844,846,850,861,865,891,904,914,927,931,938,987,991,997,1045,1054,1058,1061,1085,1091,1214,1224,1228,1231,1325,1374,1378,1381,1446,1456,1459,1479,1481,1485,1537,1553,1558,1584],[10,11,13],"h1",{"id":12},"building-a-high-availability-postgresql-cluster-patroni-etcd-haproxy-keepalived","Building a High-Availability PostgreSQL Cluster (Patroni + etcd + HAProxy + Keepalived)",[15,16,17,18,22,23,26],"p",{},"PostgreSQL on its own is a single point of failure: if the server goes down, the application goes down with it. This article shows how to stand up a 3-node cluster that survives the loss of a machine ",[19,20,21],"strong",{},"with no human intervention"," and ",[19,24,25],{},"without changing the connection string on the application side",".",[15,28,29,30,34],{},"The goal isn't to copy-paste commands blindly, but to understand ",[31,32,33],"em",{},"why"," each building block exists, so you can adapt the whole thing to your own network.",[15,36,37,38,41],{},"The result: a single virtual IP (the ",[31,39,40],{},"VIP",") that applications connect to as if it were a regular database. Behind it, three servers watch each other continuously; if the primary fails, a replica is promoted within seconds and the VIP fails over automatically.",[43,44],"hr",{},[46,47,49],"h2",{"id":48},"part-1-the-architecture-and-why-this-stack","Part 1 — The architecture and why this stack",[51,52,54],"h3",{"id":53},"the-high-availability-problem","The high-availability problem",[15,56,57],{},"For a database to stay available when a server goes down, you have to solve four distinct problems:",[59,60,61,68,74,80],"ol",{},[62,63,64,67],"li",{},[19,65,66],{},"Store the data more than once"," → PostgreSQL replication (one primary writes, replicas follow).",[62,69,70,73],{},[19,71,72],{},"Decide who the primary is",", and change it without conflict when it fails → an orchestrator (Patroni).",[62,75,76,79],{},[19,77,78],{},"Agree on that decision across several nodes",", even during a network partition → a consensus store (etcd).",[62,81,82,85],{},[19,83,84],{},"Route clients to the right server"," without them knowing who is primary → a router (HAProxy) + a single address that follows the service (VIP via Keepalived).",[15,87,88],{},"Each component solves exactly one of these problems. That's why there are four of them.",[51,90,92],{"id":91},"the-four-components","The four components",[94,95,96,112],"table",{},[97,98,99],"thead",{},[100,101,102,106,109],"tr",{},[103,104,105],"th",{},"Component",[103,107,108],{},"Role",[103,110,111],{},"What it solves",[113,114,115,129,142,155,168],"tbody",{},[100,116,117,123,126],{},[118,119,120],"td",{},[19,121,122],{},"PostgreSQL",[118,124,125],{},"The database engine",[118,127,128],{},"Stores and replicates the data",[100,130,131,136,139],{},[118,132,133],{},[19,134,135],{},"etcd",[118,137,138],{},"Distributed key-value store (the \"DCS\")",[118,140,141],{},"Shared source of truth: who the leader is",[100,143,144,149,152],{},[118,145,146],{},[19,147,148],{},"Patroni",[118,150,151],{},"Orchestrator on top of PostgreSQL",[118,153,154],{},"Bootstrap, replication, election, failover",[100,156,157,162,165],{},[118,158,159],{},[19,160,161],{},"HAProxy",[118,163,164],{},"TCP proxy",[118,166,167],{},"Routes each connection to the current primary",[100,169,170,175,178],{},[118,171,172],{},[19,173,174],{},"Keepalived",[118,176,177],{},"Virtual IP management (VRRP)",[118,179,180],{},"Floats a single IP onto a healthy node",[51,182,184],{"id":183},"why-three-nodes-and-not-two","Why three nodes, and not two",[15,186,187,188,191,192,195,196,199],{},"etcd (like any consensus system) decides by ",[19,189,190],{},"majority",". With 3 nodes, the majority is 2: the cluster keeps working if ",[19,193,194],{},"one"," node goes down. With 2 nodes, losing one node loses the majority — nobody can decide who the primary is anymore, and the cluster freezes as a safety measure (to avoid ",[31,197,198],{},"split-brain",", two primaries diverging).",[15,201,202],{},[19,203,204],{},"Simple rule: an odd number of nodes. Three is the useful minimum.",[51,206,208],{"id":207},"the-path-of-a-request","The path of a request",[210,211,216],"pre",{"className":212,"code":214,"language":215},[213],"language-text","Application\n    │  connects to the VIP (e.g. 192.168.11.50:5432)\n    ▼\nKeepalived  ── the VIP is mounted on the healthy, prioritized node\n    ▼\nHAProxy     ── probes all 3 nodes, only forwards to the PRIMARY\n    ▼\nPostgreSQL (primary)  ←── replicate ──  PostgreSQL (replicas ×2)\n    ▲\nPatroni     ── monitors, and via etcd decides who is primary\n","text",[217,218,214],"code",{"__ignoreMap":219},"",[15,221,222,223,226,227,230,231,234,235,237],{},"Key point: ",[19,224,225],{},"HAProxy does not guess"," who the primary is. It queries Patroni's REST API on each node (",[217,228,229],{},"GET \u002Fprimary","). Only the node that is actually primary answers ",[217,232,233],{},"200",". Replicas answer something else and are discarded. When a failover happens, it's the former replica — now primary — that starts answering ",[217,236,233],{},", and HAProxy redirects traffic automatically, with no reconfiguration.",[51,239,241],{"id":240},"security-tls-everywhere","Security: TLS everywhere",[15,243,244],{},"All internal traffic is encrypted and authenticated with X.509 certificates:",[246,247,248,253,262],"ul",{},[62,249,250,252],{},[19,251,135],{}," requires mutual certificates (peer-to-peer and client). A private CA signs one certificate per node.",[62,254,255,257,258,261],{},[19,256,122],{}," uses a shared certificate whose ",[31,259,260],{},"SANs"," cover the VIP + the 3 IPs + localhost, so the connection is valid no matter which node is serving.",[62,263,264,267,268,271],{},[19,265,266],{},"pg_hba"," only allows ",[217,269,270],{},"hostssl"," connections (never plaintext), restricted to the cluster's subnet.",[43,273],{},[46,275,277],{"id":276},"part-2-preparation-network-configuration-preflight","Part 2 — Preparation: network, configuration, preflight",[15,279,280],{},"The entire deployment comes down to two scripts and one configuration file:",[246,282,283,293,302],{},[62,284,285,288,289,292],{},[217,286,287],{},"cluster.env"," — ",[19,290,291],{},"all"," the variables (IPs, ports, secrets, PG version). It's the only file you edit.",[62,294,295,298,299,26],{},[217,296,297],{},"preflight.sh"," — checks the machine is ready, ",[19,300,301],{},"writes nothing",[62,303,304,307,308,311],{},[217,305,306],{},"deploy.sh"," — installs and configures everything. ",[19,309,310],{},"Idempotent",": re-runnable without breaking an existing cluster.",[15,313,314,315,318],{},"All three files live in the blog's labs repo, under the ",[217,316,317],{},"postgresql-ha-cluster"," subfolder. Grab them:",[210,320,324],{"className":321,"code":322,"language":323,"meta":219,"style":219},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone https:\u002F\u002Fgithub.com\u002Fdenisakp\u002Fdev-life-labs\ncd dev-life-labs\u002Fpostgresql-ha-cluster\n","bash",[217,325,326,342],{"__ignoreMap":219},[327,328,331,335,339],"span",{"class":329,"line":330},"line",1,[327,332,334],{"class":333},"sBMFI","git",[327,336,338],{"class":337},"sfazB"," clone",[327,340,341],{"class":337}," https:\u002F\u002Fgithub.com\u002Fdenisakp\u002Fdev-life-labs\n",[327,343,345,349],{"class":329,"line":344},2,[327,346,348],{"class":347},"s2Zo4","cd",[327,350,351],{"class":337}," dev-life-labs\u002Fpostgresql-ha-cluster\n",[15,353,354],{},"This is the folder you'll later copy to each node (see Part 3).",[51,356,358],{"id":357},"network-prerequisites","Network prerequisites",[15,360,361],{},"First and foremost, you need:",[246,363,364,370],{},[62,365,366,369],{},[19,367,368],{},"3 servers"," running Debian\u002FUbuntu on the same subnet, with static IPs.",[62,371,372,375],{},[19,373,374],{},"One free IP"," in that subnet for the VIP — check it:",[210,377,379],{"className":321,"code":378,"language":323,"meta":219,"style":219},"ping -c2 192.168.11.50    # must NOT respond → the VIP is free\n",[217,380,381],{"__ignoreMap":219},[327,382,383,386,389,393],{"class":329,"line":330},[327,384,385],{"class":333},"ping",[327,387,388],{"class":337}," -c2",[327,390,392],{"class":391},"sbssI"," 192.168.11.50",[327,394,396],{"class":395},"sHwdD","    # must NOT respond → the VIP is free\n",[246,398,399],{},[62,400,401,402,405,406,409,410,409,413,416],{},"The ",[19,403,404],{},"network interface name"," of each server (often ",[217,407,408],{},"ens3",", ",[217,411,412],{},"enp0s3",[217,414,415],{},"eth0","…):",[210,418,420],{"className":321,"code":419,"language":323,"meta":219,"style":219},"ip link show\n",[217,421,422],{"__ignoreMap":219},[327,423,424,427,430],{"class":329,"line":330},[327,425,426],{"class":333},"ip",[327,428,429],{"class":337}," link",[327,431,432],{"class":337}," show\n",[15,434,435,436,439],{},"If the interface (",[217,437,438],{},"IFACE",") is wrong, the VIP will never come up. That's mistake number one.",[51,441,443],{"id":442},"the-configuration-file","The configuration file",[15,445,446],{},"Start from the template:",[210,448,450],{"className":321,"code":449,"language":323,"meta":219,"style":219},"cp cluster.env.example cluster.env && chmod 600 cluster.env\n",[217,451,452],{"__ignoreMap":219},[327,453,454,457,460,463,467,470,473],{"class":329,"line":330},[327,455,456],{"class":333},"cp",[327,458,459],{"class":337}," cluster.env.example",[327,461,462],{"class":337}," cluster.env",[327,464,466],{"class":465},"sMK4o"," &&",[327,468,469],{"class":333}," chmod",[327,471,472],{"class":391}," 600",[327,474,475],{"class":337}," cluster.env\n",[15,477,478],{},"The essential fields:",[210,480,484],{"className":481,"code":482,"language":483,"meta":219,"style":219},"language-ini shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Identity\nCLUSTER_NAME=\"postgresql-cluster\"\nPG_VERSION=\"18\"\nETCD_VERSION=\"v3.6.12\"\n\n# Network\nSUBNET=\"192.168.11.0\u002F24\"      # cluster subnet\nVIP=\"192.168.11.50\"           # virtual IP (the applications' entry point)\nIFACE=\"ens3\"                  # interface that will carry the VIP\nAPP_SUBNET=\"192.168.11.0\u002F24\"  # where the clients come from\n\n# Nodes\nDB1_NAME=\"db1\"; DB1_IP=\"192.168.11.45\"   # initial primary, VRRP priority 100\nDB2_NAME=\"db2\"; DB2_IP=\"192.168.11.46\"   # standby, priority 90\nDB3_NAME=\"db3\"; DB3_IP=\"192.168.11.47\"   # standby, priority 80\n\n# Secrets — generate each with:  openssl rand -base64 24\nPG_SUPERUSER_PASSWORD=\"...\"\nPG_REPLICATOR_PASSWORD=\"...\"\nPATRONI_RESTAPI_PASSWORD=\"...\"\nVRRP_AUTH_PASS=\"........\"     # WARNING: max 8 characters (VRRP truncates beyond that)\n","ini",[217,485,486,491,509,524,539,546,552,570,587,603,620,625,631,649,667,685,690,696,711,725,739],{"__ignoreMap":219},[327,487,488],{"class":329,"line":330},[327,489,490],{"class":395},"# Identity\n",[327,492,493,497,500,503,506],{"class":329,"line":344},[327,494,496],{"class":495},"swJcz","CLUSTER_NAME",[327,498,499],{"class":465},"=",[327,501,502],{"class":465},"\"",[327,504,505],{"class":337},"postgresql-cluster",[327,507,508],{"class":465},"\"\n",[327,510,512,515,517,519,522],{"class":329,"line":511},3,[327,513,514],{"class":495},"PG_VERSION",[327,516,499],{"class":465},[327,518,502],{"class":465},[327,520,521],{"class":337},"18",[327,523,508],{"class":465},[327,525,527,530,532,534,537],{"class":329,"line":526},4,[327,528,529],{"class":495},"ETCD_VERSION",[327,531,499],{"class":465},[327,533,502],{"class":465},[327,535,536],{"class":337},"v3.6.12",[327,538,508],{"class":465},[327,540,542],{"class":329,"line":541},5,[327,543,545],{"emptyLinePlaceholder":544},true,"\n",[327,547,549],{"class":329,"line":548},6,[327,550,551],{"class":395},"# Network\n",[327,553,555,558,560,562,565,567],{"class":329,"line":554},7,[327,556,557],{"class":495},"SUBNET",[327,559,499],{"class":465},[327,561,502],{"class":465},[327,563,564],{"class":337},"192.168.11.0\u002F24",[327,566,502],{"class":465},[327,568,569],{"class":395},"      # cluster subnet\n",[327,571,573,575,577,579,582,584],{"class":329,"line":572},8,[327,574,40],{"class":495},[327,576,499],{"class":465},[327,578,502],{"class":465},[327,580,581],{"class":337},"192.168.11.50",[327,583,502],{"class":465},[327,585,586],{"class":395},"           # virtual IP (the applications' entry point)\n",[327,588,590,592,594,596,598,600],{"class":329,"line":589},9,[327,591,438],{"class":495},[327,593,499],{"class":465},[327,595,502],{"class":465},[327,597,408],{"class":337},[327,599,502],{"class":465},[327,601,602],{"class":395},"                  # interface that will carry the VIP\n",[327,604,606,609,611,613,615,617],{"class":329,"line":605},10,[327,607,608],{"class":495},"APP_SUBNET",[327,610,499],{"class":465},[327,612,502],{"class":465},[327,614,564],{"class":337},[327,616,502],{"class":465},[327,618,619],{"class":395},"  # where the clients come from\n",[327,621,623],{"class":329,"line":622},11,[327,624,545],{"emptyLinePlaceholder":544},[327,626,628],{"class":329,"line":627},12,[327,629,630],{"class":395},"# Nodes\n",[327,632,634,637,639,641,644,646],{"class":329,"line":633},13,[327,635,636],{"class":495},"DB1_NAME",[327,638,499],{"class":465},[327,640,502],{"class":465},[327,642,643],{"class":337},"db1",[327,645,502],{"class":465},[327,647,648],{"class":395},"; DB1_IP=\"192.168.11.45\"   # initial primary, VRRP priority 100\n",[327,650,652,655,657,659,662,664],{"class":329,"line":651},14,[327,653,654],{"class":495},"DB2_NAME",[327,656,499],{"class":465},[327,658,502],{"class":465},[327,660,661],{"class":337},"db2",[327,663,502],{"class":465},[327,665,666],{"class":395},"; DB2_IP=\"192.168.11.46\"   # standby, priority 90\n",[327,668,670,673,675,677,680,682],{"class":329,"line":669},15,[327,671,672],{"class":495},"DB3_NAME",[327,674,499],{"class":465},[327,676,502],{"class":465},[327,678,679],{"class":337},"db3",[327,681,502],{"class":465},[327,683,684],{"class":395},"; DB3_IP=\"192.168.11.47\"   # standby, priority 80\n",[327,686,688],{"class":329,"line":687},16,[327,689,545],{"emptyLinePlaceholder":544},[327,691,693],{"class":329,"line":692},17,[327,694,695],{"class":395},"# Secrets — generate each with:  openssl rand -base64 24\n",[327,697,699,702,704,706,709],{"class":329,"line":698},18,[327,700,701],{"class":495},"PG_SUPERUSER_PASSWORD",[327,703,499],{"class":465},[327,705,502],{"class":465},[327,707,708],{"class":337},"...",[327,710,508],{"class":465},[327,712,714,717,719,721,723],{"class":329,"line":713},19,[327,715,716],{"class":495},"PG_REPLICATOR_PASSWORD",[327,718,499],{"class":465},[327,720,502],{"class":465},[327,722,708],{"class":337},[327,724,508],{"class":465},[327,726,728,731,733,735,737],{"class":329,"line":727},20,[327,729,730],{"class":495},"PATRONI_RESTAPI_PASSWORD",[327,732,499],{"class":465},[327,734,502],{"class":465},[327,736,708],{"class":337},[327,738,508],{"class":465},[327,740,742,745,747,749,752,754],{"class":329,"line":741},21,[327,743,744],{"class":495},"VRRP_AUTH_PASS",[327,746,499],{"class":465},[327,748,502],{"class":465},[327,750,751],{"class":337},"........",[327,753,502],{"class":465},[327,755,756],{"class":395},"     # WARNING: max 8 characters (VRRP truncates beyond that)\n",[15,758,759],{},"Two traps that come up every time:",[246,761,762,770],{},[62,763,764,769],{},[19,765,766,768],{},[217,767,744],{}," ≤ 8 characters."," The VRRP protocol truncates silently; two nodes with passwords that differ after truncation won't see each other, and the VIP can split in two.",[62,771,772,777],{},[19,773,774,776],{},[217,775,287],{}," must be identical on all 3 servers",", secrets included. Certificates and authentication rely on it.",[51,779,781],{"id":780},"vrrp-priorities","VRRP priorities",[15,783,784,787,788,791],{},[217,785,786],{},"DB1=100 > DB2=90 > DB3=80",". Keepalived mounts the VIP on the healthy node with the highest priority. db1 has it by default; if it goes down, db2 takes over, then db3. When db1 comes back, it reclaims the VIP (highest priority). These priorities are ",[19,789,790],{},"independent"," of the PostgreSQL role: the VIP follows HAProxy health, not the primary — but since HAProxy only routes to the primary, the result is consistent end to end.",[51,793,795],{"id":794},"the-preflight","The preflight",[15,797,798,799,802],{},"Run on ",[19,800,801],{},"each"," node before deployment:",[210,804,806],{"className":321,"code":805,"language":323,"meta":219,"style":219},"sudo .\u002Fpreflight.sh db1\n",[217,807,808],{"__ignoreMap":219},[327,809,810,813,816],{"class":329,"line":330},[327,811,812],{"class":333},"sudo",[327,814,815],{"class":337}," .\u002Fpreflight.sh",[327,817,818],{"class":337}," db1\n",[15,820,821,822,825,826,829],{},"It checks, without modifying anything: secrets filled in (no ",[217,823,824],{},"CHANGE_ME","), VIP within the subnet, interface present, node IP correct, other nodes reachable, ports free, enough RAM\u002Fdisk, ",[19,827,828],{},"clock synchronized (NTP)"," — critical: clock drift between nodes makes failover unstable.",[246,831,832,838],{},[62,833,834,837],{},[217,835,836],{},"[FAIL]"," = blocking, fix before continuing.",[62,839,840,843],{},[217,841,842],{},"[WARN]"," = worth checking, but deployment can still proceed.",[43,845],{},[46,847,849],{"id":848},"part-3-deployment-verification-failover","Part 3 — Deployment, verification, failover",[15,851,852,853,856,857,860],{},"The order is ",[19,854,855],{},"mandatory",": ",[217,858,859],{},"db1 → certificate distribution → db2 → db3",". db1 generates the CA and everyone's certificates; db2 and db3 cannot start without receiving them.",[51,862,864],{"id":863},"step-1-deploy-db1","Step 1 — Deploy db1",[210,866,868],{"className":321,"code":867,"language":323,"meta":219,"style":219},"sudo .\u002Fpreflight.sh db1\nsudo .\u002Fdeploy.sh db1 --no-upgrade\n",[217,869,870,878],{"__ignoreMap":219},[327,871,872,874,876],{"class":329,"line":330},[327,873,812],{"class":333},[327,875,815],{"class":337},[327,877,818],{"class":337},[327,879,880,882,885,888],{"class":329,"line":344},[327,881,812],{"class":333},[327,883,884],{"class":337}," .\u002Fdeploy.sh",[327,886,887],{"class":337}," db1",[327,889,890],{"class":337}," --no-upgrade\n",[892,893,894],"blockquote",{},[15,895,896,899,900,903],{},[217,897,898],{},"--no-upgrade"," skips the global ",[217,901,902],{},"apt upgrade",". Recommended if the VMs host other services (you don't want to upgrade the whole system along the way). Drop it for a fresh, dedicated server.",[15,905,906,907,909,910,913],{},"What ",[217,908,306],{}," does on db1, in order: targeted update → package installation (PostgreSQL, Patroni, HAProxy, Keepalived) + the etcd binary → ",[19,911,912],{},"generation of the CA and all certificates"," → etcd configuration and start → Patroni, HAProxy, Keepalived configuration → scheduled daily etcd backup.",[15,915,916,917,920,921,22,924,26],{},"At the end, db1 is ",[19,918,919],{},"not primary yet",": etcd is waiting for quorum (≥ 2 nodes). That's expected. The script produces two archives, ",[217,922,923],{},"certs-db2.tar.gz",[217,925,926],{},"certs-db3.tar.gz",[51,928,930],{"id":929},"step-2-distribute-the-certificates","Step 2 — Distribute the certificates",[15,932,933,934,937],{},"The deployment ",[19,935,936],{},"does no automatic SSH"," (by design: no keys to propagate, authentication stays interactive). You copy the archives yourself:",[210,939,941],{"className":321,"code":940,"language":323,"meta":219,"style":219},"scp certs-db2.tar.gz user@192.168.11.46:\u002Ftmp\u002F\nscp certs-db3.tar.gz user@192.168.11.47:\u002Ftmp\u002F\n# and cluster.env, identical:\nscp cluster.env user@192.168.11.46:~\u002Fdev-life-labs\u002Fpostgresql-ha-cluster\u002F\nscp cluster.env user@192.168.11.47:~\u002Fdev-life-labs\u002Fpostgresql-ha-cluster\u002F\n",[217,942,943,954,964,969,978],{"__ignoreMap":219},[327,944,945,948,951],{"class":329,"line":330},[327,946,947],{"class":333},"scp",[327,949,950],{"class":337}," certs-db2.tar.gz",[327,952,953],{"class":337}," user@192.168.11.46:\u002Ftmp\u002F\n",[327,955,956,958,961],{"class":329,"line":344},[327,957,947],{"class":333},[327,959,960],{"class":337}," certs-db3.tar.gz",[327,962,963],{"class":337}," user@192.168.11.47:\u002Ftmp\u002F\n",[327,965,966],{"class":329,"line":511},[327,967,968],{"class":395},"# and cluster.env, identical:\n",[327,970,971,973,975],{"class":329,"line":526},[327,972,947],{"class":333},[327,974,462],{"class":337},[327,976,977],{"class":337}," user@192.168.11.46:~\u002Fdev-life-labs\u002Fpostgresql-ha-cluster\u002F\n",[327,979,980,982,984],{"class":329,"line":541},[327,981,947],{"class":333},[327,983,462],{"class":337},[327,985,986],{"class":337}," user@192.168.11.47:~\u002Fdev-life-labs\u002Fpostgresql-ha-cluster\u002F\n",[51,988,990],{"id":989},"step-3-deploy-db2-then-db3","Step 3 — Deploy db2 then db3",[15,992,993,994,996],{},"On ",[19,995,661],{}," (then, identically, on db3) — clone the repo, then deploy:",[210,998,1000],{"className":321,"code":999,"language":323,"meta":219,"style":219},"git clone https:\u002F\u002Fgithub.com\u002Fdenisakp\u002Fdev-life-labs\ncd dev-life-labs\u002Fpostgresql-ha-cluster\nchmod 600 cluster.env\nsudo .\u002Fpreflight.sh db2\nsudo .\u002Fdeploy.sh db2 --no-upgrade\n",[217,1001,1002,1010,1016,1025,1034],{"__ignoreMap":219},[327,1003,1004,1006,1008],{"class":329,"line":330},[327,1005,334],{"class":333},[327,1007,338],{"class":337},[327,1009,341],{"class":337},[327,1011,1012,1014],{"class":329,"line":344},[327,1013,348],{"class":347},[327,1015,351],{"class":337},[327,1017,1018,1021,1023],{"class":329,"line":511},[327,1019,1020],{"class":333},"chmod",[327,1022,472],{"class":391},[327,1024,475],{"class":337},[327,1026,1027,1029,1031],{"class":329,"line":526},[327,1028,812],{"class":333},[327,1030,815],{"class":337},[327,1032,1033],{"class":337}," db2\n",[327,1035,1036,1038,1040,1043],{"class":329,"line":541},[327,1037,812],{"class":333},[327,1039,884],{"class":337},[327,1041,1042],{"class":337}," db2",[327,1044,890],{"class":337},[15,1046,1047,1049,1050,1053],{},[217,1048,306],{}," detects the archive in ",[217,1051,1052],{},"\u002Ftmp",", extracts the certificates, configures the node and joins the cluster. As soon as db2 starts, etcd quorum forms (2 nodes), Patroni elects db1 as primary and db2 syncs as a replica. db3 then joins the same way.",[51,1055,1057],{"id":1056},"step-4-verify","Step 4 — Verify",[15,1059,1060],{},"From any node:",[210,1062,1064],{"className":321,"code":1063,"language":323,"meta":219,"style":219},"# 1 Leader + 2 Replicas streaming\npatronictl -c \u002Fetc\u002Fpatroni\u002Fconfig.yml list\n",[217,1065,1066,1071],{"__ignoreMap":219},[327,1067,1068],{"class":329,"line":330},[327,1069,1070],{"class":395},"# 1 Leader + 2 Replicas streaming\n",[327,1072,1073,1076,1079,1082],{"class":329,"line":344},[327,1074,1075],{"class":333},"patronictl",[327,1077,1078],{"class":337}," -c",[327,1080,1081],{"class":337}," \u002Fetc\u002Fpatroni\u002Fconfig.yml",[327,1083,1084],{"class":337}," list\n",[210,1086,1089],{"className":1087,"code":1088,"language":215},[213],"+ Cluster: postgresql-cluster ------+---------+----+-----------+\n| Member | Host          | Role    | State   | TL | Lag in MB |\n+--------+---------------+---------+---------+----+-----------+\n| db1    | 192.168.11.45 | Leader  | running |  1 |           |\n| db2    | 192.168.11.46 | Replica | running |  1 |         0 |\n| db3    | 192.168.11.47 | Replica | running |  1 |         0 |\n+--------+---------------+---------+---------+----+-----------+\n",[217,1090,1088],{"__ignoreMap":219},[210,1092,1094],{"className":321,"code":1093,"language":323,"meta":219,"style":219},"# etcd: 3 healthy members\netcdctl --endpoints=https:\u002F\u002F192.168.11.45:2379,https:\u002F\u002F192.168.11.46:2379,https:\u002F\u002F192.168.11.47:2379 \\\n  --cacert=\u002Fetc\u002Fetcd\u002Fssl\u002Fca.crt --cert=\u002Fetc\u002Fetcd\u002Fssl\u002Fetcd-db1.crt --key=\u002Fetc\u002Fetcd\u002Fssl\u002Fetcd-db1.key \\\n  endpoint health\n\n# Connection via the VIP → must return 'f' (primary, read\u002Fwrite)\nPGPASSWORD='\u003Csuperuser>' psql -h 192.168.11.50 -U postgres -c 'SELECT pg_is_in_recovery();'\n\n# The VIP is indeed mounted on the primary\nip addr show ens3 | grep 192.168.11.50\n",[217,1095,1096,1101,1113,1126,1134,1138,1143,1183,1187,1192],{"__ignoreMap":219},[327,1097,1098],{"class":329,"line":330},[327,1099,1100],{"class":395},"# etcd: 3 healthy members\n",[327,1102,1103,1106,1109],{"class":329,"line":344},[327,1104,1105],{"class":333},"etcdctl",[327,1107,1108],{"class":337}," --endpoints=https:\u002F\u002F192.168.11.45:2379,https:\u002F\u002F192.168.11.46:2379,https:\u002F\u002F192.168.11.47:2379",[327,1110,1112],{"class":1111},"sTEyZ"," \\\n",[327,1114,1115,1118,1121,1124],{"class":329,"line":511},[327,1116,1117],{"class":337},"  --cacert=\u002Fetc\u002Fetcd\u002Fssl\u002Fca.crt",[327,1119,1120],{"class":337}," --cert=\u002Fetc\u002Fetcd\u002Fssl\u002Fetcd-db1.crt",[327,1122,1123],{"class":337}," --key=\u002Fetc\u002Fetcd\u002Fssl\u002Fetcd-db1.key",[327,1125,1112],{"class":1111},[327,1127,1128,1131],{"class":329,"line":526},[327,1129,1130],{"class":337},"  endpoint",[327,1132,1133],{"class":337}," health\n",[327,1135,1136],{"class":329,"line":541},[327,1137,545],{"emptyLinePlaceholder":544},[327,1139,1140],{"class":329,"line":548},[327,1141,1142],{"class":395},"# Connection via the VIP → must return 'f' (primary, read\u002Fwrite)\n",[327,1144,1145,1148,1150,1153,1156,1158,1161,1164,1166,1169,1172,1174,1177,1180],{"class":329,"line":554},[327,1146,1147],{"class":1111},"PGPASSWORD",[327,1149,499],{"class":465},[327,1151,1152],{"class":465},"'",[327,1154,1155],{"class":337},"\u003Csuperuser>",[327,1157,1152],{"class":465},[327,1159,1160],{"class":333}," psql",[327,1162,1163],{"class":337}," -h",[327,1165,392],{"class":391},[327,1167,1168],{"class":337}," -U",[327,1170,1171],{"class":337}," postgres",[327,1173,1078],{"class":337},[327,1175,1176],{"class":465}," '",[327,1178,1179],{"class":337},"SELECT pg_is_in_recovery();",[327,1181,1182],{"class":465},"'\n",[327,1184,1185],{"class":329,"line":572},[327,1186,545],{"emptyLinePlaceholder":544},[327,1188,1189],{"class":329,"line":589},[327,1190,1191],{"class":395},"# The VIP is indeed mounted on the primary\n",[327,1193,1194,1196,1199,1202,1205,1208,1211],{"class":329,"line":605},[327,1195,426],{"class":333},[327,1197,1198],{"class":337}," addr",[327,1200,1201],{"class":337}," show",[327,1203,1204],{"class":337}," ens3",[327,1206,1207],{"class":465}," |",[327,1209,1210],{"class":333}," grep",[327,1212,1213],{"class":391}," 192.168.11.50\n",[15,1215,1216,1219,1220,1223],{},[217,1217,1218],{},"pg_is_in_recovery()"," returning ",[217,1221,1222],{},"f"," confirms the VIP leads to the writable primary.",[51,1225,1227],{"id":1226},"step-5-firewall","Step 5 — Firewall",[15,1229,1230],{},"On all 3 nodes, restrict the cluster ports to its subnet:",[210,1232,1234],{"className":321,"code":1233,"language":323,"meta":219,"style":219},"sudo ufw allow from 192.168.11.0\u002F24 to any port 2379,2380,5432,5433,8008 proto tcp\n# VRRP between the nodes:\nsudo ufw allow from 192.168.11.0\u002F24 to any proto vrrp 2>\u002Fdev\u002Fnull || \\\n  sudo ufw allow from 192.168.11.0\u002F24 to 224.0.0.18\n",[217,1235,1236,1270,1275,1307],{"__ignoreMap":219},[327,1237,1238,1240,1243,1246,1249,1252,1255,1258,1261,1264,1267],{"class":329,"line":330},[327,1239,812],{"class":333},[327,1241,1242],{"class":337}," ufw",[327,1244,1245],{"class":337}," allow",[327,1247,1248],{"class":337}," from",[327,1250,1251],{"class":337}," 192.168.11.0\u002F24",[327,1253,1254],{"class":337}," to",[327,1256,1257],{"class":337}," any",[327,1259,1260],{"class":337}," port",[327,1262,1263],{"class":337}," 2379,2380,5432,5433,8008",[327,1265,1266],{"class":337}," proto",[327,1268,1269],{"class":337}," tcp\n",[327,1271,1272],{"class":329,"line":344},[327,1273,1274],{"class":395},"# VRRP between the nodes:\n",[327,1276,1277,1279,1281,1283,1285,1287,1289,1291,1293,1296,1299,1302,1305],{"class":329,"line":511},[327,1278,812],{"class":333},[327,1280,1242],{"class":337},[327,1282,1245],{"class":337},[327,1284,1248],{"class":337},[327,1286,1251],{"class":337},[327,1288,1254],{"class":337},[327,1290,1257],{"class":337},[327,1292,1266],{"class":337},[327,1294,1295],{"class":337}," vrrp",[327,1297,1298],{"class":465}," 2>",[327,1300,1301],{"class":337},"\u002Fdev\u002Fnull",[327,1303,1304],{"class":465}," ||",[327,1306,1112],{"class":1111},[327,1308,1309,1312,1314,1316,1318,1320,1322],{"class":329,"line":526},[327,1310,1311],{"class":333},"  sudo",[327,1313,1242],{"class":337},[327,1315,1245],{"class":337},[327,1317,1248],{"class":337},[327,1319,1251],{"class":337},[327,1321,1254],{"class":337},[327,1323,1324],{"class":391}," 224.0.0.18\n",[94,1326,1327,1337],{},[97,1328,1329],{},[100,1330,1331,1334],{},[103,1332,1333],{},"Port",[103,1335,1336],{},"Service",[113,1338,1339,1347,1358,1366],{},[100,1340,1341,1344],{},[118,1342,1343],{},"2379 \u002F 2380",[118,1345,1346],{},"etcd (client \u002F peer)",[100,1348,1349,1352],{},[118,1350,1351],{},"5432",[118,1353,1354,1355],{},"HAProxy — ",[19,1356,1357],{},"the port exposed to applications",[100,1359,1360,1363],{},[118,1361,1362],{},"5433",[118,1364,1365],{},"Internal PostgreSQL (managed by Patroni)",[100,1367,1368,1371],{},[118,1369,1370],{},"8008",[118,1372,1373],{},"Patroni REST API (HAProxy health check)",[51,1375,1377],{"id":1376},"the-test-that-validates-everything-failover","The test that validates everything: failover",[15,1379,1380],{},"This is the whole point of the cluster. We simulate the primary going down:",[210,1382,1384],{"className":321,"code":1383,"language":323,"meta":219,"style":219},"# Identify the leader\npatronictl -c \u002Fetc\u002Fpatroni\u002Fconfig.yml list\n\n# On the leader node: stop Patroni\nsudo systemctl stop patroni\n\n# A few seconds later, on another node:\npatronictl -c \u002Fetc\u002Fpatroni\u002Fconfig.yml list   # → a replica has become Leader\n",[217,1385,1386,1391,1401,1405,1410,1423,1427,1432],{"__ignoreMap":219},[327,1387,1388],{"class":329,"line":330},[327,1389,1390],{"class":395},"# Identify the leader\n",[327,1392,1393,1395,1397,1399],{"class":329,"line":344},[327,1394,1075],{"class":333},[327,1396,1078],{"class":337},[327,1398,1081],{"class":337},[327,1400,1084],{"class":337},[327,1402,1403],{"class":329,"line":511},[327,1404,545],{"emptyLinePlaceholder":544},[327,1406,1407],{"class":329,"line":526},[327,1408,1409],{"class":395},"# On the leader node: stop Patroni\n",[327,1411,1412,1414,1417,1420],{"class":329,"line":541},[327,1413,812],{"class":333},[327,1415,1416],{"class":337}," systemctl",[327,1418,1419],{"class":337}," stop",[327,1421,1422],{"class":337}," patroni\n",[327,1424,1425],{"class":329,"line":548},[327,1426,545],{"emptyLinePlaceholder":544},[327,1428,1429],{"class":329,"line":554},[327,1430,1431],{"class":395},"# A few seconds later, on another node:\n",[327,1433,1434,1436,1438,1440,1443],{"class":329,"line":572},[327,1435,1075],{"class":333},[327,1437,1078],{"class":337},[327,1439,1081],{"class":337},[327,1441,1442],{"class":337}," list",[327,1444,1445],{"class":395},"   # → a replica has become Leader\n",[15,1447,1448,1449,1451,1452,1455],{},"What happens within seconds, with no intervention: Patroni loses the leader lease in etcd → a replica nominates itself and is elected → it is promoted to primary → its API now answers ",[217,1450,233],{}," on ",[217,1453,1454],{},"\u002Fprimary"," → HAProxy redirects traffic → the VIP stays on a node where HAProxy is healthy. Applications, connected to the VIP, see only a brief interruption.",[15,1457,1458],{},"Bring the former primary back in:",[210,1460,1462],{"className":321,"code":1461,"language":323,"meta":219,"style":219},"sudo systemctl start patroni   # it returns as a Replica and resyncs\n",[217,1463,1464],{"__ignoreMap":219},[327,1465,1466,1468,1470,1473,1476],{"class":329,"line":330},[327,1467,812],{"class":333},[327,1469,1416],{"class":337},[327,1471,1472],{"class":337}," start",[327,1474,1475],{"class":337}," patroni",[327,1477,1478],{"class":395},"   # it returns as a Replica and resyncs\n",[43,1480],{},[46,1482,1484],{"id":1483},"recap","Recap",[94,1486,1487,1497],{},[97,1488,1489],{},[100,1490,1491,1494],{},[103,1492,1493],{},"Building block",[103,1495,1496],{},"Without it…",[113,1498,1499,1507,1515,1522,1529],{},[100,1500,1501,1504],{},[118,1502,1503],{},"PostgreSQL + replication",[118,1505,1506],{},"no copy of the data",[100,1508,1509,1512],{},[118,1510,1511],{},"etcd (quorum of 3)",[118,1513,1514],{},"no agreement on who is primary → split-brain",[100,1516,1517,1519],{},[118,1518,148],{},[118,1520,1521],{},"no automatic election or failover",[100,1523,1524,1526],{},[118,1525,161],{},[118,1527,1528],{},"clients don't know who the primary is",[100,1530,1531,1534],{},[118,1532,1533],{},"Keepalived (VIP)",[118,1535,1536],{},"the connection string would have to change on every switchover",[15,1538,1539,1540,1543,1544,1546,1547,1549,1550,26],{},"To adapt this to your environment, you only touch ",[19,1541,1542],{},"one file"," (",[217,1545,287],{},"): subnet, IPs, interface, secrets. The rest — certificates, configuration of the four services, backup scheduling — is generated by ",[217,1548,306],{},", which is idempotent: on error, fix it and re-run ",[217,1551,1552],{},"sudo .\u002Fdeploy.sh dbX --no-upgrade",[15,1554,1555],{},[19,1556,1557],{},"Traps to remember:",[246,1559,1560,1563,1569,1573,1578,1581],{},[62,1561,1562],{},"Mandatory order: db1 → certs → db2 → db3.",[62,1564,1565,1566,1568],{},"Correct ",[217,1567,438],{},", otherwise the VIP won't come up.",[62,1570,1571,768],{},[217,1572,744],{},[62,1574,1575,1577],{},[217,1576,287],{}," identical on all 3 nodes.",[62,1579,1580],{},"Clock synchronized (NTP) on all three.",[62,1582,1583],{},"Odd number of nodes (3 minimum).",[1585,1586,1587],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":219,"searchDepth":344,"depth":344,"links":1589},[1590,1597,1603,1611],{"id":48,"depth":344,"text":49,"children":1591},[1592,1593,1594,1595,1596],{"id":53,"depth":511,"text":54},{"id":91,"depth":511,"text":92},{"id":183,"depth":511,"text":184},{"id":207,"depth":511,"text":208},{"id":240,"depth":511,"text":241},{"id":276,"depth":344,"text":277,"children":1598},[1599,1600,1601,1602],{"id":357,"depth":511,"text":358},{"id":442,"depth":511,"text":443},{"id":780,"depth":511,"text":781},{"id":794,"depth":511,"text":795},{"id":848,"depth":344,"text":849,"children":1604},[1605,1606,1607,1608,1609,1610],{"id":863,"depth":511,"text":864},{"id":929,"depth":511,"text":930},{"id":989,"depth":511,"text":990},{"id":1056,"depth":511,"text":1057},{"id":1226,"depth":511,"text":1227},{"id":1376,"depth":511,"text":1377},{"id":1483,"depth":344,"text":1484},"2026-06-25","How to build a PostgreSQL high-availability cluster with automatic failover using Patroni, etcd, HAProxy and Keepalived. Learn the architecture, prerequisites, and step-by-step deployment so any node failure is handled with zero app changes.","md",null,{},"\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster",{"title":5,"description":1613},"set-up-postgresql-ha-cluster-with-patroni","databases\u002Fpostgres\u002F1.setup-postgres-ha-cluster",[122,1622,148,135,161,174],"High-Availability",[1624,1625,1626],"databases","postgresql","high-availability","z6v5quWNyWXoehkAzVpjQxRzsyxb0kh-rbwUs9vZhvk",{"id":1629,"title":1630,"body":1631,"date":1612,"description":3017,"extension":1614,"img":1615,"meta":3018,"navigation":544,"path":3019,"seo":3020,"slug":1619,"stem":3021,"tags":3022,"topics":3023,"__hash__":3024},"content\u002Fdatabases\u002Fpostgres\u002F1.setup-postgres-ha-cluster.fr.md","Mettre en place un cluster PostgreSQL haute disponibilité à 3 nœuds avec Patroni et etcd",{"type":7,"value":1632,"toc":2993},[1633,1637,1647,1654,1660,1662,1666,1670,1673,1699,1702,1706,1783,1787,1801,1806,1810,1816,1832,1836,1839,1863,1865,1869,1872,1898,1904,1922,1925,1929,1932,1946,1962,1978,1990,1996,2000,2003,2023,2026,2255,2258,2276,2280,2289,2293,2300,2312,2322,2334,2336,2340,2351,2355,2377,2387,2397,2408,2412,2419,2461,2465,2471,2515,2523,2527,2530,2550,2555,2653,2661,2665,2668,2746,2787,2791,2794,2855,2868,2871,2889,2891,2895,2946,2960,2965,2990],[10,1634,1636],{"id":1635},"construire-un-cluster-postgresql-haute-disponibilité-patroni-etcd-haproxy-keepalived","Construire un cluster PostgreSQL haute disponibilité (Patroni + etcd + HAProxy + Keepalived)",[15,1638,1639,1640,1643,1644,26],{},"PostgreSQL seul est un point unique de défaillance : si le serveur tombe, l'application tombe. Cet article montre comment monter un cluster à 3 nœuds qui survit à la perte d'une machine ",[19,1641,1642],{},"sans intervention humaine"," et ",[19,1645,1646],{},"sans changer la chaîne de connexion côté applications",[15,1648,1649,1650,1653],{},"L'objectif n'est pas de copier-coller des commandes sans comprendre, mais de saisir ",[31,1651,1652],{},"pourquoi"," chaque brique existe, pour pouvoir adapter le tout à votre propre réseau.",[15,1655,1656,1657,1659],{},"Le résultat : une seule IP virtuelle (la ",[31,1658,40],{},") sur laquelle les applications se connectent comme sur une base normale. Derrière, trois serveurs se surveillent en permanence ; en cas de panne du primaire, un réplica est promu en quelques secondes et la VIP bascule automatiquement.",[43,1661],{},[46,1663,1665],{"id":1664},"partie-1-larchitecture-et-pourquoi-ce-stack","Partie 1 — L'architecture et pourquoi ce stack",[51,1667,1669],{"id":1668},"le-problème-de-la-haute-disponibilité","Le problème de la haute disponibilité",[15,1671,1672],{},"Pour qu'une base reste disponible quand un serveur tombe, il faut résoudre quatre problèmes distincts :",[59,1674,1675,1681,1687,1693],{},[62,1676,1677,1680],{},[19,1678,1679],{},"Stocker la donnée plusieurs fois"," → réplication PostgreSQL (un primaire écrit, des réplicas suivent).",[62,1682,1683,1686],{},[19,1684,1685],{},"Décider qui est le primaire",", et le changer sans conflit quand il tombe → un orchestrateur (Patroni).",[62,1688,1689,1692],{},[19,1690,1691],{},"Se mettre d'accord à plusieurs sur cette décision",", même en cas de coupure réseau → un magasin à consensus (etcd).",[62,1694,1695,1698],{},[19,1696,1697],{},"Diriger les clients vers le bon serveur"," sans qu'ils sachent qui est primaire → un routeur (HAProxy) + une adresse unique qui suit le service (VIP via Keepalived).",[15,1700,1701],{},"Chaque composant règle exactement un de ces problèmes. C'est pour ça qu'il y en a quatre.",[51,1703,1705],{"id":1704},"les-quatre-composants","Les quatre composants",[94,1707,1708,1721],{},[97,1709,1710],{},[100,1711,1712,1715,1718],{},[103,1713,1714],{},"Composant",[103,1716,1717],{},"Rôle",[103,1719,1720],{},"Ce qu'il résout",[113,1722,1723,1735,1747,1759,1771],{},[100,1724,1725,1729,1732],{},[118,1726,1727],{},[19,1728,122],{},[118,1730,1731],{},"Le moteur de base de données",[118,1733,1734],{},"Stocke et réplique la donnée",[100,1736,1737,1741,1744],{},[118,1738,1739],{},[19,1740,135],{},[118,1742,1743],{},"Magasin clé-valeur distribué (le « DCS »)",[118,1745,1746],{},"Source de vérité partagée : qui est le leader",[100,1748,1749,1753,1756],{},[118,1750,1751],{},[19,1752,148],{},[118,1754,1755],{},"Orchestrateur au-dessus de PostgreSQL",[118,1757,1758],{},"Bootstrap, réplication, élection, failover",[100,1760,1761,1765,1768],{},[118,1762,1763],{},[19,1764,161],{},[118,1766,1767],{},"Proxy TCP",[118,1769,1770],{},"Route chaque connexion vers le primaire courant",[100,1772,1773,1777,1780],{},[118,1774,1775],{},[19,1776,174],{},[118,1778,1779],{},"Gestion d'IP virtuelle (VRRP)",[118,1781,1782],{},"Fait flotter une IP unique vers un nœud sain",[51,1784,1786],{"id":1785},"pourquoi-trois-nœuds-et-pas-deux","Pourquoi trois nœuds, et pas deux",[15,1788,1789,1790,1793,1794,1797,1798,1800],{},"etcd (comme tout système à consensus) décide à la ",[19,1791,1792],{},"majorité",". Avec 3 nœuds, la majorité est 2 : le cluster continue de fonctionner si ",[19,1795,1796],{},"un"," nœud tombe. Avec 2 nœuds, perdre un nœud fait perdre la majorité — plus personne ne peut décider qui est primaire, et le cluster se fige par sécurité (pour éviter le ",[31,1799,198],{},", deux primaires qui divergent).",[15,1802,1803],{},[19,1804,1805],{},"Règle simple : un nombre impair de nœuds. Trois est le minimum utile.",[51,1807,1809],{"id":1808},"le-chemin-dune-requête","Le chemin d'une requête",[210,1811,1814],{"className":1812,"code":1813,"language":215},[213],"Application\n    │  se connecte à la VIP (ex: 192.168.11.50:5432)\n    ▼\nKeepalived  ── la VIP est montée sur le nœud sain prioritaire\n    ▼\nHAProxy     ── teste les 3 nœuds, n'envoie qu'au PRIMAIRE\n    ▼\nPostgreSQL (primaire)  ←── répliquent ──  PostgreSQL (réplicas ×2)\n    ▲\nPatroni     ── surveille, et via etcd décide qui est primaire\n",[217,1815,1813],{"__ignoreMap":219},[15,1817,1818,1819,1822,1823,1825,1826,1828,1829,1831],{},"Point clé : ",[19,1820,1821],{},"HAProxy ne devine pas"," qui est primaire. Il interroge l'API REST de Patroni sur chaque nœud (",[217,1824,229],{},"). Seul le nœud réellement primaire répond ",[217,1827,233],{},". Les réplicas répondent autre chose et sont écartés. Quand le failover se produit, c'est l'ancien réplica devenu primaire qui se met à répondre ",[217,1830,233],{},", et HAProxy redirige le trafic — automatiquement, sans reconfiguration.",[51,1833,1835],{"id":1834},"sécurité-tls-partout","Sécurité : TLS partout",[15,1837,1838],{},"Tout le trafic interne est chiffré et authentifié par certificats X.509 :",[246,1840,1841,1846,1855],{},[62,1842,1843,1845],{},[19,1844,135],{}," exige des certificats mutuels (peer-to-peer et client). Une CA privée signe un certificat par nœud.",[62,1847,1848,1850,1851,1854],{},[19,1849,122],{}," utilise un certificat partagé dont les ",[31,1852,1853],{},"SAN"," couvrent la VIP + les 3 IP + localhost, pour que la connexion soit valide quel que soit le nœud qui sert.",[62,1856,1857,1859,1860,1862],{},[19,1858,266],{}," n'autorise que des connexions ",[217,1861,270],{}," (jamais en clair), restreintes au sous-réseau du cluster.",[43,1864],{},[46,1866,1868],{"id":1867},"partie-2-préparation-réseau-configuration-pré-vol","Partie 2 — Préparation : réseau, configuration, pré-vol",[15,1870,1871],{},"Tout le déploiement tient dans deux scripts et un fichier de configuration :",[246,1873,1874,1882,1890],{},[62,1875,1876,288,1878,1881],{},[217,1877,287],{},[19,1879,1880],{},"toutes"," les variables (IP, ports, secrets, version PG). C'est le seul fichier à éditer.",[62,1883,1884,1886,1887,26],{},[217,1885,297],{}," — vérifie que la machine est prête, ",[19,1888,1889],{},"n'écrit rien",[62,1891,1892,1894,1895,1897],{},[217,1893,306],{}," — installe et configure tout. ",[19,1896,310],{}," : rejouable sans casser un cluster existant.",[15,1899,1900,1901,1903],{},"Les trois fichiers sont dans le repo des labs du blog, sous-dossier ",[217,1902,317],{},". Récupérez-les :",[210,1905,1906],{"className":321,"code":322,"language":323,"meta":219,"style":219},[217,1907,1908,1916],{"__ignoreMap":219},[327,1909,1910,1912,1914],{"class":329,"line":330},[327,1911,334],{"class":333},[327,1913,338],{"class":337},[327,1915,341],{"class":337},[327,1917,1918,1920],{"class":329,"line":344},[327,1919,348],{"class":347},[327,1921,351],{"class":337},[15,1923,1924],{},"C'est ce dossier que vous copierez ensuite sur chaque nœud (voir Partie 3).",[51,1926,1928],{"id":1927},"pré-requis-réseau","Pré-requis réseau",[15,1930,1931],{},"Avant tout, il faut :",[246,1933,1934,1940],{},[62,1935,1936,1939],{},[19,1937,1938],{},"3 serveurs"," Debian\u002FUbuntu sur le même sous-réseau, avec IP fixes.",[62,1941,1942,1945],{},[19,1943,1944],{},"Une IP libre"," dans ce sous-réseau pour la VIP — vérifiez-la :",[210,1947,1949],{"className":321,"code":1948,"language":323,"meta":219,"style":219},"ping -c2 192.168.11.50    # NE doit PAS répondre → la VIP est libre\n",[217,1950,1951],{"__ignoreMap":219},[327,1952,1953,1955,1957,1959],{"class":329,"line":330},[327,1954,385],{"class":333},[327,1956,388],{"class":337},[327,1958,392],{"class":391},[327,1960,1961],{"class":395},"    # NE doit PAS répondre → la VIP est libre\n",[246,1963,1964],{},[62,1965,1966,1967,1970,1971,409,1973,409,1975,1977],{},"Le ",[19,1968,1969],{},"nom de l'interface réseau"," de chaque serveur (souvent ",[217,1972,408],{},[217,1974,412],{},[217,1976,415],{},"…) :",[210,1979,1980],{"className":321,"code":419,"language":323,"meta":219,"style":219},[217,1981,1982],{"__ignoreMap":219},[327,1983,1984,1986,1988],{"class":329,"line":330},[327,1985,426],{"class":333},[327,1987,429],{"class":337},[327,1989,432],{"class":337},[15,1991,1992,1993,1995],{},"Si l'interface (",[217,1994,438],{},") est mal renseignée, la VIP ne montera jamais. C'est l'erreur n°1.",[51,1997,1999],{"id":1998},"le-fichier-de-configuration","Le fichier de configuration",[15,2001,2002],{},"On part du modèle :",[210,2004,2005],{"className":321,"code":449,"language":323,"meta":219,"style":219},[217,2006,2007],{"__ignoreMap":219},[327,2008,2009,2011,2013,2015,2017,2019,2021],{"class":329,"line":330},[327,2010,456],{"class":333},[327,2012,459],{"class":337},[327,2014,462],{"class":337},[327,2016,466],{"class":465},[327,2018,469],{"class":333},[327,2020,472],{"class":391},[327,2022,475],{"class":337},[15,2024,2025],{},"Les champs essentiels :",[210,2027,2029],{"className":481,"code":2028,"language":483,"meta":219,"style":219},"# Identité\nCLUSTER_NAME=\"postgresql-cluster\"\nPG_VERSION=\"18\"\nETCD_VERSION=\"v3.6.12\"\n\n# Réseau\nSUBNET=\"192.168.11.0\u002F24\"      # sous-réseau du cluster\nVIP=\"192.168.11.50\"           # IP virtuelle (entrée des applications)\nIFACE=\"ens3\"                  # interface qui portera la VIP\nAPP_SUBNET=\"192.168.11.0\u002F24\"  # d'où viennent les clients\n\n# Nœuds\nDB1_NAME=\"db1\"; DB1_IP=\"192.168.11.45\"   # primaire initial, priorité VRRP 100\nDB2_NAME=\"db2\"; DB2_IP=\"192.168.11.46\"   # secours, priorité 90\nDB3_NAME=\"db3\"; DB3_IP=\"192.168.11.47\"   # secours, priorité 80\n\n# Secrets — générer chacun avec :  openssl rand -base64 24\nPG_SUPERUSER_PASSWORD=\"...\"\nPG_REPLICATOR_PASSWORD=\"...\"\nPATRONI_RESTAPI_PASSWORD=\"...\"\nVRRP_AUTH_PASS=\"........\"     # ATTENTION : max 8 caractères (VRRP tronque au-delà)\n",[217,2030,2031,2036,2048,2060,2072,2076,2081,2096,2111,2126,2141,2145,2150,2165,2180,2195,2199,2204,2216,2228,2240],{"__ignoreMap":219},[327,2032,2033],{"class":329,"line":330},[327,2034,2035],{"class":395},"# Identité\n",[327,2037,2038,2040,2042,2044,2046],{"class":329,"line":344},[327,2039,496],{"class":495},[327,2041,499],{"class":465},[327,2043,502],{"class":465},[327,2045,505],{"class":337},[327,2047,508],{"class":465},[327,2049,2050,2052,2054,2056,2058],{"class":329,"line":511},[327,2051,514],{"class":495},[327,2053,499],{"class":465},[327,2055,502],{"class":465},[327,2057,521],{"class":337},[327,2059,508],{"class":465},[327,2061,2062,2064,2066,2068,2070],{"class":329,"line":526},[327,2063,529],{"class":495},[327,2065,499],{"class":465},[327,2067,502],{"class":465},[327,2069,536],{"class":337},[327,2071,508],{"class":465},[327,2073,2074],{"class":329,"line":541},[327,2075,545],{"emptyLinePlaceholder":544},[327,2077,2078],{"class":329,"line":548},[327,2079,2080],{"class":395},"# Réseau\n",[327,2082,2083,2085,2087,2089,2091,2093],{"class":329,"line":554},[327,2084,557],{"class":495},[327,2086,499],{"class":465},[327,2088,502],{"class":465},[327,2090,564],{"class":337},[327,2092,502],{"class":465},[327,2094,2095],{"class":395},"      # sous-réseau du cluster\n",[327,2097,2098,2100,2102,2104,2106,2108],{"class":329,"line":572},[327,2099,40],{"class":495},[327,2101,499],{"class":465},[327,2103,502],{"class":465},[327,2105,581],{"class":337},[327,2107,502],{"class":465},[327,2109,2110],{"class":395},"           # IP virtuelle (entrée des applications)\n",[327,2112,2113,2115,2117,2119,2121,2123],{"class":329,"line":589},[327,2114,438],{"class":495},[327,2116,499],{"class":465},[327,2118,502],{"class":465},[327,2120,408],{"class":337},[327,2122,502],{"class":465},[327,2124,2125],{"class":395},"                  # interface qui portera la VIP\n",[327,2127,2128,2130,2132,2134,2136,2138],{"class":329,"line":605},[327,2129,608],{"class":495},[327,2131,499],{"class":465},[327,2133,502],{"class":465},[327,2135,564],{"class":337},[327,2137,502],{"class":465},[327,2139,2140],{"class":395},"  # d'où viennent les clients\n",[327,2142,2143],{"class":329,"line":622},[327,2144,545],{"emptyLinePlaceholder":544},[327,2146,2147],{"class":329,"line":627},[327,2148,2149],{"class":395},"# Nœuds\n",[327,2151,2152,2154,2156,2158,2160,2162],{"class":329,"line":633},[327,2153,636],{"class":495},[327,2155,499],{"class":465},[327,2157,502],{"class":465},[327,2159,643],{"class":337},[327,2161,502],{"class":465},[327,2163,2164],{"class":395},"; DB1_IP=\"192.168.11.45\"   # primaire initial, priorité VRRP 100\n",[327,2166,2167,2169,2171,2173,2175,2177],{"class":329,"line":651},[327,2168,654],{"class":495},[327,2170,499],{"class":465},[327,2172,502],{"class":465},[327,2174,661],{"class":337},[327,2176,502],{"class":465},[327,2178,2179],{"class":395},"; DB2_IP=\"192.168.11.46\"   # secours, priorité 90\n",[327,2181,2182,2184,2186,2188,2190,2192],{"class":329,"line":669},[327,2183,672],{"class":495},[327,2185,499],{"class":465},[327,2187,502],{"class":465},[327,2189,679],{"class":337},[327,2191,502],{"class":465},[327,2193,2194],{"class":395},"; DB3_IP=\"192.168.11.47\"   # secours, priorité 80\n",[327,2196,2197],{"class":329,"line":687},[327,2198,545],{"emptyLinePlaceholder":544},[327,2200,2201],{"class":329,"line":692},[327,2202,2203],{"class":395},"# Secrets — générer chacun avec :  openssl rand -base64 24\n",[327,2205,2206,2208,2210,2212,2214],{"class":329,"line":698},[327,2207,701],{"class":495},[327,2209,499],{"class":465},[327,2211,502],{"class":465},[327,2213,708],{"class":337},[327,2215,508],{"class":465},[327,2217,2218,2220,2222,2224,2226],{"class":329,"line":713},[327,2219,716],{"class":495},[327,2221,499],{"class":465},[327,2223,502],{"class":465},[327,2225,708],{"class":337},[327,2227,508],{"class":465},[327,2229,2230,2232,2234,2236,2238],{"class":329,"line":727},[327,2231,730],{"class":495},[327,2233,499],{"class":465},[327,2235,502],{"class":465},[327,2237,708],{"class":337},[327,2239,508],{"class":465},[327,2241,2242,2244,2246,2248,2250,2252],{"class":329,"line":741},[327,2243,744],{"class":495},[327,2245,499],{"class":465},[327,2247,502],{"class":465},[327,2249,751],{"class":337},[327,2251,502],{"class":465},[327,2253,2254],{"class":395},"     # ATTENTION : max 8 caractères (VRRP tronque au-delà)\n",[15,2256,2257],{},"Deux pièges qui reviennent toujours :",[246,2259,2260,2268],{},[62,2261,2262,2267],{},[19,2263,2264,2266],{},[217,2265,744],{}," ≤ 8 caractères."," Le protocole VRRP tronque silencieusement ; deux nœuds avec des mots de passe différents après troncature ne se voient pas et la VIP peut se dédoubler.",[62,2269,2270,2275],{},[19,2271,2272,2274],{},[217,2273,287],{}," doit être identique sur les 3 serveurs",", secrets compris. Les certificats et l'authentification reposent dessus.",[51,2277,2279],{"id":2278},"les-priorités-vrrp","Les priorités VRRP",[15,2281,2282,2284,2285,2288],{},[217,2283,786],{},". Keepalived monte la VIP sur le nœud sain de plus haute priorité. db1 l'a par défaut ; s'il tombe, db2 la récupère, puis db3. Quand db1 revient, il reprend la VIP (priorité la plus haute). Ces priorités sont ",[19,2286,2287],{},"indépendantes"," du rôle PostgreSQL : la VIP suit la santé HAProxy, pas le primaire — mais comme HAProxy ne route que vers le primaire, le résultat est cohérent de bout en bout.",[51,2290,2292],{"id":2291},"le-pré-vol","Le pré-vol",[15,2294,2295,2296,2299],{},"À lancer sur ",[19,2297,2298],{},"chaque"," nœud avant le déploiement :",[210,2301,2302],{"className":321,"code":805,"language":323,"meta":219,"style":219},[217,2303,2304],{"__ignoreMap":219},[327,2305,2306,2308,2310],{"class":329,"line":330},[327,2307,812],{"class":333},[327,2309,815],{"class":337},[327,2311,818],{"class":337},[15,2313,2314,2315,2317,2318,2321],{},"Il contrôle, sans rien modifier : secrets remplis (pas de ",[217,2316,824],{},"), VIP dans le sous-réseau, interface présente, IP du nœud correcte, autres nœuds joignables, ports libres, RAM\u002Fdisque suffisants, ",[19,2319,2320],{},"horloge synchronisée (NTP)"," — critique : une dérive d'horloge entre nœuds rend le failover instable.",[246,2323,2324,2329],{},[62,2325,2326,2328],{},[217,2327,836],{}," = bloquant, à corriger avant de continuer.",[62,2330,2331,2333],{},[217,2332,842],{}," = vérifiable, mais le déploiement reste possible.",[43,2335],{},[46,2337,2339],{"id":2338},"partie-3-déploiement-vérification-failover","Partie 3 — Déploiement, vérification, failover",[15,2341,2342,2343,2346,2347,2350],{},"L'ordre est ",[19,2344,2345],{},"impératif"," : ",[217,2348,2349],{},"db1 → distribution des certificats → db2 → db3",". db1 génère la CA et les certificats de tout le monde ; db2 et db3 ne peuvent pas démarrer sans les recevoir.",[51,2352,2354],{"id":2353},"étape-1-déployer-db1","Étape 1 — Déployer db1",[210,2356,2357],{"className":321,"code":867,"language":323,"meta":219,"style":219},[217,2358,2359,2367],{"__ignoreMap":219},[327,2360,2361,2363,2365],{"class":329,"line":330},[327,2362,812],{"class":333},[327,2364,815],{"class":337},[327,2366,818],{"class":337},[327,2368,2369,2371,2373,2375],{"class":329,"line":344},[327,2370,812],{"class":333},[327,2372,884],{"class":337},[327,2374,887],{"class":337},[327,2376,890],{"class":337},[892,2378,2379],{},[15,2380,2381,2383,2384,2386],{},[217,2382,898],{}," saute le ",[217,2385,902],{}," global. Recommandé si les VM hébergent d'autres services (on ne veut pas mettre à jour tout le système au passage). Retirez-le pour un serveur dédié neuf.",[15,2388,2389,2390,2392,2393,2396],{},"Ce que fait ",[217,2391,306],{}," sur db1, dans l'ordre : mise à jour ciblée → installation des paquets (PostgreSQL, Patroni, HAProxy, Keepalived) + binaire etcd → ",[19,2394,2395],{},"génération de la CA et de tous les certificats"," → configuration et démarrage d'etcd → configuration de Patroni, HAProxy, Keepalived → sauvegarde etcd quotidienne planifiée.",[15,2398,2399,2400,2403,2404,1643,2406,26],{},"À la fin, db1 ",[19,2401,2402],{},"n'est pas encore primaire"," : etcd attend le quorum (≥ 2 nœuds). C'est normal. Le script génère deux archives ",[217,2405,923],{},[217,2407,926],{},[51,2409,2411],{"id":2410},"étape-2-distribuer-les-certificats","Étape 2 — Distribuer les certificats",[15,2413,2414,2415,2418],{},"Le déploiement ",[19,2416,2417],{},"ne fait aucun SSH automatique"," (par choix : pas de clés à propager, l'authentification reste interactive). Vous copiez vous-même les archives :",[210,2420,2422],{"className":321,"code":2421,"language":323,"meta":219,"style":219},"scp certs-db2.tar.gz user@192.168.11.46:\u002Ftmp\u002F\nscp certs-db3.tar.gz user@192.168.11.47:\u002Ftmp\u002F\n# et le cluster.env, identique :\nscp cluster.env user@192.168.11.46:~\u002Fdev-life-labs\u002Fpostgresql-ha-cluster\u002F\nscp cluster.env user@192.168.11.47:~\u002Fdev-life-labs\u002Fpostgresql-ha-cluster\u002F\n",[217,2423,2424,2432,2440,2445,2453],{"__ignoreMap":219},[327,2425,2426,2428,2430],{"class":329,"line":330},[327,2427,947],{"class":333},[327,2429,950],{"class":337},[327,2431,953],{"class":337},[327,2433,2434,2436,2438],{"class":329,"line":344},[327,2435,947],{"class":333},[327,2437,960],{"class":337},[327,2439,963],{"class":337},[327,2441,2442],{"class":329,"line":511},[327,2443,2444],{"class":395},"# et le cluster.env, identique :\n",[327,2446,2447,2449,2451],{"class":329,"line":526},[327,2448,947],{"class":333},[327,2450,462],{"class":337},[327,2452,977],{"class":337},[327,2454,2455,2457,2459],{"class":329,"line":541},[327,2456,947],{"class":333},[327,2458,462],{"class":337},[327,2460,986],{"class":337},[51,2462,2464],{"id":2463},"étape-3-déployer-db2-puis-db3","Étape 3 — Déployer db2 puis db3",[15,2466,2467,2468,2470],{},"Sur ",[19,2469,661],{}," (puis, à l'identique, sur db3) — cloner le repo, puis déployer :",[210,2472,2473],{"className":321,"code":999,"language":323,"meta":219,"style":219},[217,2474,2475,2483,2489,2497,2505],{"__ignoreMap":219},[327,2476,2477,2479,2481],{"class":329,"line":330},[327,2478,334],{"class":333},[327,2480,338],{"class":337},[327,2482,341],{"class":337},[327,2484,2485,2487],{"class":329,"line":344},[327,2486,348],{"class":347},[327,2488,351],{"class":337},[327,2490,2491,2493,2495],{"class":329,"line":511},[327,2492,1020],{"class":333},[327,2494,472],{"class":391},[327,2496,475],{"class":337},[327,2498,2499,2501,2503],{"class":329,"line":526},[327,2500,812],{"class":333},[327,2502,815],{"class":337},[327,2504,1033],{"class":337},[327,2506,2507,2509,2511,2513],{"class":329,"line":541},[327,2508,812],{"class":333},[327,2510,884],{"class":337},[327,2512,1042],{"class":337},[327,2514,890],{"class":337},[15,2516,2517,2519,2520,2522],{},[217,2518,306],{}," détecte l'archive dans ",[217,2521,1052],{},", extrait les certificats, configure le nœud et rejoint le cluster. Dès que db2 démarre, le quorum etcd se forme (2 nœuds), Patroni élit db1 primaire et db2 se synchronise en réplica. db3 rejoint ensuite de la même façon.",[51,2524,2526],{"id":2525},"étape-4-vérifier","Étape 4 — Vérifier",[15,2528,2529],{},"Depuis n'importe quel nœud :",[210,2531,2533],{"className":321,"code":2532,"language":323,"meta":219,"style":219},"# 1 Leader + 2 Replica en streaming\npatronictl -c \u002Fetc\u002Fpatroni\u002Fconfig.yml list\n",[217,2534,2535,2540],{"__ignoreMap":219},[327,2536,2537],{"class":329,"line":330},[327,2538,2539],{"class":395},"# 1 Leader + 2 Replica en streaming\n",[327,2541,2542,2544,2546,2548],{"class":329,"line":344},[327,2543,1075],{"class":333},[327,2545,1078],{"class":337},[327,2547,1081],{"class":337},[327,2549,1084],{"class":337},[210,2551,2553],{"className":2552,"code":1088,"language":215},[213],[217,2554,1088],{"__ignoreMap":219},[210,2556,2558],{"className":321,"code":2557,"language":323,"meta":219,"style":219},"# etcd : 3 membres healthy\netcdctl --endpoints=https:\u002F\u002F192.168.11.45:2379,https:\u002F\u002F192.168.11.46:2379,https:\u002F\u002F192.168.11.47:2379 \\\n  --cacert=\u002Fetc\u002Fetcd\u002Fssl\u002Fca.crt --cert=\u002Fetc\u002Fetcd\u002Fssl\u002Fetcd-db1.crt --key=\u002Fetc\u002Fetcd\u002Fssl\u002Fetcd-db1.key \\\n  endpoint health\n\n# Connexion via la VIP → doit retourner 'f' (primaire, en lecture\u002Fécriture)\nPGPASSWORD='\u003Csuperuser>' psql -h 192.168.11.50 -U postgres -c 'SELECT pg_is_in_recovery();'\n\n# La VIP est bien montée sur le primaire\nip addr show ens3 | grep 192.168.11.50\n",[217,2559,2560,2565,2573,2583,2589,2593,2598,2628,2632,2637],{"__ignoreMap":219},[327,2561,2562],{"class":329,"line":330},[327,2563,2564],{"class":395},"# etcd : 3 membres healthy\n",[327,2566,2567,2569,2571],{"class":329,"line":344},[327,2568,1105],{"class":333},[327,2570,1108],{"class":337},[327,2572,1112],{"class":1111},[327,2574,2575,2577,2579,2581],{"class":329,"line":511},[327,2576,1117],{"class":337},[327,2578,1120],{"class":337},[327,2580,1123],{"class":337},[327,2582,1112],{"class":1111},[327,2584,2585,2587],{"class":329,"line":526},[327,2586,1130],{"class":337},[327,2588,1133],{"class":337},[327,2590,2591],{"class":329,"line":541},[327,2592,545],{"emptyLinePlaceholder":544},[327,2594,2595],{"class":329,"line":548},[327,2596,2597],{"class":395},"# Connexion via la VIP → doit retourner 'f' (primaire, en lecture\u002Fécriture)\n",[327,2599,2600,2602,2604,2606,2608,2610,2612,2614,2616,2618,2620,2622,2624,2626],{"class":329,"line":554},[327,2601,1147],{"class":1111},[327,2603,499],{"class":465},[327,2605,1152],{"class":465},[327,2607,1155],{"class":337},[327,2609,1152],{"class":465},[327,2611,1160],{"class":333},[327,2613,1163],{"class":337},[327,2615,392],{"class":391},[327,2617,1168],{"class":337},[327,2619,1171],{"class":337},[327,2621,1078],{"class":337},[327,2623,1176],{"class":465},[327,2625,1179],{"class":337},[327,2627,1182],{"class":465},[327,2629,2630],{"class":329,"line":572},[327,2631,545],{"emptyLinePlaceholder":544},[327,2633,2634],{"class":329,"line":589},[327,2635,2636],{"class":395},"# La VIP est bien montée sur le primaire\n",[327,2638,2639,2641,2643,2645,2647,2649,2651],{"class":329,"line":605},[327,2640,426],{"class":333},[327,2642,1198],{"class":337},[327,2644,1201],{"class":337},[327,2646,1204],{"class":337},[327,2648,1207],{"class":465},[327,2650,1210],{"class":333},[327,2652,1213],{"class":391},[15,2654,2655,2657,2658,2660],{},[217,2656,1218],{}," qui renvoie ",[217,2659,1222],{}," confirme que la VIP mène bien au primaire inscriptible.",[51,2662,2664],{"id":2663},"étape-5-pare-feu","Étape 5 — Pare-feu",[15,2666,2667],{},"Sur les 3 nœuds, restreindre les ports du cluster à son sous-réseau :",[210,2669,2671],{"className":321,"code":2670,"language":323,"meta":219,"style":219},"sudo ufw allow from 192.168.11.0\u002F24 to any port 2379,2380,5432,5433,8008 proto tcp\n# VRRP entre les nœuds :\nsudo ufw allow from 192.168.11.0\u002F24 to any proto vrrp 2>\u002Fdev\u002Fnull || \\\n  sudo ufw allow from 192.168.11.0\u002F24 to 224.0.0.18\n",[217,2672,2673,2697,2702,2730],{"__ignoreMap":219},[327,2674,2675,2677,2679,2681,2683,2685,2687,2689,2691,2693,2695],{"class":329,"line":330},[327,2676,812],{"class":333},[327,2678,1242],{"class":337},[327,2680,1245],{"class":337},[327,2682,1248],{"class":337},[327,2684,1251],{"class":337},[327,2686,1254],{"class":337},[327,2688,1257],{"class":337},[327,2690,1260],{"class":337},[327,2692,1263],{"class":337},[327,2694,1266],{"class":337},[327,2696,1269],{"class":337},[327,2698,2699],{"class":329,"line":344},[327,2700,2701],{"class":395},"# VRRP entre les nœuds :\n",[327,2703,2704,2706,2708,2710,2712,2714,2716,2718,2720,2722,2724,2726,2728],{"class":329,"line":511},[327,2705,812],{"class":333},[327,2707,1242],{"class":337},[327,2709,1245],{"class":337},[327,2711,1248],{"class":337},[327,2713,1251],{"class":337},[327,2715,1254],{"class":337},[327,2717,1257],{"class":337},[327,2719,1266],{"class":337},[327,2721,1295],{"class":337},[327,2723,1298],{"class":465},[327,2725,1301],{"class":337},[327,2727,1304],{"class":465},[327,2729,1112],{"class":1111},[327,2731,2732,2734,2736,2738,2740,2742,2744],{"class":329,"line":526},[327,2733,1311],{"class":333},[327,2735,1242],{"class":337},[327,2737,1245],{"class":337},[327,2739,1248],{"class":337},[327,2741,1251],{"class":337},[327,2743,1254],{"class":337},[327,2745,1324],{"class":391},[94,2747,2748,2756],{},[97,2749,2750],{},[100,2751,2752,2754],{},[103,2753,1333],{},[103,2755,1336],{},[113,2757,2758,2764,2773,2780],{},[100,2759,2760,2762],{},[118,2761,1343],{},[118,2763,1346],{},[100,2765,2766,2768],{},[118,2767,1351],{},[118,2769,1354,2770],{},[19,2771,2772],{},"le port exposé aux applications",[100,2774,2775,2777],{},[118,2776,1362],{},[118,2778,2779],{},"PostgreSQL interne (géré par Patroni)",[100,2781,2782,2784],{},[118,2783,1370],{},[118,2785,2786],{},"API REST Patroni (health check HAProxy)",[51,2788,2790],{"id":2789},"le-test-qui-valide-tout-le-failover","Le test qui valide tout : le failover",[15,2792,2793],{},"C'est l'intérêt même du cluster. On simule la panne du primaire :",[210,2795,2797],{"className":321,"code":2796,"language":323,"meta":219,"style":219},"# Repérer le leader\npatronictl -c \u002Fetc\u002Fpatroni\u002Fconfig.yml list\n\n# Sur le nœud leader : couper Patroni\nsudo systemctl stop patroni\n\n# Quelques secondes plus tard, sur un autre nœud :\npatronictl -c \u002Fetc\u002Fpatroni\u002Fconfig.yml list   # → un réplica est devenu Leader\n",[217,2798,2799,2804,2814,2818,2823,2833,2837,2842],{"__ignoreMap":219},[327,2800,2801],{"class":329,"line":330},[327,2802,2803],{"class":395},"# Repérer le leader\n",[327,2805,2806,2808,2810,2812],{"class":329,"line":344},[327,2807,1075],{"class":333},[327,2809,1078],{"class":337},[327,2811,1081],{"class":337},[327,2813,1084],{"class":337},[327,2815,2816],{"class":329,"line":511},[327,2817,545],{"emptyLinePlaceholder":544},[327,2819,2820],{"class":329,"line":526},[327,2821,2822],{"class":395},"# Sur le nœud leader : couper Patroni\n",[327,2824,2825,2827,2829,2831],{"class":329,"line":541},[327,2826,812],{"class":333},[327,2828,1416],{"class":337},[327,2830,1419],{"class":337},[327,2832,1422],{"class":337},[327,2834,2835],{"class":329,"line":548},[327,2836,545],{"emptyLinePlaceholder":544},[327,2838,2839],{"class":329,"line":554},[327,2840,2841],{"class":395},"# Quelques secondes plus tard, sur un autre nœud :\n",[327,2843,2844,2846,2848,2850,2852],{"class":329,"line":572},[327,2845,1075],{"class":333},[327,2847,1078],{"class":337},[327,2849,1081],{"class":337},[327,2851,1442],{"class":337},[327,2853,2854],{"class":395},"   # → un réplica est devenu Leader\n",[15,2856,2857,2858,2861,2862,2864,2865,2867],{},"Ce qui se passe en quelques secondes, sans intervention : Patroni perd le bail (",[31,2859,2860],{},"lease",") du leader dans etcd → un réplica se porte candidat et est élu → il est promu primaire → son API répond désormais ",[217,2863,233],{}," sur ",[217,2866,1454],{}," → HAProxy redirige le trafic → la VIP reste sur un nœud où HAProxy est sain. Les applications, connectées à la VIP, ne voient qu'une brève coupure.",[15,2869,2870],{},"On réintègre l'ancien primaire :",[210,2872,2874],{"className":321,"code":2873,"language":323,"meta":219,"style":219},"sudo systemctl start patroni   # il revient comme Replica et se resynchronise\n",[217,2875,2876],{"__ignoreMap":219},[327,2877,2878,2880,2882,2884,2886],{"class":329,"line":330},[327,2879,812],{"class":333},[327,2881,1416],{"class":337},[327,2883,1472],{"class":337},[327,2885,1475],{"class":337},[327,2887,2888],{"class":395},"   # il revient comme Replica et se resynchronise\n",[43,2890],{},[46,2892,2894],{"id":2893},"récapitulatif","Récapitulatif",[94,2896,2897,2907],{},[97,2898,2899],{},[100,2900,2901,2904],{},[103,2902,2903],{},"Brique",[103,2905,2906],{},"Sans elle…",[113,2908,2909,2917,2925,2932,2939],{},[100,2910,2911,2914],{},[118,2912,2913],{},"PostgreSQL + réplication",[118,2915,2916],{},"pas de copie de la donnée",[100,2918,2919,2922],{},[118,2920,2921],{},"etcd (quorum 3)",[118,2923,2924],{},"pas d'accord sur qui est primaire → split-brain",[100,2926,2927,2929],{},[118,2928,148],{},[118,2930,2931],{},"pas d'élection ni de failover automatiques",[100,2933,2934,2936],{},[118,2935,161],{},[118,2937,2938],{},"les clients ne savent pas qui est primaire",[100,2940,2941,2943],{},[118,2942,1533],{},[118,2944,2945],{},"la chaîne de connexion devrait changer à chaque bascule",[15,2947,2948,2949,1543,2952,2954,2955,2957,2958,26],{},"Pour adapter à votre environnement, vous ne touchez qu'à ",[19,2950,2951],{},"un seul fichier",[217,2953,287],{},") : sous-réseau, IP, interface, secrets. Le reste — certificats, configuration des quatre services, planification des sauvegardes — est généré par ",[217,2956,306],{},", qui est idempotent : en cas d'erreur, corrigez et relancez ",[217,2959,1552],{},[15,2961,2962],{},[19,2963,2964],{},"Pièges à retenir :",[246,2966,2967,2970,2975,2979,2984,2987],{},[62,2968,2969],{},"Ordre obligatoire : db1 → certs → db2 → db3.",[62,2971,2972,2974],{},[217,2973,438],{}," correct, sinon la VIP ne monte pas.",[62,2976,2977,2266],{},[217,2978,744],{},[62,2980,2981,2983],{},[217,2982,287],{}," identique sur les 3 nœuds.",[62,2985,2986],{},"Horloge synchronisée (NTP) sur les trois.",[62,2988,2989],{},"Nombre de nœuds impair (3 minimum).",[1585,2991,2992],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}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 .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}",{"title":219,"searchDepth":344,"depth":344,"links":2994},[2995,3002,3008,3016],{"id":1664,"depth":344,"text":1665,"children":2996},[2997,2998,2999,3000,3001],{"id":1668,"depth":511,"text":1669},{"id":1704,"depth":511,"text":1705},{"id":1785,"depth":511,"text":1786},{"id":1808,"depth":511,"text":1809},{"id":1834,"depth":511,"text":1835},{"id":1867,"depth":344,"text":1868,"children":3003},[3004,3005,3006,3007],{"id":1927,"depth":511,"text":1928},{"id":1998,"depth":511,"text":1999},{"id":2278,"depth":511,"text":2279},{"id":2291,"depth":511,"text":2292},{"id":2338,"depth":344,"text":2339,"children":3009},[3010,3011,3012,3013,3014,3015],{"id":2353,"depth":511,"text":2354},{"id":2410,"depth":511,"text":2411},{"id":2463,"depth":511,"text":2464},{"id":2525,"depth":511,"text":2526},{"id":2663,"depth":511,"text":2664},{"id":2789,"depth":511,"text":2790},{"id":2893,"depth":344,"text":2894},"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.",{},"\u002Fdatabases\u002Fpostgres\u002Fsetup-postgres-ha-cluster.fr",{"title":1630,"description":3017},"databases\u002Fpostgres\u002F1.setup-postgres-ha-cluster.fr",[122,1622,148,135,161,174],[1624,1625],"_QZDLr394smLN9BgOiGsAAQtnD1gzCAr0u30auOrbfw",[3026,3029],{"title":3027,"path":3028},"Backup and Restore MongoDB in a Docker Environment","\u002Fdatabases\u002Fmongo\u002Fbackup-and-restore-mongodb-in-docker-environment",{"title":3030,"path":3031},"Deploy Express.js API using Docker","\u002Fdocker\u002Fdeploy-expressjs-api-using-docker",[3033,3046],{"path":3034,"title":3035,"description":3036,"date":3037,"tags":3038,"topics":3043},"\u002Fdatabases\u002Fmongo\u002Fset-up-mongodb-replica-with-docker","Why I built my own MongoDB image to run a replica set in production","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.","2025-12-13",[3039,3040,3041,3042],"MongoDB","Replica-Set","Docker","Docker-compose",[1624,3044,3045],"docker","mongodb",{"path":3028,"title":3027,"description":3047,"date":3048,"tags":3049,"topics":3052},"How to create a full backup of a MongoDB database running in a Docker container and restore the backup to a new MongoDB container.","2024-07-18",[3039,3041,3042,3050,3051],"Backup","Restore",[1624,3044,3045],1783419615571]