# ============================================================================ # DIDI PostgreSQL HA cluster — Patroni + etcd + HAProxy (IaC, reproducible) # # Containerized mirror of the production topology (see ../MIGRATION.md): # prod: 3× PG/Patroni (10.11.50.160-162) + 3× etcd (163-165) # + 2× HAProxy (166/169, VIP 167) + pgBackRest (168) # here: 3× Spilo (Patroni+PG, Zalando) + 3× etcd + 1× HAProxy # → same failover semantics, single-host footprint for # demo/recepție/DR-rehearsal. # # Endpoints (identical contract to production): # localhost:5000 → leader (read-write) [HAProxy checks Patroni /primary] # localhost:5001 → replicas (read-only) [HAProxy checks Patroni /replica] # localhost:7000 → HAProxy stats UI # # Usage: # docker compose up -d # # wait ~30s for leader election, then: # psql -h localhost -p 5000 -U postgres # password: $PG_SUPERUSER_PASSWORD # # restore DIDI schema: # psql -h localhost -p 5000 -U postgres -f ../DIDI_full_export_2026-07-02.sql # # failover drill: # docker compose stop $(docker compose ps --format '{{.Name}}' | head -1) # # → a replica is promoted in seconds; :5000 keeps serving writes. # ============================================================================ x-etcd-common: &etcd-common image: quay.io/coreos/etcd:v3.5.16 restart: unless-stopped networks: [didi-ha] environment: &etcd-env ETCD_INITIAL_CLUSTER: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 ETCD_INITIAL_CLUSTER_STATE: new ETCD_INITIAL_CLUSTER_TOKEN: didi-pg-ha ETCD_AUTO_COMPACTION_RETENTION: "1" ETCD_ENABLE_V2: "true" x-spilo-common: &spilo-common image: ghcr.io/zalando/spilo-16:3.3-p3 restart: unless-stopped networks: [didi-ha] environment: &spilo-env SCOPE: didi # Patroni cluster name (etcd namespace) PGVERSION: "16" ETCD3_HOSTS: "'etcd1:2379','etcd2:2379','etcd3:2379'" PGPASSWORD_SUPERUSER: ${PG_SUPERUSER_PASSWORD:-didi-super-secret} PGPASSWORD_ADMIN: ${PG_ADMIN_PASSWORD:-didi-admin-secret} PGPASSWORD_STANDBY: ${PG_STANDBY_PASSWORD:-didi-standby-secret} ALLOW_NOSSL: "true" healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:8008/health || exit 1"] interval: 10s timeout: 5s retries: 6 start_period: 60s services: etcd1: <<: *etcd-common container_name: didi-ha-etcd1 command: etcd --name etcd1 --listen-peer-urls http://0.0.0.0:2380 --initial-advertise-peer-urls http://etcd1:2380 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://etcd1:2379 etcd2: <<: *etcd-common container_name: didi-ha-etcd2 command: etcd --name etcd2 --listen-peer-urls http://0.0.0.0:2380 --initial-advertise-peer-urls http://etcd2:2380 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://etcd2:2379 etcd3: <<: *etcd-common container_name: didi-ha-etcd3 command: etcd --name etcd3 --listen-peer-urls http://0.0.0.0:2380 --initial-advertise-peer-urls http://etcd3:2380 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://etcd3:2379 pg-node1: <<: *spilo-common container_name: didi-ha-pg1 hostname: pg-node1 depends_on: [etcd1, etcd2, etcd3] volumes: [pg1-data:/home/postgres/pgdata] pg-node2: <<: *spilo-common container_name: didi-ha-pg2 hostname: pg-node2 depends_on: [etcd1, etcd2, etcd3] volumes: [pg2-data:/home/postgres/pgdata] pg-node3: <<: *spilo-common container_name: didi-ha-pg3 hostname: pg-node3 depends_on: [etcd1, etcd2, etcd3] volumes: [pg3-data:/home/postgres/pgdata] haproxy: image: haproxy:2.9-alpine container_name: didi-ha-haproxy restart: unless-stopped networks: [didi-ha] depends_on: [pg-node1, pg-node2, pg-node3] ports: - "5000:5000" # read-write → Patroni leader - "5001:5001" # read-only → replicas - "7000:7000" # stats UI volumes: - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro volumes: pg1-data: pg2-data: pg3-data: networks: didi-ha: name: didi-ha