postgresql: add cutover staging compose (ADR-0001 Phase 3). Lives in cutover/ subdir — deliberately excluded from stack-deploy.sh folder merge. Deployed only by cutover script as separate postgresqlha stack.
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-07-28 11:07:03 -07:00
parent 87ae54fb3f
commit f4f0749969
@@ -0,0 +1,200 @@
# ─────────────────────────────────────────────────────────────────────────
# postgresql/cutover/postgresql-ha-staging.yaml — ADR-0001 Phase 3, Stage 2
#
# Deployed by postgres-ha-cutover.sh as a SEPARATE stack:
# docker stack deploy -c postgresql/cutover/postgresql-ha-staging.yaml postgresqlha
#
# ⚠️ NEVER deploy this via stack-deploy.sh or under the "postgresql" stack
# name. It lives in cutover/ (a subdirectory) deliberately:
# stack-deploy.sh folder mode merges all *.yml/yaml at -maxdepth 1 of
# postgresql/, and merging this into the production stack would cause
# an outage. Subdirectory files are excluded from that merge.
#
# What this stage does:
# - Joins the EXISTING postgresql_db-backend overlay (external) — the
# live single-instance stack is untouched and keeps the "postgresql"
# and "db" aliases.
# - patroni-0/patroni-1 bootstrap a NEW cluster (scope: postgres-ha) by
# CLONING the live instance via pg_basebackup (CLONE_WITH_BASEBACKUP,
# CLONE_HOST=postgresql → resolves to the OLD instance during staging).
# CLONE_* is set on BOTH patroni nodes: whichever wins the initial
# leader race performs the clone; the other syncs from it.
# - HAProxy runs WITHOUT production aliases and WITHOUT published ports.
# It only gets those in postgresql-ha-final.yaml at promote time.
# - Superuser is PGadmin (matches the cloned data's existing superuser,
# same password secret), NOT Spilo's default "postgres".
# ─────────────────────────────────────────────────────────────────────────
version: "3.6"
services:
etcd-1:
image: quay.io/coreos/etcd:v3.5.9
hostname: etcd-1
command:
- etcd
- --name=etcd-1
- --data-dir=/etcd-data
- --initial-advertise-peer-urls=http://etcd-1:2380
- --listen-peer-urls=http://0.0.0.0:2380
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://etcd-1:2379
- --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
- --initial-cluster-state=new
- --initial-cluster-token=postgresql-ha-etcd
volumes:
- /volume1/docker/PostgreSQL/etcd-1-data:/etcd-data
networks:
- postgresql_db-backend
deploy:
placement:
constraints:
- node.hostname == docker-1
etcd-2:
image: quay.io/coreos/etcd:v3.5.9
hostname: etcd-2
command:
- etcd
- --name=etcd-2
- --data-dir=/etcd-data
- --initial-advertise-peer-urls=http://etcd-2:2380
- --listen-peer-urls=http://0.0.0.0:2380
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://etcd-2:2379
- --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
- --initial-cluster-state=new
- --initial-cluster-token=postgresql-ha-etcd
volumes:
- /volume1/docker/PostgreSQL/etcd-2-data:/etcd-data
networks:
- postgresql_db-backend
deploy:
placement:
constraints:
- node.hostname == docker-2
etcd-3:
image: quay.io/coreos/etcd:v3.5.9
hostname: etcd-3
command:
- etcd
- --name=etcd-3
- --data-dir=/etcd-data
- --initial-advertise-peer-urls=http://etcd-3:2380
- --listen-peer-urls=http://0.0.0.0:2380
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://etcd-3:2379
- --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
- --initial-cluster-state=new
- --initial-cluster-token=postgresql-ha-etcd
volumes:
- /volume1/docker/PostgreSQL/etcd-3-data:/etcd-data
networks:
- postgresql_db-backend
deploy:
placement:
constraints:
- node.hostname == docker-3
patroni-0:
image: ghcr.io/zalando/spilo-17:4.0-p3
hostname: patroni-0
command:
- /bin/sh
- -c
- |
export PGPASSWORD_SUPERUSER="$(cat /run/secrets/postgresql_password)"
export PGPASSWORD_STANDBY="$(cat /run/secrets/postgresql_replication_password)"
export PATRONI_RESTAPI_PASSWORD="$(cat /run/secrets/postgresql_patroni_password)"
export CLONE_PASSWORD="$(cat /run/secrets/postgresql_password)"
exec /bin/sh /launch.sh init
environment:
SCOPE: postgres-ha
PATRONI_NAME: patroni-0
ETCD_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"'
PGUSER_SUPERUSER: PGadmin
PGUSER_STANDBY: standby
PATRONI_RESTAPI_USERNAME: patroni
PGROOT: /home/postgres/pgdata/pgroot
# Clone-from-live: only runs when PGDATA is empty (first bootstrap).
# "postgresql" resolves to the OLD single instance during staging.
CLONE_METHOD: CLONE_WITH_BASEBACKUP
CLONE_SCOPE: legacy-single
CLONE_HOST: postgresql
CLONE_PORT: "5432"
CLONE_USER: PGadmin
secrets:
- postgresql_password
- postgresql_replication_password
- postgresql_patroni_password
volumes:
- /volume1/docker/PostgreSQL/patroni-0-data:/home/postgres/pgdata
networks:
- postgresql_db-backend
deploy:
placement:
constraints:
- node.labels.pg-role == primary
patroni-1:
image: ghcr.io/zalando/spilo-17:4.0-p3
hostname: patroni-1
command:
- /bin/sh
- -c
- |
export PGPASSWORD_SUPERUSER="$(cat /run/secrets/postgresql_password)"
export PGPASSWORD_STANDBY="$(cat /run/secrets/postgresql_replication_password)"
export PATRONI_RESTAPI_PASSWORD="$(cat /run/secrets/postgresql_patroni_password)"
export CLONE_PASSWORD="$(cat /run/secrets/postgresql_password)"
exec /bin/sh /launch.sh init
environment:
SCOPE: postgres-ha
PATRONI_NAME: patroni-1
ETCD_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"'
PGUSER_SUPERUSER: PGadmin
PGUSER_STANDBY: standby
PATRONI_RESTAPI_USERNAME: patroni
PGROOT: /home/postgres/pgdata/pgroot
CLONE_METHOD: CLONE_WITH_BASEBACKUP
CLONE_SCOPE: legacy-single
CLONE_HOST: postgresql
CLONE_PORT: "5432"
CLONE_USER: PGadmin
secrets:
- postgresql_password
- postgresql_replication_password
- postgresql_patroni_password
volumes:
- /volume1/docker/PostgreSQL/patroni-1-data:/home/postgres/pgdata
networks:
- postgresql_db-backend
deploy:
placement:
constraints:
- node.labels.pg-role == replica
# Staging HAProxy: NO production aliases, NO published ports. Reachable
# in-network as "haproxy" for pre-promote testing only.
haproxy:
image: haproxy:2.9-alpine
hostname: haproxy
volumes:
- /volume1/docker/compose-files/postgresql/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
networks:
- postgresql_db-backend
deploy:
mode: global
networks:
postgresql_db-backend:
external: true
secrets:
postgresql_password:
external: true
postgresql_replication_password:
external: true
postgresql_patroni_password:
external: true