[{"data":1,"prerenderedAt":153},["ShallowReactive",2],{"seo-verification":3,"blog-postgresql-high-availability-with-patroni-on-vps-en":6},{"google":4,"bing":5},"EycwPY2XMyTkVzas3n1ygeNJFGAH513qrMjfDljzsMQ","",{"id":7,"slug":8,"slugs":9,"title":12,"excerpt":13,"readTime":14,"views":15,"isPinned":16,"publishedAt":17,"category":18,"categories":24,"featuredImage":26,"bgImage":27,"posterImage":28,"relatedSolution":26,"intro":29,"sections":30,"ctaTitle":103,"ctaBody":104,"ctaButton":105,"ctaUrl":106,"relatedPosts":107},290,"postgresql-high-availability-with-patroni-on-vps",{"fr":10,"en":8,"ar":11},"postgresql-haute-disponibilite-patroni-vps","توافر-postgresql-عال-مع-patroni-على-خادم-vps","PostgreSQL High Availability with Patroni on VPS","3-node PostgreSQL cluster with Patroni 4.1 and etcd 3.6: automatic failover under 30 seconds, synchronous replication, no managed service required.",12,0,false,"2026-08-21T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},6,"Databases","bases-de-donnees","bg-teal-500\u002F10 text-teal-400","database",[25],{"id":19,"name":20,"slug":21,"color":22,"icon":23},null,"\u002Fblog\u002Fcovers\u002Fbg.svg","\u002Fblog\u002Fcovers\u002Fpostgresql-haute-disponibilite-patroni-vps-poster.svg","A multi-tenant SaaS or critical application cannot afford a database outage. Amazon RDS and Aurora solve the problem, but their billing model grows with the load. Patroni 4.1 combined with etcd 3.6 delivers the same availability level on three root-access VPS instances: automatic failover in under 30 seconds, configurable synchronous replication, and a REST API to manage the cluster from a terminal. This guide covers the full installation, failover demonstration, and daily operations.",[31,35,45,48,51,59,87,90,93,97,100],{"type":32,"title":33,"body":34},"h2","Why PostgreSQL high availability on VPS — and not RDS","Amazon RDS Multi-AZ handles failover well, but its pricing model is designed so that the bill grows non-linearly with usage: storage, provisioned IOPS, simultaneous connections, and Multi-AZ replication itself are billed separately. For a SaaS with a growing database, the extra cost quickly exceeds what a dedicated VPS would cost.\n\nOn three root-access VPS instances, Patroni provides the same service: a single leader accepts writes, two synchronous or asynchronous replicas follow it, and etcd holds the quorum. If the leader goes down, Patroni detects the absence via the etcd lease (configurable TTL, typically 30 seconds) and promotes the most up-to-date replica. No human intervention, no manual DNS update needed if a load balancer like \u003Ca href=\"\u002Fblog\u002Fdeployer-avec-haproxy-vps\">HAProxy\u003C\u002Fa> points to Patroni's health endpoint.",{"type":36,"title":37,"items":38},"ul","What this cluster gives you",[39,40,41,42,43,44],"**Automatic failover under 30 seconds** — Patroni detects leader loss via etcd lease expiry and promotes without intervention.","**Configurable synchronous replication** — `synchronous_mode: true` guarantees no committed transaction is lost if the leader crashes.","**Built-in REST API** — `GET \u002Fleader`, `GET \u002Fhealth`, `POST \u002Fswitchover`: the cluster can be queried and managed without a PostgreSQL client.","**Fixed, predictable cost** — three fixed-resource VPS instances, no billing surprises tied to traffic.","**Free extensions** — `pg_hba.conf`, `postgresql.conf`, `pgvector`, `PostGIS`: no restrictions imposed by a managed service.","**Centralised backups** — pgBackRest 2.59 integrates natively with Patroni for incremental backups from the replica.",{"type":32,"title":46,"body":47},"Cluster architecture: three nodes, one quorum","The cluster rests on three layers:\n\n**etcd** holds the distributed configuration store (DCS). It owns the leader lease. If the Patroni leader does not renew this lease within the TTL, etcd releases it and standbys run for election. With three etcd nodes (one per VPS), the quorum tolerates the loss of one node without losing availability.\n\n**Patroni** runs on each VPS alongside PostgreSQL. It handles cluster initialisation, `postgresql.conf` and `pg_hba.conf` configuration, replication lag tracking, and failover. It exposes a REST API on port 8008.\n\n**PostgreSQL** is managed entirely by Patroni — never edit `postgresql.conf` directly; all changes go through `patronictl edit-config` to stay synchronised across all three nodes.\n\nThe replication flow: the leader receives writes as WAL, replicas connect via `pg_basebackup` on first start then follow the WAL stream continuously. In synchronous mode, the leader waits for at least one replica's acknowledgement before returning `COMMIT` to the client.",{"type":32,"title":49,"body":50},"Prerequisites: resources and network","This guide was written with Patroni 4.1.5, etcd 3.6.6, and PostgreSQL 17 on Debian 12.",{"type":36,"title":52,"items":53},"Minimum recommended resources per node",[54,55,56,57,58],"**2 vCPU \u002F 4 GB RAM** — sufficient to start; plan for 8 GB RAM once the database exceeds a few GB of `shared_buffers`.","**NVMe SSD** — WAL replication is sensitive to write latency; a spinning disk degrades the replication lag.","**Private network between the three nodes** — etcd-to-etcd and Patroni-to-PostgreSQL communication must not transit over the internet.","**Dedicated IPv4** — for external client access and the `pg_hba.conf` of replicas.","**NTP synchronised (chrony) — drift \u003C 1 s** — etcd refuses quorum if a node's clock drifts by more than one second. This is the most common pitfall on VPS.",{"type":60,"title":61,"steps":62},"steps","Installation: from scratch to a running cluster",[63,66,69,72,75,78,81,84],{"title":64,"body":65},"Synchronise the clock on all three nodes","On each node, install and enable `chrony` before anything else:\n\n```bash\napt install -y chrony\nsystemctl enable --now chronyd\nchronyc tracking\n```\n\nVerify that `System time offset` is under 0.1 seconds. A drift above 1 second causes etcd timeouts and election loops.",{"title":67,"body":68},"Install etcd 3.6 on all three nodes","Set the environment variables specific to each node (replace `NODE1_IP`, `NODE2_IP`, `NODE3_IP` with private IPs):\n\n```bash\nETCD_VER=v3.6.6\ncurl -L https:\u002F\u002Fgithub.com\u002Fetcd-io\u002Fetcd\u002Freleases\u002Fdownload\u002F${ETCD_VER}\u002Fetcd-${ETCD_VER}-linux-amd64.tar.gz \\\n  | tar xz -C \u002Fusr\u002Flocal\u002Fbin --strip-components=1 etcd-${ETCD_VER}-linux-amd64\u002Fetcd \\\n                                                   etcd-${ETCD_VER}-linux-amd64\u002Fetcdctl\n```\n\nCreate `\u002Fetc\u002Fetcd\u002Fetcd.conf.yml` on each node (example for `pg-node1`):\n\n```bash\nname: pg-node1\ndata-dir: \u002Fvar\u002Flib\u002Fetcd\nlisten-peer-urls: http:\u002F\u002FNODE1_IP:2380\nlisten-client-urls: http:\u002F\u002FNODE1_IP:2379,http:\u002F\u002F127.0.0.1:2379\ninitial-advertise-peer-urls: http:\u002F\u002FNODE1_IP:2380\nadvertise-client-urls: http:\u002F\u002FNODE1_IP:2379\ninitial-cluster: pg-node1=http:\u002F\u002FNODE1_IP:2380,pg-node2=http:\u002F\u002FNODE2_IP:2380,pg-node3=http:\u002F\u002FNODE3_IP:2380\ninitial-cluster-token: pg-cluster-token\ninitial-cluster-state: new\n```\n\nCreate the systemd unit, enable and start etcd on all three nodes before moving to the next step.",{"title":70,"body":71},"Verify etcd quorum","On any node:\n\n```bash\netcdctl --endpoints=http:\u002F\u002FNODE1_IP:2379,http:\u002F\u002FNODE2_IP:2379,http:\u002F\u002FNODE3_IP:2379 \\\n  endpoint status --write-out=table\n```\n\nWait until three rows appear — one showing `IS LEADER` true, the other two false — and `ERRORS` is empty. If a node is missing, check the firewall on ports 2379 and 2380.",{"title":73,"body":74},"Install PostgreSQL and Patroni","On all three nodes:\n\n```bash\n# PostgreSQL from the official PGDG repository\napt install -y curl ca-certificates\ncurl -fsSL https:\u002F\u002Fwww.postgresql.org\u002Fmedia\u002Fkeys\u002FACCC4CF8.asc | gpg --dearmor -o \u002Fetc\u002Fapt\u002Ftrusted.gpg.d\u002Fpostgresql.gpg\necho \"deb https:\u002F\u002Fapt.postgresql.org\u002Fpub\u002Frepos\u002Fapt bookworm-pgdg main\" > \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fpgdg.list\napt update && apt install -y postgresql-17\n\n# Patroni and the etcd driver\npip3 install patroni[etcd3] psycopg2-binary\n```\n\nStop PostgreSQL — Patroni takes over cluster initialisation:\n\n```bash\nsystemctl stop postgresql\nsystemctl disable postgresql\n```",{"title":76,"body":77},"Configure Patroni on each node","Create `\u002Fetc\u002Fpatroni\u002Fpatroni.yml` (example for `pg-node1`):\n\n```yaml\nscope: pg-cluster\nnamespace: \u002Fservice\u002F\nname: pg-node1\n\nrestapi:\n  listen: NODE1_IP:8008\n  connect_address: NODE1_IP:8008\n\netcd3:\n  hosts: NODE1_IP:2379,NODE2_IP:2379,NODE3_IP:2379\n\nbootstrap:\n  dcs:\n    ttl: 30\n    loop_wait: 10\n    retry_timeout: 10\n    maximum_lag_on_failover: 1048576\n    synchronous_mode: true\n    synchronous_node_count: 1\n    postgresql:\n      use_pg_rewind: true\n      use_slots: true\n      parameters:\n        wal_level: replica\n        hot_standby: \"on\"\n        wal_keep_size: 128MB\n        max_wal_senders: 10\n        max_replication_slots: 10\n\n  initdb:\n    - encoding: UTF8\n    - data-checksums\n\n  pg_hba:\n    - host replication replicator 0.0.0.0\u002F0 scram-sha-256\n    - host all all 0.0.0.0\u002F0 scram-sha-256\n\npostgresql:\n  listen: NODE1_IP:5432\n  connect_address: NODE1_IP:5432\n  data_dir: \u002Fvar\u002Flib\u002Fpostgresql\u002F17\u002Fmain\n  bin_dir: \u002Fusr\u002Flib\u002Fpostgresql\u002F17\u002Fbin\n  authentication:\n    replication:\n      username: replicator\n      password: 'YOUR_REPLICATION_PASSWORD'\n    superuser:\n      username: postgres\n      password: 'YOUR_POSTGRES_PASSWORD'\n```\n\nAdapt `NODE1_IP` for each node.",{"title":79,"body":80},"Start Patroni and initialise the cluster","Create the systemd unit `\u002Fetc\u002Fsystemd\u002Fsystem\u002Fpatroni.service`:\n\n```ini\n[Unit]\nDescription=Patroni PostgreSQL HA\nAfter=network.target etcd.service\nRequires=etcd.service\n\n[Service]\nType=simple\nUser=postgres\nExecStart=\u002Fusr\u002Flocal\u002Fbin\u002Fpatroni \u002Fetc\u002Fpatroni\u002Fpatroni.yml\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n```\n\nStart on `pg-node1` first (this node runs `initdb` and becomes the leader), then on the other two with a few seconds gap:\n\n```bash\nsystemctl daemon-reload\nsystemctl enable --now patroni\n```\n\nFollow the initialisation:\n\n```bash\npatronictl -c \u002Fetc\u002Fpatroni\u002Fpatroni.yml list\n```",{"title":82,"body":83},"Verify the initial cluster state","Expected output after full initialisation:\n\n```bash\n+ Cluster: pg-cluster (7234567890123456789) +---------+----+-----------+\n| Member    | Host             | Role         | State   | TL | Lag in MB |\n+-----------+------------------+--------------+---------+----+-----------+\n| pg-node1  | NODE1_IP:5432    | Leader       | running |  1 |           |\n| pg-node2  | NODE2_IP:5432    | Sync Standby | running |  1 | 0         |\n| pg-node3  | NODE3_IP:5432    | Replica      | running |  1 | 0         |\n+-----------+------------------+--------------+---------+----+-----------+\n```\n\n`pg-node2` shows as `Sync Standby`: every committed transaction on the leader is guaranteed to be on this node before `COMMIT` is returned to the client.",{"title":85,"body":86},"Configure pg_hba.conf via Patroni","Never edit `pg_hba.conf` directly. Use `patronictl edit-config` to add access rules in the `pg_hba` section — Patroni propagates the configuration across all nodes and reloads PostgreSQL automatically:\n\n```bash\npatronictl -c \u002Fetc\u002Fpatroni\u002Fpatroni.yml edit-config\n```\n\nAdd your rules in the `pg_hba` YAML block. On VPS, `host all all 0.0.0.0\u002F0 scram-sha-256` is a starting point to refine based on your private network.",{"type":32,"title":88,"body":89},"Failover and switchover: demonstration","**Simulated failover — abrupt leader shutdown.**\n\nBefore stopping, note the cluster state:\n\n```bash\npatronictl -c \u002Fetc\u002Fpatroni\u002Fpatroni.yml list\n# → pg-node1 is Leader, pg-node2 is Sync Standby\n```\n\nStop Patroni on the leader:\n\n```bash\nsystemctl stop patroni   # on pg-node1\n```\n\nWatch the promotion on one of the standbys:\n\n```bash\npatronictl -c \u002Fetc\u002Fpatroni\u002Fpatroni.yml list\n# → (after 10 to 30 seconds)\n# pg-node2 : Leader  | running | TL 2\n# pg-node3 : Replica | running | TL 2 | 0 MB\n# pg-node1 : stopped\n```\n\nPatroni waits for the etcd lease to expire (TTL = 30 s), then `pg-node2` (the sync standby) acquires the lease and promotes itself. The actual delay is typically between 10 and 30 seconds depending on `loop_wait`.\n\n**Planned switchover — zero-downtime switch.**\n\nFor scheduled maintenance, prefer `switchover`, which waits for the target replica to be caught up before switching:\n\n```bash\npatronictl -c \u002Fetc\u002Fpatroni\u002Fpatroni.yml switchover pg-cluster \\\n  --master pg-node1 --candidate pg-node2\n```\n\nPatroni waits for zero lag, signals `pg-node2` to promote, then `pg-node1` reconnects as a replica. Effective duration: under 5 seconds under normal conditions.\n\n**Status REST API.**\n\nWithout a PostgreSQL client, query the state from a load balancer or monitoring script:\n\n```bash\ncurl -s http:\u002F\u002FNODE1_IP:8008\u002Fleader    # 200 = this is the leader\ncurl -s http:\u002F\u002FNODE2_IP:8008\u002Freplica   # 200 = this is a healthy replica\ncurl -s http:\u002F\u002FNODE1_IP:8008\u002Fhealth    # JSON: state, role, lag\n```\n\nHAProxy can point its health checks to `\u002Fleader` and `\u002Freplica` to route writes and reads to the right nodes. See the \u003Ca href=\"\u002Fblog\u002Fdeployer-avec-haproxy-vps\">HAProxy on VPS\u003C\u002Fa> guide for the full wiring.",{"type":32,"title":91,"body":92},"Daily operations","**Backups with pgBackRest 2.59.**\n\nInstall pgBackRest on all three nodes and designate a shared repository (S3 object storage, NFS, or a dedicated local directory). The recommended setup pulls backups from a replica to avoid loading the leader:\n\n```bash\npgbackrest --stanza=pg-cluster --type=full backup\n```\n\nEnable compression and daily incremental backups in `pgbackrest.conf` (`repo1-retention-full=7`). See the \u003Ca href=\"\u002Fblog\u002Fsauvegardes-restic-vps\">backups on VPS\u003C\u002Fa> guide for complementary strategies.\n\n**Cluster monitoring.**\n\n`patronictl list` shows the lag in MB per replica. Alert when the lag exceeds a threshold (e.g. 50 MB): this signals either a slow replica or a network issue. The `GET \u002Fpatroni` endpoint returns a full JSON payload including `xlog_location` and `replication_state`.\n\n**Vertical scaling.**\n\nTo increase a node's resources: stop Patroni on that node (it becomes a disconnected replica), resize the VPS, restart. Patroni reconnects and catches up automatically via `pg_rewind` or `pg_basebackup` depending on how far behind it is.\n\n**`synchronous_commit` trade-off.**\n\nWith `synchronous_mode: true`, each `COMMIT` waits for the sync standby's acknowledgement. On a local private network, each `COMMIT` waits for the sync standby's acknowledgement — the delay depends on the network latency between nodes (monitor with `pg_stat_replication.replay_lag`). On a wider network (nodes in different datacenters), this latency can impact write-intensive applications. In that case, switch to `synchronous_mode: false` with asynchronous replication: you lose the zero-data-loss guarantee on leader crash, but writes remain fast. This trade-off must be documented explicitly in your configuration.",{"type":94,"title":95,"body":96},"tip","Hardening: mutual TLS auth between nodes","By default, etcd communication and PostgreSQL replication connections travel in plaintext over the private network. On a shared network or multi-tenant environment, enable mutual TLS auth.\n\nFor etcd, generate a CA and per-node certificates, then add to `etcd.conf.yml`:\n\n```yaml\nclient-transport-security:\n  cert-file: \u002Fetc\u002Fetcd\u002Ftls\u002Fserver.crt\n  key-file: \u002Fetc\u002Fetcd\u002Ftls\u002Fserver.key\n  trusted-ca-file: \u002Fetc\u002Fetcd\u002Ftls\u002Fca.crt\n  client-cert-auth: true\npeer-transport-security:\n  cert-file: \u002Fetc\u002Fetcd\u002Ftls\u002Fpeer.crt\n  key-file: \u002Fetc\u002Fetcd\u002Ftls\u002Fpeer.key\n  trusted-ca-file: \u002Fetc\u002Fetcd\u002Ftls\u002Fca.crt\n  peer-client-cert-auth: true\n```\n\nFor PostgreSQL replication, use `sslmode=verify-full` in Patroni's `primary_conninfo` connection parameters. Each replica then verifies the leader's certificate.",{"type":32,"title":98,"body":99},"Troubleshooting: the five most common errors","**1. etcd quorum lost — cluster refuses to elect a leader.**\n\nSymptom: `patronictl list` shows all nodes as `running` but no `Leader`. Cause: one etcd node is unreachable and the quorum (2 of 3) is no longer met. Check with `etcdctl endpoint status` — the failing node appears with no response or a connection error. Fix the node or temporarily remove it from the cluster (`etcdctl member remove`).\n\n**2. NTP drift — election loop.**\n\nSymptom: the leader changes every 30 seconds, Patroni logs show `failed to update leader key`. Cause: one node's clock is drifting by more than one second. Check with `chronyc tracking` on each node and fix before restarting Patroni.\n\n**3. Potential split-brain — `pg_rewind` refuses to apply.**\n\nSymptom: a former leader restarts and Patroni refuses to rejoin it as a replica, with error `could not connect to the target server: pg_rewind target server must be in standby mode`. The server kept writing after losing the lease. Fix: `pg_rewind --target-pgdata=\u002Fvar\u002Flib\u002Fpostgresql\u002F17\u002Fmain --source-server='host=NEW_LEADER_IP ...'`, then restart Patroni.\n\n**4. Peer connection rejected — missing `pg_hba.conf` entry.**\n\nSymptom: replication initialises but fails with `FATAL: no pg_hba.conf entry for replication connection`. The rule `host replication replicator 0.0.0.0\u002F0 scram-sha-256` is not present in the `pg_hba` section of `patroni.yml`. Add it via `patronictl edit-config` — not directly in `pg_hba.conf`.\n\n**5. Persistent lag after failover — insufficient `max_wal_senders`.**\n\nSymptom: the replica shows a lag that does not decrease after promotion. Common cause: `max_wal_senders` is too low (default of 10 on some versions) and the replication slot is saturated. Increase to 20 via `patronictl edit-config` (parameter `max_wal_senders`) and reload.",{"type":32,"title":101,"body":102},"A cluster that is operated, not improvised","Patroni 4.1 with etcd 3.6 covers most of what a managed service offers on availability: automatic election, synchronous replication, management API. The difference is control: root access, free extensions, fixed cost, and the ability to debug the failing node instead of waiting for third-party support.\n\nThe operational prerequisite is not Patroni's complexity — the procedure above shows it is manageable. It is discipline on three points: NTP synchronised, backups verified regularly, and a failover runbook tested before the outage happens.\n\nTo get started, a 3-node Patroni cluster requires three VPS instances with root access, a dedicated IPv4, and a private network. See the basic installation guide \u003Ca href=\"\u002Fblog\u002Fheberger-postgresql-vps\">PostgreSQL on VPS\u003C\u002Fa> and the comparison \u003Ca href=\"\u002Fblog\u002Fpostgresql-self-hosted-vs-rds\">self-hosted vs Amazon RDS\u003C\u002Fa> to choose the approach that fits your workload.","Three VPS for a Patroni cluster","A 3-node PostgreSQL Patroni cluster requires three VPS instances with root access, dedicated IPv4, and a private network. All ServOrbit VPS templates deliver full root access and a private network between instances.","ServOrbit VPS Cloud","\u002Fvps-cloud",[108,125,140],{"id":109,"slug":110,"slugs":111,"title":114,"excerpt":115,"readTime":116,"views":15,"isPinned":16,"publishedAt":117,"category":118,"categories":119,"featuredImage":26,"bgImage":27,"posterImage":121,"relatedSolution":122},56,"postgresql-on-a-vps-a-reliable-and-controlled-database",{"fr":112,"en":110,"ar":113},"heberger-postgresql-vps","postgresql-على-خادم-vps-قاعدة-بيانات-موثوقة-ومتحكم-بها","PostgreSQL on a VPS: a reliable and controlled database","Host PostgreSQL on a VPS: volumes, backups, restricted network access and sound configuration for your applications.",4,"2026-04-25T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},[120],{"id":19,"name":20,"slug":21,"color":22,"icon":23},"\u002Fblog\u002Fcovers\u002Fheberger-postgresql-vps-poster.svg",{"categorySlug":123,"appSlug":124},"databases","postgresql-stack",{"id":126,"slug":127,"slugs":128,"title":131,"excerpt":132,"readTime":133,"views":134,"isPinned":16,"publishedAt":135,"category":136,"categories":137,"featuredImage":26,"bgImage":27,"posterImage":139,"relatedSolution":26},239,"self-hosted-postgresql-vs-amazon-rds-roi-comparison",{"fr":129,"en":127,"ar":130},"postgresql-self-hosted-vs-rds","postgresql-sur-vps-vs-amazon-rds-مقارنة-التكلفة-2026","Self-Hosted PostgreSQL vs Amazon RDS: ROI Comparison","Real cost of PostgreSQL on a VPS vs Amazon RDS in 2026: figures, configuration, troubleshooting and migration guide.",9,2,"2026-08-09T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},[138],{"id":19,"name":20,"slug":21,"color":22,"icon":23},"\u002Fblog\u002Fcovers\u002Fpostgresql-self-hosted-vs-rds-poster.svg",{"id":141,"slug":142,"slugs":143,"title":146,"excerpt":147,"readTime":116,"views":15,"isPinned":16,"publishedAt":148,"category":149,"categories":150,"featuredImage":26,"bgImage":27,"posterImage":152,"relatedSolution":26},225,"postgresql-end-of-life-plan-your-major-upgrade",{"fr":144,"en":142,"ar":145},"postgresql-fin-de-vie-planifier-montee-version","postgresql-ونهاية-الدعم-تخطيط-الترقية-الكبرى","PostgreSQL end of life: plan your major upgrade","PostgreSQL supports each major version for five years, ending in November. Find out which one you run and plan the upgrade without losing data.","2026-08-05T00:00:00+00:00",{"id":19,"name":20,"slug":21,"color":22,"icon":23},[151],{"id":19,"name":20,"slug":21,"color":22,"icon":23},"\u002Fblog\u002Fcovers\u002Fpostgresql-fin-de-vie-planifier-montee-version-poster.svg",1787581011064]