Skip to content

This is the multi-page printable view of this section. .

Return to the regular view of this page.

Concepts

Understand Pigsty’s core concepts, architecture design, learn how high availability, backup recovery, iac, security works

Pigsty is a portable, extensible, open-source PostgreSQL distribution. It uses declarative configuration and automation to build production database services in local environments. Its tools, scripts, and operating practices support PostgreSQL services at enterprise RDS levels.

Pigsty means PostgreSQL In Great STYle. It also means Postgres, Infras, Graphics, Service, Toolbox, it’s all Yours. It provides self-hosted PostgreSQL with graphical monitoring. Find source code on GitHub and information in the official documentation. Try the Web UI in the online demo.

pigsty-banner


Why Pigsty? What Can It Do?

PostgreSQL provides the database engine. Production service also requires high availability, backup and recovery, monitoring and alerts, access control, tuning, extensions, connection pools, and load balancing.

Pigsty automates this operational work.

Pigsty provides:

  • Ready-to-Use PostgreSQL Distribution

    Pigsty integrates 600 extensions for distributed, time-series, geographic, spatial, graph, vector, search, and other multi-modal workloads. It provides production database services for PostgreSQL 14–18 on EL, Debian, and Ubuntu.

  • Self-Healing High Availability Architecture

    The high availability architecture uses Patroni, Etcd, and HAProxy for automatic hardware failover and traffic routing. Primary failure recovery has RTO < 45s and RPO ≈ 0. Cluster maintenance and upgrades can proceed in sequence without application coordination.

  • Complete Point-in-Time Recovery Capability

    pgBackRest and an optional Silo object-storage cluster provide point-in-time recovery (PITR). Recover quickly to a selected time after software defects or accidental data deletion.

  • Flexible Service Access and Traffic Management

    HAProxy, Pgbouncer, and VIP provide service access with read/write separation, connection pools, and automatic routing. These components provide stable, high-performance database access with transaction pooling.

  • Monitoring and Diagnostics

    VictoriaMetrics and Grafana provide monitoring practices with more than three thousand metric types. Views range from global dashboards to CRUD operations on individual objects.

  • Declarative Configuration Management

    Pigsty follows Infrastructure as Code. Describe the required environment in declarative configuration. The system applies the changes needed to reach that state.

  • Modular Architecture Design

    The modular architecture supports different deployments. Combine the core PostgreSQL module with optional Redis, MINIO (Silo), and Etcd modules. Pigsty also supports PG-compatible kernels and operating modes.

  • Solid Security Best Practices

    Security features include a self-signed CA, encrypted communication, AES-encrypted backups, and SCRAM-SHA-256 password hashes. Pigsty also supplies an ACL model and HBA rules that use least privilege.

  • Simple and Easy Deployment

    Prepackaged dependencies support one-step installation without Internet access. Local sandboxes can run on VMs with 1 core and 2GB RAM, with the same functions as production environments. Use Vagrant for local sandboxes or Terraform for cloud deployments.


What Pigsty Is Not

Pigsty is not a complete PaaS (Platform as a Service) system.

  • Supply your own hardware resources. Pigsty runs on physical servers, VMs, or cloud instances that you provide. It does not create or manage them. Terraform templates help prepare cloud resources.

  • Pigsty runs on the operating system. It does not require Kubernetes or Docker, but can coexist with them. Its Docker module runs stateless applications.

  • Pigsty focuses on PostgreSQL. It also supports components such as Redis, Etcd, and Silo, but PostgreSQL remains its core.

  • Pigsty avoids vendor lock-in. It uses open-source components, preserves the PostgreSQL kernel, and adds no proprietary protocols. You can continue to use managed PostgreSQL clusters after removing Pigsty.

You can choose how to build and operate your database services:

  • Override any default parameter or configuration template.
  • Use the declarative API or manage components directly with Ansible, Patroni, pgBackRest, and other tools.
  • Manage the full database lifecycle, or use monitoring alone for existing instances and RDS.

Pigsty operates at the database service layer. It combines existing tools to deliver PostgreSQL services on your hardware.


Evolution of PostgreSQL Deployment

The following comparison explains the PostgreSQL deployment approaches that Pigsty addresses.

Manual Deployment Era

Traditional deployment requires DBAs to install PostgreSQL, configure replication and monitoring, and handle failures manually. This has several costs:

  • Repeated work: Each instance needs manual operations, which can introduce errors.
  • Inconsistent configuration: Different DBA practices make maintenance harder.
  • Longer recovery: Manual failure handling takes time and can introduce human errors.
  • Limited monitoring: Separate monitoring systems make detection and diagnosis harder.

Managed Database Era

Cloud providers offer managed databases (RDS) to reduce this work. These services also have constraints:

  • Cost: Service fees typically range from several times to dozens of times the hardware cost.
  • Vendor lock-in: Dependence on one cloud platform complicates migration.
  • Feature limits: Advanced features, extensions, and parameter changes can be restricted.
  • Data control: Cloud storage reduces direct control over data.

Local RDS Era

Pigsty provides a third option: local database services with capabilities comparable to or greater than cloud RDS.

This approach combines:

  • Automation: One-step deployment, automatic configuration, and automatic failure recovery provide convenience comparable to cloud RDS.
  • Control: Run on your infrastructure and retain control of your data.
  • Cost: Operate enterprise database services at costs close to the hardware cost.
  • Features: Use PostgreSQL capabilities and ecosystem extensions without imposed restrictions.
  • Portability: Open-source components allow migration without vendor lock-in.

This approach is particularly suitable for:

  • Private and hybrid clouds: Enterprises needing to run databases in local environments.
  • Cost-sensitive users: Organizations looking to reduce database TCO.
  • High-security scenarios: Critical data requiring complete autonomy and control.
  • PostgreSQL power users: Scenarios requiring advanced features and rich extensions.
  • Development and testing: Quickly setting up databases locally that match production environments.

What’s Next

Continue with these topics:

1 - Architecture

Pigsty’s modular architecture—declarative composition, on-demand customization, flexible deployment.

Pigsty uses a modular architecture and declarative interface. Combine modules to meet your requirements.


Modules

Pigsty uses a modular design with six main default modules: PGSQL, INFRA, NODE, ETCD, REDIS, and MINIO.

  • PGSQL: Self-healing HA Postgres clusters powered by Patroni, Pgbouncer, HAproxy, PgBackrest, and more.
  • INFRA: Local software repo, Nginx, Grafana, Victoria, AlertManager, Blackbox Exporter—the complete observability stack.
  • NODE: Tune nodes to desired state—hostname, timezone, NTP, ssh, sudo, haproxy, docker, vector, keepalived.
  • ETCD: Distributed key-value store as DCS for HA Postgres clusters: consensus leader election/config management/service discovery.
  • REDIS: Redis servers supporting standalone primary-replica, sentinel, and cluster modes with full monitoring.
  • MINIO: S3-compatible simple object storage that can serve as an optional backup destination for PG databases.

Combine modules through declarative configuration. For host monitoring, install INFRA on infrastructure nodes and NODE on managed nodes. Install ETCD and PGSQL across multiple nodes to form an HA PostgreSQL cluster automatically. You can reuse Pigsty infrastructure for custom modules; see REDIS and MINIO as examples. Compatibility modes, such as PostgreSQL Mongo mode, combine standard PGSQL and Docker APP workflows.

Every module requires NODE. Install it first on managed nodes. By default, NODE uses the local software repository and thus has a weak dependency on INFRA. The deploy.yml playbook bootstraps administration/infrastructure nodes with INFRA to resolve this circular dependency.

pigsty-sandbox


Standalone Installation

By default, Pigsty installs on one physical or virtual node. deploy.yml installs INFRA, ETCD, PGSQL, and optional MINIO on the current node. Monitoring includes VictoriaMetrics, VictoriaLogs, VictoriaTraces, Grafana, Alertmanager, and Blackbox Exporter. A standalone PostgreSQL instance serves as the CMDB: cluster pg-meta, database meta.

This node has monitoring, visualization, and PostgreSQL with automatic PITR configuration. One node cannot provide HA. Use it for development, tests, demonstrations, and data visualization or analysis. It can also serve as an administration node for additional nodes.

pigsty-arch


Monitoring

Use the standalone meta node as an administration node and monitoring center for additional nodes and database servers.

You can use Pigsty monitoring independently. It configures VictoriaMetrics/Grafana with dashboards for host nodes and PostgreSQL databases. A small configuration enables production monitoring and alerts for existing hosts and databases, including those managed outside Pigsty.

pigsty-dashboard.jpg


HA PostgreSQL Clusters

Pigsty lets you operate your own production HA PostgreSQL RDS service in your chosen environment.

To create an HA cluster, describe it in configuration and run the playbook:

pg-test:
  hosts:
    10.10.10.11: { pg_seq: 1, pg_role: primary }
    10.10.10.12: { pg_seq: 2, pg_role: replica }
    10.10.10.13: { pg_seq: 3, pg_role: replica }
  vars: { pg_cluster: pg-test }
$ bin/pgsql-add pg-test  # Initialize cluster 'pg-test'

In less than 10 minutes, the cluster has service access, monitoring, backup PITR, and HA configured.

pigsty-ha.png

Patroni, etcd, and HAProxy provide automatic recovery from hardware failure. By default, primary failover completes within 45 seconds. HAProxy uses Patroni health checks to route read/write traffic to the new primary and avoid split brain. Clients need no configuration change or application restart. Replica failures and planned switchovers cause only a brief interruption to the current query.

pgBackRest and optional Silo provide local and cloud PITR for software failures, human errors, and datacenter disasters. They also support cross-region replication and disaster recovery after datacenter failure.

1.1 - Nodes

A node is an abstraction of hardware/OS resources—physical machines, bare metal, VMs, or containers/pods.

A node represents hardware resources and an operating system. It can be a physical server, virtual machine, container, or pod.

A node needs a Linux OS, systemd, and standard CPU, memory, disk, and network resources.

Install modules on nodes. The installed modules determine the node type:

TypeDescription
Regular NodeA node managed by Pigsty
ADMIN NodeThe node that runs Ansible to issue management commands
INFRA NodeNodes with the INFRA module installed
ETCD NodeNodes with the ETCD module for DCS
MINIO NodeNodes with the MINIO module for object storage
PGSQL NodeNodes with the PGSQL module installed
…Nodes with other modules…

In a single-node deployment, one node has all roles: regular, administration, infrastructure, ETCD, and database.


Regular Node

Use node.yml to configure managed nodes to the target state. A regular node can run these services:

ComponentPortDescriptionStatus
node_exporter9100Host metrics exporterEnabled
haproxy9101HAProxy load balancer (admin port)Enabled
vector9598Log collection agentEnabled
docker9323Container runtime supportOptional
keepalivedn/aL2 VIP for node clusterOptional
keepalived_exporter9650Keepalived status monitorOptional

node_exporter, vector, and haproxy are enabled by default. They export host metrics, collect logs, and balance traffic. Docker, keepalived, and keepalived_exporter are optional.


ADMIN Node

A deployment has exactly one administration node that runs Ansible playbooks and issues deployment or management commands.

This node has ssh/sudo access to every other node. Restrict access carefully. See Security Model: Trust Boundaries for its trusted access and critical assets.

During single-node installation and configuration, the current node becomes the administration node. A laptop with Ansible and SSH access to all managed nodes can also serve this role. This is not recommended for production.

For example, a laptop managing a cloud Pigsty VM is the administration node.

Production installations usually reserve 1–2 DBA machines for administration. With limited resources, INFRA nodes can also perform administration. All infrastructure nodes have Ansible by default.


INFRA Node

A Pigsty deployment may have 1 or more INFRA nodes; large production environments typically have 2-3.

The infra group in the inventory defines which nodes are INFRA nodes. These nodes run the INFRA module with these components:

ComponentPortDescription
nginx80/443Web UI, local software repository
grafana3000Visualization platform
victoriaMetrics8428Time-series database (metrics)
victoriaLogs9428Log collection server
victoriaTraces10428Trace collection server
vmalert8880Alerting and derived metrics
alertmanager9059Alert aggregation and routing
blackbox_exporter9115Blackbox probing (ping nodes/VIPs)
dnsmasq53Internal DNS resolution
chronyd123NTP time server
ansible-Playbook execution

Nginx provides the module Web UI and software repository. Services on multiple infrastructure nodes are independent. Each Grafana can access all monitoring data sources.

Pigsty is licensed under Apache-2.0, though embedded Grafana component uses AGPLv3.


ETCD Node

The ETCD module provides Distributed Consensus Service (DCS) for PostgreSQL high availability.

The etcd group in the inventory defines ETCD nodes. These nodes run etcd servers on two ports:

ComponentPortDescription
etcd2379ETCD key-value store (client port)
etcd2380ETCD cluster peer communication

MINIO Node

The MINIO module provides optional backup storage for PostgreSQL.

The minio inventory group defines MINIO module nodes. In v4.5.0, these nodes run Silo servers on:

ComponentPortDescription
silo9000S3 API endpoint
silo9001Silo admin console

PGSQL Node

A PGSQL node has the PGSQL module. Each node runs one PostgreSQL instance, giving a 1:1 mapping.

node_id_from_pg lets a PGSQL node use its PostgreSQL instance name. The default is true.

PGSQL nodes run these additional components beyond regular node services:

ComponentPortDescriptionStatus
postgres5432PostgreSQL database serverEnabled
pgbouncer6432PgBouncer connection poolEnabled
patroni8008Patroni HA managementEnabled
pg_exporter9630PostgreSQL metrics exporterEnabled
pgbouncer_exporter9631PgBouncer metrics exporterEnabled
pgbackrest_exporter9854pgBackRest metrics exporterEnabled
vip-managern/aBinds L2 VIP to cluster primaryOptional
{{ pg_cluster }}-primary5433HAProxy service: pooled read/writeEnabled
{{ pg_cluster }}-replica5434HAProxy service: pooled read-onlyEnabled
{{ pg_cluster }}-default5436HAProxy service: primary direct connectionEnabled
{{ pg_cluster }}-offline5438HAProxy service: offline readEnabled
{{ pg_cluster }}-<service>543xHAProxy service: custom PostgreSQL servicesCustom

vip-manager runs only when a PG VIP is configured. Define custom services through pg_services. haproxy exposes them on additional ports.


Node Relationships

Regular nodes normally select an INFRA node with admin_ip. For example, global admin_ip = 10.10.10.10 directs nodes to services at that address.

Parameters that reference ${admin_ip}:

ParameterModuleDefault ValueDescription
repo_endpointINFRAhttp://${admin_ip}:80Software repo URL
repo_upstream.baseurlINFRAhttp://${admin_ip}/pigstyLocal repo baseurl
infra_portal.endpointINFRA${admin_ip}:<port>Nginx proxy backend
dns_recordsINFRA["${admin_ip} i.pigsty", ...]DNS records
node_default_etc_hostsNODE["${admin_ip} i.pigsty"]Default static DNS
node_etc_hostsNODE-Custom static DNS
node_dns_serversNODE["${admin_ip}"]Dynamic DNS servers
node_ntp_serversNODE-NTP servers (optional)

The administration node usually also provides infrastructure. With multiple infrastructure nodes, the first usually administers the environment and others provide backups.

Large deployments can separate administration from infrastructure. For example, use 1–2 small DBA-managed administration hosts and 2–3 high-capacity physical monitoring servers.

Typical node counts by deployment scale:

ScaleADMININFRAETCDMINIOPGSQL
Single-node11101
3-node13303
Small prod1230N
Large prod2354+N

1.2 - Infrastructure

Infrastructure module architecture, components, and functionality in Pigsty.

Production HA PostgreSQL needs monitoring, alerts, logs, time synchronization, DNS, and software repositories. The INFRA module provides these services. It is optional but strongly recommended.


Overview

The following single-node deployment diagram shows INFRA components on the right:

ComponentTypeDescription
NginxWeb ServerUnified entry for WebUI, local repo, reverse proxy for internal services
RepoSoftware RepoAPT/DNF repository with all RPM/DEB packages needed for deployment
GrafanaVisualizationDisplays metrics, logs, and traces; hosts dashboards, reports, and custom data apps
VictoriaMetricsTime Series DBScrapes all metrics, Prometheus API compatible, provides VMUI query interface
VictoriaLogsLog PlatformCentralized log storage; all nodes run Vector by default, pushing logs here
VictoriaTracesTracingCollects slow SQL, service traces, and other tracing data
VMAlertEval Rule/AlertEvaluates alerting rules, pushes events to Alertmanager
AlertManagerAlert ManagerAggregates alerts, dispatches notifications via email, Webhook, etc.
BlackboxExporterBlackbox ProbeProbes reachability of IPs/VIPs/URLs
DNSMASQDNS ServiceProvides DNS resolution for domains used within Pigsty [Optional]
ChronydTime SyncProvides NTP time synchronization to ensure consistent time across nodes [Optional]
CACertificateIssues encryption certificates within the environment
AnsibleOrchestrationBatch, declarative, agentless tool for managing large numbers of servers

pigsty-arch


Nginx

Nginx provides access to Pigsty WebUI services. HTTP and HTTPS use 80 and 443 by default. See the Live Demo.

IP Access (replace)Domain (HTTP)Domain (HTTPS)Public Demo
http://10.10.10.10http://i.pigstyhttps://i.pigstyhttps://demo.pigsty.io

Nginx exposes the Grafana, VictoriaMetrics (VMUI), AlertManager, and HAProxy web interfaces. It also serves the local software repository and other static files.

Nginx configures local web servers or reverse proxy servers based on definitions in infra_portal.

infra_portal:
  home : { domain: i.pigsty }

The default administration homepage is i.pigsty. Its endpoints proxy these components:

EndpointComponentNative PortNotesPublic Demo
/Nginx80/443Homepage, local repo, file serverdemo.pigsty.io
/ui/Grafana3000Grafana dashboard entrydemo.pigsty.io/ui/
/vmetrics/VictoriaMetrics8428Time series DB Web UIdemo.pigsty.io/vmetrics/
/vlogs/VictoriaLogs9428Log DB Web UIdemo.pigsty.io/vlogs/
/vtraces/VictoriaTraces10428Tracing Web UIdemo.pigsty.io/vtraces/
/vmalert/VMAlert8880Alert rule managementdemo.pigsty.io/vmalert/
/alertmgr/AlertManager9059Alert management Web UIdemo.pigsty.io/alertmgr/
/blackbox/Blackbox9115Blackbox probe

Pigsty online demo homepage

Configure Nginx as a file server or reverse proxy. HTTPS supports self-signed and publicly trusted certificates.

For more information, see: Tutorial: Nginx—Expose Web Services via Proxy and Tutorial: Certbot—Request and Renew HTTPS Certificates


Repo

During installation, Pigsty creates a local software repository on the infrastructure node to speed later installations. See the Live Demo.

The default directory is /www/pigsty. Nginx serves it at /pigsty:

For offline installation, copy a prepared local software repository into the target environment. If /www/pigsty/repo_complete exists, deployment skips upstream downloads and uses that repository. sow generates this file as a completion marker and SHA-256 manifest. To force a rebuild, run ./infra.yml -t repo_build -e repo_build=true.

repo

For more information, see: Config: INFRA - REPO


Grafana

Grafana visualizes Pigsty metrics, logs, and other monitoring data. See the Live Demo.

Grafana listens on port 3000 by default and is proxied via Nginx at the /ui path:

IP Access (replace)Domain (HTTP)Domain (HTTPS)Public Demo
http://10.10.10.10/uihttp://i.pigsty/uihttps://i.pigsty/uihttps://demo.pigsty.io/ui

Pigsty includes dashboards for VictoriaMetrics, Logs, and Traces. URL links let you move between summary and detail views for diagnosis.

Grafana also supports low-code visualization. Pigsty installs ECharts, victoriametrics-datasource, and victorialogs-datasource plugins by default. Vector/Victoria data sources use the names vmetrics-*, vlogs-*, and vtraces-* for custom dashboards.

dashboard

For more information, see: Config: INFRA - GRAFANA.


VictoriaMetrics

VictoriaMetrics collects and stores Pigsty monitoring metrics as time-series data. See the Live Demo.

The default port is 8428. Access it through Nginx at /vmetrics or the p.pigsty domain:

VictoriaMetrics supports the Prometheus API, PromQL, remote read/write protocols, and the Alertmanager API. Its VMUI supports direct queries for metric exploration. It also supplies data to Grafana.

vmetrics

For more information, see: Config: INFRA - VMETRICS


VictoriaLogs

VictoriaLogs centrally stores structured logs from all Pigsty nodes. See the Live Demo.

It listens on port 9428 by default, mounted at Nginx /vlogs path:

Managed nodes run Vector by default. It collects system, PostgreSQL, Patroni, Pgbouncer, and other logs, structures them, and sends them to VictoriaLogs. The built-in Web UI supports search and filtering. Use the Grafana victorialogs-datasource plugin for visual analysis.

vlogs

For more information, see: Config: INFRA - VLOGS


VictoriaTraces

VictoriaTraces is used for collecting trace data and slow SQL records. Live Demo

It listens on port 10428 by default, mounted at Nginx /vtraces path:

VictoriaTraces provides a Jaeger-compatible interface for service traces and slow SQL queries. Use it with Grafana to locate performance limits and trace their causes.

For more information, see: Config: INFRA - VTRACES


VMAlert

VMAlert evaluates alert rules and sends triggered events to Alertmanager. See the Live Demo.

It listens on port 8880 by default, mounted at Nginx /vmalert path:

VMAlert periodically evaluates rules against VictoriaMetrics data. Pigsty includes rules for common failures in PGSQL, NODE, REDIS, and other modules.

vmalert

For more information, see: Config: INFRA - VMALERT


AlertManager

AlertManager handles alert event aggregation, deduplication, grouping, and dispatch. Live Demo

It listens on port 9059 by default, mounted at Nginx /alertmgr path, and also accessible via the a.pigsty domain:

AlertManager supports email, Webhook, Slack, PagerDuty, WeChat Work, and other notification channels. Routing rules select destinations by severity and module. Silencing and inhibition are also supported.

alertmanager

For more information, see: Config: INFRA - AlertManager


BlackboxExporter

Blackbox Exporter probes targets to check reachability.

It listens on port 9115 by default, mounted at Nginx /blackbox path:

Probe methods include ICMP Ping, TCP ports, and HTTP/HTTPS endpoints. Check VIP reachability, service ports, and external dependencies to determine the scope of a failure.

blackbox

For more information, see: Config: INFRA - BLACKBOX


Ansible

Ansible runs Pigsty deployment, configuration, and management playbooks.

Installation adds Ansible to the administration/infrastructure node. Declarative configuration specifies the target state. Idempotent playbooks can run repeatedly to reach that state without additional side effects.

Ansible provides:

  • Agentless execution: Connect through SSH without installing an agent on managed nodes.
  • Declarative configuration: Describe the target state; configuration also documents it.
  • Idempotent operations: Repeated runs produce consistent results and can retry partial failures.

For more information, see: Playbooks: Pigsty Playbook


DNSMASQ

DNSMASQ provides DNS resolution on INFRA nodes, resolving domain names to their corresponding IP addresses.

DNSMASQ listens on port 53 (UDP/TCP) by default, providing DNS resolution for all nodes. Records are stored in the /etc/dnsmasq.d/pigsty directory.

Other modules register domain names with DNSMASQ during deployment. Client nodes can use infrastructure nodes as DNS servers and access services by name. DNS is optional; Pigsty also works without it.

For more information, see: Config: INFRA - DNS and Tutorial: DNS—Configure Domain Resolution


Chronyd

Chronyd synchronizes node clocks through NTP. Its time-source service uses UDP port 123 by default.

Distributed systems need synchronized clocks. Logs need comparable timestamps, certificate checks need accurate time, and PostgreSQL streaming replication is sensitive to clock drift. In isolated networks, other nodes can synchronize to an infrastructure node acting as their NTP server.

All nodes run chronyd by default, using public pool.ntp.org servers. The Node module manages chronyd. In isolated networks, use admin_ip to select the INFRA node as the internal time source.

For more information, see: Config: NODE - TIME


INFRA Node vs Regular Node

In Pigsty, the relationship between nodes and infrastructure is a weak circular dependency: node_monitor → infra → node

NODE does not itself require INFRA. Its node_monitor functions require the infrastructure monitoring platform and services.

infra.yml and deploy resolve this dependency through these steps:

Alternatively, use phased deployment: initialize infrastructure nodes before regular nodes.

How Are Nodes Coupled to Infrastructure?

Regular nodes use admin_ip to select an INFRA node for infrastructure services.

For example, global admin_ip = 10.10.10.10 normally directs all nodes to infrastructure at that address.

Change this address to switch infrastructure providers for many nodes. These parameters may reference ${admin_ip}:

ParameterModuleDefault ValueDescription
repo_endpointINFRAhttp://${admin_ip}:80Software repo URL
repo_upstream.baseurlINFRAhttp://${admin_ip}/pigstyLocal repo baseurl
infra_portal.endpointINFRA${admin_ip}:<port>Nginx proxy backend
dns_recordsINFRA["${admin_ip} i.pigsty", ...]DNS records
node_default_etc_hostsNODE["${admin_ip} i.pigsty"]Default static DNS
node_etc_hostsNODE[]Custom static DNS
node_dns_serversNODE["${admin_ip}"]Dynamic DNS servers
node_ntp_serversNODE["pool pool.ntp.org iburst"]NTP servers (optional)

For example, local uses the Nginx software repository at admin_ip:80/pigsty. DNS uses DNSMASQ at admin_ip:53. Both choices are optional. Nodes can install from Internet repositories, as most single-node templates do. They can also leave DNS unconfigured because Pigsty does not require it.


INFRA Node vs ADMIN Node

The ADMIN node usually also serves as the INFRA node, including in single-node deployments. With multiple infrastructure nodes, the first infra member usually administers the environment and others provide backups. You can also separate the roles.

For example, large production deployments can use 1–2 small DBA-managed VMs for administration. Use 2–3 high-capacity physical servers, or more, for monitoring infrastructure. Set admin_ip to an infrastructure node address. The parameter retains its historical name from when administration and infrastructure roles were combined.

For local management of cloud nodes, install Ansible on your laptop and select cloud servers as managed targets. The laptop is the administration node. Cloud servers provide infrastructure.

all:
  children:
    infra:   { hosts: { 10.10.10.10: { infra_seq: 1 , ansible_host: your_ssh_alias } } }  # <--- Use ansible_host to point to cloud node (fill in ssh alias)
    etcd:    { hosts: { 10.10.10.10: { etcd_seq: 1 } }, vars: { etcd_cluster: etcd } }    # SSH connection will use: ssh your_ssh_alias
    pg-meta: { hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary } }, vars: { pg_cluster: pg-meta } }
  vars:
    version: v4.5.0
    admin_ip: 10.10.10.10
    region: default

Multiple INFRA Nodes

One infrastructure node usually meets the requirements. Failure of its INFRA module does not interrupt database services on other nodes.

For higher monitoring and alerting availability, use multiple infrastructure nodes. Two nodes can provide redundancy and monitor each other. A distributed Victoria cluster can use more nodes for horizontal scaling without a fixed limit.

Each infrastructure node operates independently. Nginx uses local services, and VictoriaMetrics collects metrics from every environment service. Nodes send logs to all VictoriaLogs endpoints by default. Grafana is the exception: each instance registers every VictoriaMetrics, Logs, Traces, and PostgreSQL instance as a data source. Thus, each Grafana can access all monitoring data.

Dashboard and data-source changes affect only the Grafana instance you edit. For shared configuration, use a PostgreSQL database as Grafana storage. See Configure Grafana High Availability.

INFRA overview dashboard

1.3 - PGSQL Arch

PostgreSQL module component interactions and data flow.

PGSQL organizes PostgreSQL as clusters. Each cluster is a logical group of database instances connected through primary-replica relationships.


Overview

The PGSQL module combines these components for production HA clusters:

ComponentTypeDescription
postgresDatabaseOpen-source relational database engine for PGSQL
patroniHAManages PostgreSQL, coordinates failover, leader election, config changes
pgbouncerPoolLightweight connection pooling middleware, reduces overhead, adds flexibility
pgbackrestBackupFull/incremental backup and WAL archiving, supports local and object storage
pg_exporterMetricsExports PostgreSQL monitoring metrics in a Prometheus-compatible format
pgbouncer_exporterMetricsExports Pgbouncer connection pool metrics
pgbackrest_exporterMetricsExports backup status metrics
vip-managerVIPBinds L2 VIP to current primary node for transparent failover [Optional]

Enable vip-manager when needed. PGSQL also uses components from other modules:

ComponentModuleTypeDescription
haproxyNODELBExposes service ports, routes traffic to primary or replicas
vectorNODELoggingCollects PostgreSQL, Patroni, Pgbouncer logs and ships to center
etcdETCDDCSDistributed consistent store for cluster metadata and leader info

The PGSQL module combines the PostgreSQL engine with supporting services. Patroni and Etcd provide HA. pgBackRest and optional Silo provide backups. HAProxy, Pgbouncer, and vip-manager provide access. Exporters and Vector provide monitoring.

You can select other database kernels and extensions. The diagram represents the engine as a CPU and the complete service as a computer.

Pigsty PostgreSQL cluster architecture
SubsystemComponentsFunction
HA SubsystemPatroni + etcdFailure detection, auto-failover, config management
Access SubsystemHAProxy + Pgbouncer + vip-managerService exposure, load balancing, pooling, VIP
Backup SubsystempgBackRest (+ Silo)Full/incremental backup, WAL archiving, PITR
Observability Subsystempg_exporter / pgbouncer_exporter / pgbackrest_exporter + VectorMetrics collection, log aggregation

Component Interaction

pigsty-arch

  • Cluster DNS is resolved by DNSMASQ on infra nodes
  • Cluster VIP is managed by vip-manager, which binds pg_vip_address to the cluster primary node.
  • Cluster services are exposed by HAProxy on nodes, different services distinguished by node ports (543x).
  • Pgbouncer is connection pooling middleware, listening on port 6432 by default, buffering connections, exposing additional metrics, and providing extra flexibility.
  • PostgreSQL listens on port 5432, providing relational database services
    • Installing PGSQL module on multiple nodes with the same cluster name automatically forms an HA cluster via streaming replication
    • PostgreSQL process is managed by patroni by default.
  • Patroni listens on port 8008 by default, supervising PostgreSQL server processes
    • Patroni starts Postgres server as child process
    • Patroni uses etcd as DCS: stores config, failure detection, and leader election.
    • Patroni provides Postgres info (e.g., primary/replica) via health checks, HAProxy uses this to distribute traffic
  • pg_exporter exposes postgres monitoring metrics on port 9630
  • pgbouncer_exporter exposes pgbouncer metrics on port 9631
  • pgBackRest uses local backup repository by default (pgbackrest_method = local)
    • If using local (default), pgBackRest creates local repository under pg_fs_bkup on primary node
    • If using minio, pgBackRest creates the backup repository on dedicated Silo or an external S3 service
  • Vector collects Postgres-related logs (postgres, pgbouncer, patroni, pgbackrest)
    • vector listens on port 9598, also exposes its own metrics to VictoriaMetrics on infra nodes
    • vector sends logs to VictoriaLogs on infra nodes

HA Subsystem

Patroni and etcd provide the HA subsystem. They detect failures, coordinate failover, and manage configuration.

Operation: Patroni manages each local PostgreSQL process. It writes the leader, members, and configuration to etcd. After primary failure, Patroni coordinates an election and promotes the healthiest replica. Recovery is automatic, with RTO typically below 45 seconds.

Key Interactions:

  • PostgreSQL: Starts, stops, reloads PG as parent process, controls its lifecycle
  • etcd: External dependency, writes/watches leader key for distributed consensus and failure detection
  • HAProxy: Provides health checks via REST API (:8008), reporting instance role
  • vip-manager: Watches leader key in etcd, auto-migrates VIP

For more information, see: High Availability and Config: PGSQL - PG_BOOTSTRAP


Access Subsystem

HAProxy, Pgbouncer, and vip-manager expose services, route traffic, and pool connections.

There are multiple access methods. A typical traffic path is: Client → DNS/VIP → HAProxy (543x) → Pgbouncer (6432) → PostgreSQL (5432)

LayerComponentPortRole
L2 VIPvip-manager-Binds L2 VIP to primary (optional)
L4 Load BalHAProxy543xService exposure, load balancing, health checks
L7 PoolPgbouncer6432Connection reuse, session management, transaction pooling

Service Ports:

  • 5433 primary: Read-write service, routes to primary Pgbouncer
  • 5434 replica: Read-only service, routes to replica Pgbouncer
  • 5436 default: Default service, direct to primary (bypasses pool)
  • 5438 offline: Offline service, direct to offline replica (ETL/analytics)

Key Features:

  • HAProxy uses Patroni REST API to determine instance role, auto-routes traffic
  • Pgbouncer uses transaction-level pooling, absorbs connection spikes, reduces PG connection overhead
  • vip-manager watches etcd leader key, auto-migrates VIP during failover

For more information, see: Service Access and Config: PGSQL - PG_ACCESS


Backup Subsystem

pgBackRest provides backups and PITR. Optional Silo or external S3 storage can serve as the remote repository.

Backup Types:

  • Full backup: Complete database copy
  • Incremental/differential backup: Only backs up changed data blocks
  • WAL archiving: Continuous transaction log archiving, enables any point-in-time recovery

Storage Backends:

  • local (default): Local disk, backups stored at pg_fs_bkup mount point
  • minio: S3-compatible object storage, supports centralized backup management and off-site DR

Key Interactions:

  • pgBackRest → PostgreSQL: Executes backup commands, manages WAL archiving
  • pgBackRest → Patroni: Recovery can bootstrap replicas as new primary or standby
  • pgbackrest_exporter → VictoriaMetrics: Exports backup status metrics through the Prometheus-compatible protocol to monitor backup health

For more information, see: PITR, Backup & Recovery, and Config: PGSQL - PG_BACKUP


Observability Subsystem

Three Exporters collect metrics, and Vector collects logs.

ComponentPortTargetKey Metrics
pg_exporter9630PostgreSQLSessions, transactions, replication lag, buffer hits
pgbouncer_exporter9631PgbouncerPool utilization, wait queue, hit rate
pgbackrest_exporter9854pgBackRestLatest backup time, size, type
vector9598postgres/patroni/pgbouncer logsStructured log stream

Data Flow:

  • Metrics: Exporter → VictoriaMetrics (INFRA) → Grafana dashboards
  • Logs: Vector → VictoriaLogs (INFRA) → Grafana log queries

pg_exporter and pgbouncer_exporter use local Unix sockets, independently of HA topology. Slim installation can disable them.

For more information, see: Config: PGSQL - PG_MONITOR


PostgreSQL

PostgreSQL is the PGSQL database engine. It listens on 5432 by default, with one instance per node.

Pigsty supports PostgreSQL 14–18 through binary packages from the PGDG repository. You can replace the default engine with a PG-compatible kernel. Up to 600 extensions are available.

Patroni manages PostgreSQL processes for HA by default. A single-node cluster has one primary. Additional nodes automatically join as replicas and receive changes through real-time physical replication. Replicas serve read-only traffic and can automatically replace a failed primary.

pigsty-ha.png

You can access PostgreSQL directly, or through HAProxy and Pgbouncer connection pool.

For more information, see: Config: PGSQL - PG_BOOTSTRAP


Patroni

Patroni is the PostgreSQL HA control component, listening on port 8008 by default.

Patroni manages PostgreSQL startup, shutdown, configuration, and health. It writes leader and member records to etcd. It coordinates failover, replication factor, and parameter changes. Its REST API serves HAProxy, monitoring, and administrators.

HAProxy checks Patroni endpoints to identify instance roles and route primary or replica traffic. vip-manager watches the etcd leader key and moves the VIP when the primary changes.

patroni

For more information, see: Config: PGSQL - PG_BOOTSTRAP


Pgbouncer

Pgbouncer provides connection pooling on port 6432 by default. Each node has one Pgbouncer instance for its PostgreSQL instance.

Pgbouncer is stateless and connects to local PostgreSQL through a Unix socket. Its default transaction pooling handles connection bursts, stabilizes session counts, and reduces lock contention. This improves performance under high concurrency.

By default, read/write 5433 and read-only 5434 traffic use Pgbouncer. Default 5436 and offline 5438 services connect directly to PostgreSQL.

pgbouncer_poolmode defaults to transaction. Disable pooling with pgbouncer_enabled.

pgbouncer.png

For more information, see: Config: PGSQL - PG_ACCESS


pgBackRest

pgBackRest provides full, incremental, and differential PostgreSQL backups and WAL archiving.

Pigsty uses pgBackRest for PITR to any time within the backup retention window.

pgBackRest coordinates with PostgreSQL for backups and archiving. By default, it creates a repository on the primary: pgbackrest_method = local. Configure Silo or external S3 storage for centralized backup management.

pgbackrest_init_backup can trigger the first full backup after initialization. Recovery integrates with Patroni to initialize replicas as new primaries or standbys.

pgbackrest

For more information, see: Backup & Recovery and Config: PGSQL - PG_BACKUP


HAProxy

HAProxy is the service entry point and load balancer, exposing multiple database service ports.

PortServiceTargetDescription
9101Admin-HAProxy statistics and admin page
5433primaryPrimary PgbouncerRead-write service, routes to primary pool
5434replicaReplica PgbouncerRead-only service, routes to replica pool
5436defaultPrimary PostgresDefault service, direct to primary (bypasses pool)
5438offlineOffline PostgresOffline service, direct to offline replica (ETL/analytics)

HAProxy checks Patroni REST endpoints to identify roles and route primary or replica traffic. pg_default_services and pg_services define services.

By default, local HAProxy instances expose services. For higher traffic, use pg_service_provider to select a dedicated HAProxy node group.

haproxy

For more information, see: Service Access and Config: PGSQL - PG_ACCESS


vip-manager

vip-manager binds L2 VIP to the current primary node. This is an optional component; enable it if your network supports L2 VIP.

vip-manager runs on each PG node. It watches the leader key that Patroni writes to etcd. It binds pg_vip_address to the primary network interface. During failover, it releases the old binding and immediately binds the VIP to the new primary.

Enable this optional component with pg_vip_enabled. All nodes must share one VLAN or VIP migration fails. Public clouds usually do not support L2 VIP. Use it for on-premises or private cloud deployments.

node-vip

For more information, see: Tutorial: VIP Configuration and Config: PGSQL - PG_ACCESS


pg_exporter

pg_exporter exports PostgreSQL monitoring metrics, listening on port 9630 by default.

pg_exporter connects to local PostgreSQL through a Unix socket. It exports sessions, buffer hits, replication lag, transaction rates, and other metrics. VictoriaMetrics on infrastructure nodes collects them.

Set collection rules with pg_exporter_config. Use pg_exporter_auto_discovery for automatic database discovery. pg_exporter_cache_ttls controls different cache lifetimes.

Disable this component through parameters. Slim installation leaves it disabled.

pg-exporter

For more information, see: Config: PGSQL - PG_MONITOR


pgbouncer_exporter

pgbouncer_exporter exports Pgbouncer connection pool metrics, listening on port 9631 by default.

pgbouncer_exporter uses the pg_exporter binary with separate metric configuration. It supports pgbouncer 1.8–1.25+. Pgbouncer statistics views provide pool utilization, wait queue, and hit rate metrics.

Disabling Pgbouncer also disables its exporter. Slim installation leaves the exporter disabled.

For more information, see: Config: PGSQL - PG_MONITOR


pgbackrest_exporter

pgbackrest_exporter exports backup status metrics, listening on port 9854 by default.

pgbackrest_exporter reads pgBackRest status to report the latest backup time, size, type, and other metrics. Alert rules can detect expired or failed backups. Large repositories or many backups can make collection expensive. The default collection interval is 2 minutes, so a completed backup can take 2 minutes to appear in monitoring.

For more information, see: Config: PGSQL - PG_MONITOR


etcd

etcd is a distributed consistent store (DCS), providing cluster metadata storage and leader election capability for Patroni.

The separate ETCD module manages etcd. PGSQL relies on it for HA. Patroni stores cluster state, leader information, and configuration there so nodes can reach consensus. vip-manager reads the leader key to move the VIP automatically.

For more information, see: ETCD Module


vector

The NODE module deploys Vector to collect PostgreSQL-related logs efficiently.

Vector reads node log directories for PostgreSQL, Pgbouncer, Patroni, and pgBackRest. It sends structured records to infrastructure-node VictoriaLogs for central storage and queries.

For more information, see: NODE Module

2 - ER Model

How Pigsty abstracts different functionality into modules, and the E-R diagrams for these modules.

A deployment is the largest entity in Pigsty. This entity-relationship diagram shows its main entities and their relationships:

Pigsty full data model ER diagram

A deployment is also an environment. Examples include Production (Prod), User Acceptance Testing (UAT), Staging, Testing, and Development (Devbox). Each environment has a Pigsty inventory that describes all its entities and attributes.

An environment usually has shared infrastructure (INFRA). This includes ETCD for the HA DCS and MINIO for centralized backup storage. Infrastructure serves multiple PostgreSQL clusters and other database modules. Deployments without infrastructure are also possible.

Pigsty organizes almost all database modules into clusters. Each cluster is an Ansible group with node resources. PostgreSQL HA, Redis, Etcd, and Silo use this model. An environment can contain multiple clusters.

2.1 - E-R Model of Infra Cluster

Entity-Relationship model for INFRA infrastructure nodes in Pigsty, component composition, and naming conventions.

The INFRA module is a management hub made of infrastructure nodes, rather than a traditional cluster. It provides core services for the deployment. Each node is an autonomous service unit that runs components such as Nginx, Grafana, and VictoriaMetrics. Together, these nodes provide observability and management for database clusters.

There are two core entities in Pigsty’s INFRA module:

  • Node: A server that runs infrastructure components. It can be a physical server, VM, container, or Pod.
  • Component: An infrastructure service on a node, such as Nginx, Grafana, or VictoriaMetrics.

INFRA nodes usually also serve as administration nodes, which form the Pigsty control plane.


Component Composition

Each INFRA node runs the following core components:

ComponentPortDescription
Nginx80/443Web portal, local repo, unified reverse proxy
Grafana3000Visualization platform, dashboards, data apps
VictoriaMetrics8428Time-series database, Prometheus API compatible
VictoriaLogs9428Log database, receives structured logs from Vector
VictoriaTraces10428Trace storage for slow SQL / request tracing
VMAlert8880Alert rule evaluator based on VictoriaMetrics
Alertmanager9059Alert aggregation and dispatch
Blackbox Exporter9115ICMP/TCP/HTTP black-box probing
DNSMASQ53DNS server for internal domain resolution
Chronyd123NTP time server

These components together form Pigsty’s observability infrastructure.


Examples

This example defines a two-node INFRA deployment:

infra:
  hosts:
    10.10.10.10: { infra_seq: 1 }
    10.10.10.11: { infra_seq: 2 }

The configuration defines these entities:

GroupDescription
infraINFRA infrastructure node group
NodeDescription
infra-110.10.10.10 INFRA node #1
infra-210.10.10.11 INFRA node #2

For production, use at least two INFRA nodes for component redundancy.


Identity Parameters

The INFRA_ID parameter group assigns deterministic identities to INFRA entities. One parameter is required:

ParameterTypeLevelDescriptionFormat
infra_seqintNodeINFRA node sequence, requiredNatural number, starting from 1, unique within group

Set the node sequence number at node level. Pigsty generates unique identifiers with these rules:

EntityGeneration RuleExample
Nodeinfra-{{ infra_seq }}infra-1, infra-2

INFRA assigns infra-N identifiers to distinguish infrastructure nodes in monitoring. This does not change their hostname or system identity. Nodes retain their existing hostname or IP address for identification.


Service Portal

INFRA nodes provide web access through Nginx. infra_portal defines the services that Nginx exposes.

The default configuration only defines the home server:

infra_portal:
  home : { domain: i.pigsty }

Pigsty configures reverse proxy endpoints for enabled components, including Grafana, VictoriaMetrics, and AlertManager. To access them through separate domains, add explicit configuration:

infra_portal:
  home         : { domain: i.pigsty }
  grafana      : { domain: g.pigsty, endpoint: "${admin_ip}:3000", websocket: true }
  prometheus   : { domain: p.pigsty, endpoint: "${admin_ip}:8428" }   # VMUI
  alertmanager : { domain: a.pigsty, endpoint: "${admin_ip}:9059" }
DomainServiceDescription
i.pigstyHomePigsty homepage
g.pigstyGrafanaMonitoring dashboard
p.pigstyVictoriaMetricsTSDB Web UI
a.pigstyAlertmanagerAlert management UI

Use domain names to access Pigsty services where possible, instead of an IP address and port.


Deployment Scale

The number of INFRA nodes depends on deployment scale and HA requirements:

ScaleINFRA NodesDescription
Dev/Test1Single-node deployment, all on one node
Small Prod1-2Single or dual node, can share with other services
Medium Prod2-3Dedicated INFRA nodes, redundant components
Large Prod3+Multiple INFRA nodes, component separation

In a single-node deployment, INFRA shares a node with PGSQL, ETCD, and other modules. Small deployments usually use INFRA nodes as the administration node, backup administration node, and local software repository (/www/pigsty). Larger deployments can assign these responsibilities to dedicated nodes.


Monitoring Label System

Pigsty collects metrics from INFRA components themselves. Unlike database modules, INFRA treats each component as a separate monitoring object. The cls (class) label distinguishes component types.

LabelDescriptionExample
clsComponent type, each forming a “class”nginx
insInstance name, format {component}-{infra_seq}nginx-1
ipINFRA node IP running the component10.10.10.10
jobVictoriaMetrics scrape job, fixed as infrainfra

A two-node INFRA deployment with infra_seq: 1 and infra_seq: 2 uses these component labels:

Componentclsins ExamplePort
Nginxnginxnginx-1, nginx-29113
Grafanagrafanagrafana-1, grafana-23000
VictoriaMetricsvmetricsvmetrics-1, vmetrics-28428
VictoriaLogsvlogsvlogs-1, vlogs-29428
VictoriaTracesvtracesvtraces-1, vtraces-210428
VMAlertvmalertvmalert-1, vmalert-28880
Alertmanageralertmanageralertmanager-1, alertmanager-29059
Blackboxblackboxblackbox-1, blackbox-29115

All INFRA component metrics have job="infra". The cls label distinguishes components:

nginx_up{cls="nginx", ins="nginx-1", ip="10.10.10.10", job="infra"}
grafana_info{cls="grafana", ins="grafana-1", ip="10.10.10.10", job="infra"}
vm_app_version{cls="vmetrics", ins="vmetrics-1", ip="10.10.10.10", job="infra"}
vlogs_rows_ingested_total{cls="vlogs", ins="vlogs-1", ip="10.10.10.10", job="infra"}
alertmanager_alerts{cls="alertmanager", ins="alertmanager-1", ip="10.10.10.10", job="infra"}

2.2 - E-R Model of PostgreSQL Cluster

Entity-Relationship model for PostgreSQL clusters in Pigsty, including E-R diagram, entity definitions, and naming conventions.

PGSQL organizes PostgreSQL as clusters. Each cluster is a logical group of database instances connected through primary-replica relationships.

Each cluster is an autonomous business unit with at least one primary instance. It exposes database capabilities through services.

There are four core entities in Pigsty’s PGSQL module:

  • Cluster: An autonomous PostgreSQL business unit and the top-level namespace for other entities.
  • Service: A named abstraction that exposes capabilities and routes traffic through node ports.
  • Instance: One PostgreSQL server, including its running processes and database files, on one node.
  • Node: A hardware resource that runs Linux and Systemd. It can be a physical server, VM, container, or Pod.

Two business entities, Database and Role, complete this logical model:

er-pgsql

Examples

The following two examples show the model. The four-node Pigsty sandbox contains a three-node pg-test cluster:

    pg-test:
      hosts:
        10.10.10.11: { pg_seq: 1, pg_role: primary }
        10.10.10.12: { pg_seq: 2, pg_role: replica }
        10.10.10.13: { pg_seq: 3, pg_role: replica }
      vars: { pg_cluster: pg-test }

This configuration defines a high-availability PostgreSQL cluster with these entities:

ClusterDescription
pg-testPostgreSQL 3-node HA cluster
InstanceDescription
pg-test-1PostgreSQL instance #1, default primary
pg-test-2PostgreSQL instance #2, initial replica
pg-test-3PostgreSQL instance #3, initial replica
ServiceDescription
pg-test-primaryRead-write service (routes to primary pgbouncer)
pg-test-replicaRead-only service (routes to replica pgbouncer)
pg-test-defaultDirect read-write service (routes to primary postgres)
pg-test-offlineOffline read service (routes to dedicated postgres)
NodeDescription
node-110.10.10.11 Node #1, hosts pg-test-1 PG instance
node-210.10.10.12 Node #2, hosts pg-test-2 PG instance
node-310.10.10.13 Node #3, hosts pg-test-3 PG instance
ha

Identity Parameters

The PG_ID parameter group assigns deterministic identities to PGSQL entities. Three parameters are required:

ParameterTypeLevelDescriptionFormat
pg_clusterstringClusterPG cluster name, requiredValid DNS name, regex [a-zA-Z0-9-]+
pg_seqintInstancePG instance number, requiredNatural number, starting from 0 or 1, unique within cluster
pg_roleenumInstancePG instance role, requiredEnum: primary, replica, offline

Set the cluster name at cluster level. Set each instance number and role at instance level. Pigsty generates unique identifiers with these rules:

EntityGeneration RuleExample
Instance{{ pg_cluster }}-{{ pg_seq }}pg-test-1, pg-test-2, pg-test-3
Service{{ pg_cluster }}-{{ pg_role }}pg-test-primary, pg-test-replica, pg-test-offline
NodeExplicitly specified or borrowed from PGpg-test-1, pg-test-2, pg-test-3

Pigsty uses an exclusive 1:1 mapping between nodes and PostgreSQL instances. By default, the node inherits its PostgreSQL instance identifier through node_id_from_pg. Set nodename to override this name. Alternatively, disable nodename_overwrite to retain the current default.


Sharding Identity Parameters

To serve one business with multiple PostgreSQL clusters (sharding), use two additional identity parameters: pg_shard and pg_group.

These clusters share one pg_shard name. Each has its own pg_group number, as in this Citus cluster:

A pg_cluster name usually combines {{ pg_shard }}{{ pg_group }}, for example pg-citus0 or pg-citus1.

all:
  children:
    pg-citus0: # citus shard 0
      hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus0 , pg_group: 0 }
    pg-citus1: # citus shard 1
      hosts: { 10.10.10.11: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus1 , pg_group: 1 }
    pg-citus2: # citus shard 2
      hosts: { 10.10.10.12: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus2 , pg_group: 2 }
    pg-citus3: # citus shard 3
      hosts: { 10.10.10.13: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus3 , pg_group: 3 }

Pigsty has dedicated dashboards for horizontally sharded clusters. Use this naming convention to compare shard performance and load in those dashboards.

Other identity parameters support specific uses:

See the PG_ID parameter documentation.


Monitoring Label System

The built-in monitoring system uses these identity parameters to identify PostgreSQL entities.

pg_up{cls="pg-test", ins="pg-test-1", ip="10.10.10.11", job="pgsql"}
pg_up{cls="pg-test", ins="pg-test-2", ip="10.10.10.12", job="pgsql"}
pg_up{cls="pg-test", ins="pg-test-3", ip="10.10.10.13", job="pgsql"}

The labels cls, ins, and ip identify the cluster name, instance name, and node IP. They accompany job in all native VictoriaMetrics metrics and VictoriaLogs log streams.

These job values are fixed:

  • pgsql: PostgreSQL metrics.
  • pgrds: Remote PostgreSQL instance metrics.
  • postgres: PostgreSQL CSV logs.
  • pgbackrest: pgBackRest logs.

Other PostgreSQL components collect logs through job: syslog.

Specific entity metrics also have these identity labels:

  • datname: Database name, if a metric belongs to a specific database.
  • relname: Table name, if a metric belongs to a specific table.
  • idxname: Index name, if a metric belongs to a specific index.
  • funcname: Function name, if a metric belongs to a specific function.
  • seqname: Sequence name, if a metric belongs to a specific sequence.
  • query: Query fingerprint, if a metric belongs to a specific query.

2.3 - E-R Model of Etcd Cluster

Entity-Relationship model for ETCD clusters in Pigsty, including E-R diagram, entity definitions, and naming conventions.

The ETCD module organizes production ETCD into clusters. A cluster is a logical entity whose ETCD instances coordinate through the Raft consensus protocol.

Each cluster is an autonomous distributed key-value store with at least one ETCD instance. It provides services through client ports.

There are three core entities in Pigsty’s ETCD module:

  • Cluster: An autonomous ETCD service unit and the top-level namespace for other entities.
  • Instance: One ETCD server process on a node, participating in Raft consensus.
  • Node: An implicitly declared hardware resource that runs Linux and Systemd.

The ETCD model has no Service entities or complex Role distinctions, unlike PostgreSQL. All ETCD instances are functionally equivalent. Raft elects one Leader; the others become Followers. Non-voting Learner members are also permitted during intermediate scale-out states.


Examples

This example defines a three-node ETCD cluster:

etcd:
  hosts:
    10.10.10.10: { etcd_seq: 1 }
    10.10.10.11: { etcd_seq: 2 }
    10.10.10.12: { etcd_seq: 3 }
  vars:
    etcd_cluster: etcd

The configuration defines these entities:

ClusterDescription
etcdETCD 3-node HA cluster
InstanceDescription
etcd-1ETCD instance #1
etcd-2ETCD instance #2
etcd-3ETCD instance #3
NodeDescription
10.10.10.10Node #1, hosts etcd-1 instance
10.10.10.11Node #2, hosts etcd-2 instance
10.10.10.12Node #3, hosts etcd-3 instance

Identity Parameters

The ETCD parameter group assigns deterministic identities to ETCD entities. Two parameters are required:

ParameterTypeLevelDescriptionFormat
etcd_clusterstringClusterETCD cluster name, requiredValid DNS name, defaults to fixed etcd
etcd_seqintInstanceETCD instance number, requiredNatural number, starting from 1, unique within cluster

Set the cluster name at cluster level and the instance number at instance level. Pigsty generates unique identifiers with these rules:

EntityGeneration RuleExample
Instance{{ etcd_cluster }}-{{ etcd_seq }}etcd-1, etcd-2, etcd-3

ETCD does not assign a separate identity to host nodes. It uses their existing hostname or IP address.


Ports & Protocols

Each ETCD instance listens on the following two ports:

PortParameterPurpose
2379etcd_portClient port, accessed by Patroni, vip-manager, etc.
2380etcd_peer_portPeer communication port, used for Raft consensus

ETCD clusters enable TLS encryption and RBAC authentication by default. Clients need the correct certificates and passwords to access ETCD.


Cluster Size

ETCD cluster size determines availability. More than half the nodes, called a quorum, must be alive to maintain this distributed coordination service.

Cluster SizeQuorumFault ToleranceUse Case
1 node10Dev, test, demo
3 nodes21Small-medium production
5 nodes32Large-scale production

ETCD supports even member counts. However, they tolerate no more failures than the next smaller odd count. They also increase deployment and quorum costs. Production clusters therefore usually have one, three, or five members. Clusters with more than five members are uncommon.


Monitoring Label System

The built-in monitoring system uses these identity parameters to identify ETCD entities.

etcd_up{cls="etcd", ins="etcd-1", ip="10.10.10.10", job="etcd"}
etcd_up{cls="etcd", ins="etcd-2", ip="10.10.10.11", job="etcd"}
etcd_up{cls="etcd", ins="etcd-3", ip="10.10.10.12", job="etcd"}

The labels cls, ins, and ip identify the cluster name, instance name, and node IP. They accompany job in all ETCD metrics collected by VictoriaMetrics. The ETCD metrics job is always etcd.

2.4 - MINIO Cluster Model

The cluster, instance, and node identity model used when Pigsty’s MINIO module deploys Silo.

MINIO is the compatibility module name for object storage. Pigsty v4.5.0 deploys Silo with minio_type: silo. It groups object storage instances into a cluster.

Each cluster is an autonomous S3-compatible object store with at least one instance. It exposes services through the S3 API port.

There are three core entities in Pigsty’s MINIO module:

  • Cluster: An autonomous object storage service unit and the top-level namespace for other entities.
  • Instance: One Silo server process on a node, managing local disks.
  • Node: An implicitly declared hardware resource that runs Linux and Systemd.

Silo also retains the Storage Pool concept for expansion.


Deployment Modes

Silo supports Pigsty’s three inventory deployment modes:

ModeCodeDescriptionUse Case
Single-Node Single-DriveSNSDSingle node, single data directory or diskDev, test, demo
Single-Node Multi-DriveSNMDSingle node, multiple disks, typically 4+Resource-constrained small deployments
Multi-Node Multi-DriveMNMDMultiple nodes, multiple disks per nodeProduction recommended

For a quick SNSD experiment, you can use a regular directory. For multiple drives, use real disk mount points. Otherwise, Silo refuses to start.


Examples

This example explicitly selects Silo, the current default backend, and defines a four-node cluster with multiple drives:

minio:
  hosts:
    10.10.10.10: { minio_seq: 1 }
    10.10.10.11: { minio_seq: 2 }
    10.10.10.12: { minio_seq: 3 }
    10.10.10.13: { minio_seq: 4 }
  vars:
    minio_cluster: minio
    minio_type: silo
    minio_data: '/data{1...4}'
    minio_node: '${minio_cluster}-${minio_seq}.pigsty'

The configuration defines four Silo nodes with four disks each. Instance identifiers retain MINIO compatibility names:

ClusterDescription
minioSilo 4-node HA cluster
InstanceDescription
minio-1Object-storage instance #1, managing 4 disks
minio-2Object-storage instance #2, managing 4 disks
minio-3Object-storage instance #3, managing 4 disks
minio-4Object-storage instance #4, managing 4 disks
NodeDescription
10.10.10.10Node #1, hosts minio-1 instance
10.10.10.11Node #2, hosts minio-2 instance
10.10.10.12Node #3, hosts minio-3 instance
10.10.10.13Node #4, hosts minio-4 instance

Identity Parameters

The MINIO parameter group assigns deterministic identities to MINIO entities. Two parameters are required:

ParameterTypeLevelDescriptionFormat
minio_clusterstringClusterObject-storage cluster name, requiredValid non-empty name, no default
minio_seqintInstanceObject-storage instance number, requiredNatural number, starting from 1, unique within cluster

Set the cluster name at cluster level and the instance number at instance level. Pigsty generates unique identifiers with these rules:

EntityGeneration RuleExample
Instance{{ minio_cluster }}-{{ minio_seq }}minio-1, minio-2, minio-3, minio-4

MINIO uses the existing hostname or IP address as the host node identity. It does not assign another identity. minio_node generates internal Silo node names for cluster discovery in /etc/hosts. These names do not identify the host node.

Roles find members throughout the inventory by minio_cluster. The Ansible group name can differ from the cluster name. The retained backend selector minio_type currently must be silo.


Core Configuration Parameters

These parameters are also critical for Silo cluster configuration:

ParameterTypeDescription
minio_typeenumRetained selector; currently only silo
minio_datapathData directory, use {x...y} for multi-drive
minio_nodestringNode name pattern for multi-node deployment
minio_domainstringService domain, defaults to sss.pigsty

These parameters determine minio_volumes, which the role writes to Silo’s MINIO_VOLUMES:

  • SNSD: Direct minio_data value, e.g., /data/minio
  • SNMD: Expanded minio_data directories, e.g., /data{1...4}
  • MNMD: Combined minio_node and minio_data, e.g., https://minio-{1...4}.pigsty:9000/data{1...4}

Ports & Services

Each object-storage instance listens on the following ports:

PortParameterPurpose
9000minio_portS3 API service port
9001minio_admin_portWeb admin console port

MINIO enables HTTPS by default through minio_https. For the default pgBackRest S3 repository configuration, keep HTTPS enabled and install the Pigsty CA correctly.

Clients can reach a multi-node Silo cluster through any member. For a stable entry point, use a load balancer such as HAProxy with a VIP.


Resource Provisioning

After deployment, Pigsty creates these Silo resources. minio_provision controls this step:

Default Buckets (defined by minio_buckets):

BucketPurpose
pgsqlPostgreSQL pgBackREST backup storage
metaMetadata storage, versioning enabled
dataGeneral data storage

Default Users (defined by minio_users):

UserDefault PasswordPolicyPurpose
pgbackrestS3User.BackuppgsqlPostgreSQL backup dedicated user
s3user_metaS3User.MetametaAccess meta bucket
s3user_dataS3User.DatadataAccess data bucket

These public default credentials are only for demonstrations and local development. Replace them before production deployment.

pgbackrest serves PostgreSQL cluster backups. The reserved users s3user_meta and s3user_data are not actively used.


Monitoring Label System

Pigsty uses these identity parameters to identify object storage entities. This example shows a Silo availability series:

minio_up{cls="minio", ins="minio-1", ip="10.10.10.10", job="minio"}
minio_up{cls="minio", ins="minio-2", ip="10.10.10.11", job="minio"}
minio_up{cls="minio", ins="minio-3", ip="10.10.10.12", job="minio"}
minio_up{cls="minio", ins="minio-4", ip="10.10.10.13", job="minio"}

cls, ins, and ip identify the cluster name, instance name, and node IP. For monitoring compatibility, job="minio" remains unchanged. The current backend label is flavor=silo. See the metric list for details.

2.5 - E-R Model of Redis Cluster

Entity-Relationship model for Redis clusters in Pigsty, including E-R diagram, entity definitions, and naming conventions.

The Redis module organizes production Redis into clusters. A cluster is a logical entity with Redis instances on one or more nodes.

Each cluster is an autonomous, high-performance cache or storage unit with at least one Redis instance. It exposes services through ports.

There are three core entities in Pigsty’s Redis module:

  • Cluster: An autonomous Redis service unit and the top-level namespace for other entities.
  • Instance: One Redis server process using a specific port on a node.
  • Node: An implicitly declared hardware resource that runs Linux and Systemd. It can host multiple Redis instances.

Redis usually runs multiple instances per node, unlike PostgreSQL. This uses multiple CPU cores on a physical server or VM. Nodes and instances therefore have a 1:N relationship. For production, Redis instances with more than 12 GB of memory are generally discouraged.


Operating Modes

redis_mode selects one of three Redis modes:

ModeCodeDescriptionHA Mechanism
StandalonestandaloneClassic master-replica, default modeRequires Sentinel
SentinelsentinelHA monitoring and auto-failover for standaloneMulti-node quorum
Native ClusterclusterRedis native distributed cluster, no sentinel neededBuilt-in auto-failover
  • Standalone: The default mode. Set replication with replica_of. HA requires a separate Sentinel cluster.
  • Sentinel: Monitor standalone clusters and provide automatic failover. Sentinel stores no business data. Multiple nodes provide HA for Sentinel itself.
  • Native Cluster: Automatically shard data across multiple primaries. Each primary can have multiple replicas. Built-in HA removes the need for Sentinel.

Examples

The following examples show each mode.

Standalone Cluster

Classic master-replica on a single node:

redis-ms:
  hosts:
    10.10.10.10:
      redis_node: 1
      redis_instances:
        6379: { }
        6380: { replica_of: '10.10.10.10 6379' }
  vars:
    redis_cluster: redis-ms
    redis_password: 'redis.ms'
    redis_max_memory: 64MB
ClusterDescription
redis-msRedis standalone cluster
NodeDescription
redis-ms-110.10.10.10 Node #1, hosts 2 instances
InstanceDescription
redis-ms-1-6379Primary instance, listening on port 6379
redis-ms-1-6380Replica instance, port 6380, replicates from 6379

Sentinel Cluster

This example puts three Sentinel instances on one node. redis_sentinel_monitor specifies the standalone clusters they monitor:

redis-sentinel:
  hosts:
    10.10.10.11:
      redis_node: 1
      redis_instances: { 26379: {}, 26380: {}, 26381: {} }
  vars:
    redis_cluster: redis-sentinel
    redis_password: 'redis.sentinel'
    redis_mode: sentinel
    redis_max_memory: 16MB
    redis_sentinel_monitor:
      - { name: redis-ms, host: 10.10.10.10, port: 6379, password: redis.ms, quorum: 2 }

Native Cluster

This native distributed cluster has two nodes and six instances: the minimum specification of three primaries and three replicas.

redis-test:
  hosts:
    10.10.10.12: { redis_node: 1, redis_instances: { 6379: {}, 6380: {}, 6381: {} } }
    10.10.10.13: { redis_node: 2, redis_instances: { 6379: {}, 6380: {}, 6381: {} } }
  vars:
    redis_cluster: redis-test
    redis_password: 'redis.test'
    redis_mode: cluster
    redis_max_memory: 32MB

This creates a 3 primary 3 replica native Redis cluster.

ClusterDescription
redis-testRedis native cluster (3P3R)
InstanceDescription
redis-test-1-6379Instance on node 1, port 6379
redis-test-1-6380Instance on node 1, port 6380
redis-test-1-6381Instance on node 1, port 6381
redis-test-2-6379Instance on node 2, port 6379
redis-test-2-6380Instance on node 2, port 6380
redis-test-2-6381Instance on node 2, port 6381
NodeDescription
redis-test-110.10.10.12 Node #1, hosts 3 instances
redis-test-210.10.10.13 Node #2, hosts 3 instances

Identity Parameters

The REDIS parameter group assigns deterministic identities to Redis entities. Three parameters are required:

ParameterTypeLevelDescriptionFormat
redis_clusterstringClusterRedis cluster name, requiredValid DNS name, regex [a-z][a-z0-9-]*
redis_nodeintNodeRedis node number, requiredNatural number, starting from 1, unique within cluster
redis_instancesdictNodeRedis instance definition, requiredJSON object, key is port, value is instance config

Set the cluster name at cluster level. Set the node number and instance definitions at node level. Pigsty generates these unique identifiers:

EntityGeneration RuleExample
Instance{{ redis_cluster }}-{{ redis_node }}-{{ port }}redis-ms-1-6379, redis-ms-1-6380

Redis uses the existing hostname or IP address as the host node identity. It does not assign another identity. redis_node names instances, not host nodes.


Instance Definition

redis_instances is a JSON object. Each key is a port number; its value is the instance configuration:

redis_instances:
  6379: { }                                      # Primary instance, no extra config
  6380: { replica_of: '10.10.10.10 6379' }       # Replica, specify upstream primary
  6381: { replica_of: '10.10.10.10 6379' }       # Replica, specify upstream primary

Each Redis instance must use a unique port on its node. Avoid reserved ports below 1024 and conflicts with Pigsty ports. In standalone mode, replica_of defines replication. Use format '<ip> <port>' to specify the upstream primary address and port.

Each Redis node also runs Redis Exporter to collect metrics from all local instances:

PortParameterPurpose
9121redis_exporter_portRedis Exporter port

The Redis deployment model has these limits:

  • Node Exclusive: A node can belong to only one Redis cluster at a time.
  • Port Unique: Instances on the same node must use different ports.
  • Password Shared: Instances on the same node must share a password because of a redis_exporter limitation.
  • Manual HA: Standalone clusters require additional Sentinel configuration for automatic failover.

Monitoring Label System

The built-in monitoring system uses these identity parameters to identify Redis entities.

redis_up{cls="redis-ms", ins="redis-ms-1-6379", ip="10.10.10.10", job="redis"}
redis_up{cls="redis-ms", ins="redis-ms-1-6380", ip="10.10.10.10", job="redis"}

The labels cls, ins, and ip identify the cluster name, instance name, and node IP. They accompany job in all Redis metrics collected by VictoriaMetrics. The Redis metrics job is always redis.

3 - Infra as Code

Pigsty uses Infrastructure as Code (IaC) philosophy to manage all components, providing declarative management for large-scale clusters.

Pigsty uses IaC and GitOps. Describe the environment in a declarative inventory. Apply it with idempotent playbooks.

Declare the target state with parameters. Playbooks can repeatedly adjust nodes to that state. This resembles Kubernetes CRDs and Operators, implemented through Ansible on physical servers and VMs.

Pigsty was designed to manage large PostgreSQL deployments. Its target was to reproduce more than 100 database clusters, PG/Redis services, and monitoring on prepared servers within ten minutes. Manual GUI operations cannot complete that work in the available time. CLI-based IaC provides precise, efficient control.

pigsty.yml describes the deployment state. Production, staging, test, and development environments use the same deployment logic. Their inventories define the differences.

Store this configuration in Git for version history and auditing. You can also store it in PostgreSQL CMDB tables as infrastructure data. Integrate either method with existing workflows.

IaC supports enterprise operators, individual developers, and smaller organizations. Default values avoid the need to understand every parameter. Start a single-node database with zero configuration. Add two IP addresses for an enterprise HA PostgreSQL cluster.


Declare Modules

This default example installs INFRA, NODE, ETCD, and PGSQL on 10.10.10.10.

# monitoring, alerting, DNS, NTP and other infrastructure cluster...
infra: { hosts: { 10.10.10.10: { infra_seq: 1 } } }

# minio cluster, s3 compatible object storage
minio: { hosts: { 10.10.10.10: { minio_seq: 1 } }, vars: { minio_cluster: minio } }

# etcd cluster, used as DCS for PostgreSQL high availability
etcd: { hosts: { 10.10.10.10: { etcd_seq: 1 } }, vars: { etcd_cluster: etcd } }

# PGSQL example cluster: pg-meta
pg-meta: { hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary } }, vars: { pg_cluster: pg-meta } }

Run these playbooks to install the modules:

./infra.yml -l 10.10.10.10  # Initialize infra module on node 10.10.10.10
./etcd.yml  -l 10.10.10.10  # Initialize etcd module on node 10.10.10.10
./minio.yml -l 10.10.10.10  # Initialize minio module on node 10.10.10.10
./pgsql.yml -l 10.10.10.10  # Initialize pgsql module on node 10.10.10.10

Declare Clusters

Install PGSQL on multiple nodes to declare a PostgreSQL cluster as one service unit:

For a three-node HA cluster using streaming replication, add this definition to all.children in pigsty.yml. The three nodes must already be managed by Pigsty.

pg-test:
  hosts:
    10.10.10.11: { pg_seq: 1, pg_role: primary }
    10.10.10.12: { pg_seq: 2, pg_role: replica }
    10.10.10.13: { pg_seq: 3, pg_role: offline }
  vars:  { pg_cluster: pg-test }

After defining the cluster, create it with playbooks:

bin/pgsql-add pg-test   # Create the pg-test cluster
pigsty-iac.jpg

Instance roles include primary, replica, offline, delayed, and synchronous standby. Cluster types include standby, Citus, Redis, MINIO (Silo), and Etcd.


Customize Cluster Content

Declare databases, users, services, and HBA rules inside each cluster. This example customizes the default pg-meta single-node cluster:

It declares six business databases and seven business users. It adds a standby service for reads without replication delay, additional pg_hba rules, a primary L2 VIP, and a custom backup strategy.

pg-meta:
  hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary , pg_offline_query: true } }
  vars:
    pg_cluster: pg-meta
    pg_databases:                       # define business databases on this cluster, array of database definition
      - name: meta                      # REQUIRED, `name` is the only mandatory field of a database definition
        baseline: cmdb.sql              # optional, database sql baseline path, (relative path among ansible search path, e.g files/)
        pgbouncer: true                 # optional, add this database to pgbouncer database list? true by default
        schemas: [pigsty]               # optional, additional schemas to be created, array of schema names
        extensions:                     # optional, additional extensions to be installed: array of `{name[,schema]}`
          - { name: postgis , schema: public }
          - { name: timescaledb }
        comment: pigsty meta database   # optional, comment string for this database
        owner: postgres                # optional, database owner, postgres by default
        template: template1            # optional, which template to use, template1 by default
        encoding: UTF8                 # optional, database encoding, UTF8 by default. (MUST same as template database)
        locale: C                      # optional, database locale, C by default.  (MUST same as template database)
        lc_collate: C                  # optional, database collate, C by default. (MUST same as template database)
        lc_ctype: C                    # optional, database ctype, C by default.   (MUST same as template database)
        tablespace: pg_default         # optional, default tablespace, 'pg_default' by default.
        allowconn: true                # optional, allow connection, true by default. false will disable connect at all
        revokeconn: false              # optional, revoke public connection privilege. false by default. (leave connect with grant option to owner)
        register_datasource: true      # optional, register this database to grafana datasources? true by default
        connlimit: -1                  # optional, database connection limit, default -1 disable limit
        pool_auth_user: dbuser_meta    # optional, all connection to this pgbouncer database will be authenticated by this user
        pool_mode: transaction         # optional, pgbouncer pool mode at database level, default transaction
        pool_size: 64                  # optional, pgbouncer pool size at database level, default 64
        pool_reserve: 32          # optional, pgbouncer pool size reserve at database level, default 32
        pool_size_min: 0               # optional, pgbouncer pool size min at database level, default 0
        pool_connlimit: 100          # optional, max database connections at database level, default 100
      - { name: grafana  ,owner: dbuser_grafana  ,revokeconn: true ,comment: grafana primary database }
      - { name: bytebase ,owner: dbuser_bytebase ,revokeconn: true ,comment: bytebase primary database }
      - { name: kong     ,owner: dbuser_kong     ,revokeconn: true ,comment: kong the api gateway database }
      - { name: gitea    ,owner: dbuser_gitea    ,revokeconn: true ,comment: gitea meta database }
      - { name: wiki     ,owner: dbuser_wiki     ,revokeconn: true ,comment: wiki meta database }
    pg_users:                           # define business users/roles on this cluster, array of user definition
      - name: dbuser_meta               # REQUIRED, `name` is the only mandatory field of a user definition
        password: DBUser.Meta           # optional, password, can be a scram-sha-256 hash string or plain text
        login: true                     # optional, can log in, true by default  (new biz ROLE should be false)
        superuser: false                # optional, is superuser? false by default
        createdb: false                 # optional, can create database? false by default
        createrole: false               # optional, can create role? false by default
        inherit: true                   # optional, can this role use inherited privileges? true by default
        replication: false              # optional, can this role do replication? false by default
        bypassrls: false                # optional, can this role bypass row level security? false by default
        pgbouncer: true                 # optional, add this user to pgbouncer user-list? false by default (production user should be true explicitly)
        connlimit: -1                   # optional, user connection limit, default -1 disable limit
        expire_in: 3650                 # optional, now + n days when this role is expired (OVERWRITE expire_at)
        expire_at: '2030-12-31'         # optional, YYYY-MM-DD 'timestamp' when this role is expired  (OVERWRITTEN by expire_in)
        comment: pigsty admin user      # optional, comment string for this user/role
        roles: [dbrole_admin]           # optional, belonged roles. default roles are: dbrole_{admin,readonly,readwrite,offline}
        parameters: {}                  # optional, role level parameters with `ALTER ROLE SET`
        pool_mode: transaction          # optional, pgbouncer pool mode at user level, transaction by default
        pool_connlimit: -1              # optional, max database connections at user level, default -1 disable limit
      - {name: dbuser_view     ,password: DBUser.Viewer   ,pgbouncer: true ,roles: [dbrole_readonly], comment: read-only viewer for meta database}
      - {name: dbuser_grafana  ,password: DBUser.Grafana  ,pgbouncer: true ,roles: [dbrole_admin]    ,comment: admin user for grafana database   }
      - {name: dbuser_bytebase ,password: DBUser.Bytebase ,pgbouncer: true ,roles: [dbrole_admin]    ,comment: admin user for bytebase database  }
      - {name: dbuser_kong     ,password: DBUser.Kong     ,pgbouncer: true ,roles: [dbrole_admin]    ,comment: admin user for kong api gateway   }
      - {name: dbuser_gitea    ,password: DBUser.Gitea    ,pgbouncer: true ,roles: [dbrole_admin]    ,comment: admin user for gitea service      }
      - {name: dbuser_wiki     ,password: DBUser.Wiki     ,pgbouncer: true ,roles: [dbrole_admin]    ,comment: admin user for wiki.js service    }
    pg_services:                        # extra services in addition to pg_default_services, array of service definition
      # standby service will route {ip|name}:5435 to sync replica's pgbouncer (5435->6432 standby)
      - name: standby                   # required, service name, the actual svc name will be prefixed with `pg_cluster`, e.g: pg-meta-standby
        port: 5435                      # required, service exposed port (work as kubernetes service node port mode)
        ip: "*"                         # optional, service bind ip address, `*` for all ip by default
        selector: "[]"                  # required, service member selector, use JMESPath to filter inventory
        dest: default                   # optional, destination port, default|postgres|pgbouncer|<port_number>, 'default' by default
        check: /sync                    # optional, health check url path, / by default
        backup: "[? pg_role == `primary`]"  # backup server selector
        maxconn: 3000                   # optional, max allowed front-end connection
        balance: roundrobin             # optional, haproxy load balance algorithm (roundrobin by default, other: leastconn)
        options: 'inter 3s fastinter 1s downinter 5s rise 3 fall 3 on-marked-down shutdown-sessions slowstart 30s maxconn 3000 maxqueue 128 weight 100'
    pg_hba_rules:
      - {user: dbuser_view , db: all ,addr: infra ,auth: pwd ,title: 'allow grafana dashboard access cmdb from infra nodes'}
    pg_vip_enabled: true
    pg_vip_address: 10.10.10.2/24
    pg_vip_interface: eth1
    pg_crontab:  # full backup daily at 1am (installed in the postgres user crontab)
      - '00 01 * * * /pg/bin/pg-backup full'

Declare Access Control

Declarative configuration also controls access permissions. This example customizes security for pg-meta:

The three-node crit.yml template prioritizes consistency and zero-loss failover. It enables L2 VIP and restricts database and pool listeners to loopback, internal, and VIP addresses. It enables TLS for the Patroni API and PgBouncer. HBA rules require SSL for database access. $libdir/passwordcheck in pg_libs enforces the password policy.

A separate pg-meta-delay cluster follows pg-meta with a one-hour delay for emergency recovery after data deletion.

pg-meta:      # 3 instance postgres cluster `pg-meta`
  hosts:
    10.10.10.10: { pg_seq: 1, pg_role: primary }
    10.10.10.11: { pg_seq: 2, pg_role: replica }
    10.10.10.12: { pg_seq: 3, pg_role: replica , pg_offline_query: true }
  vars:
    pg_cluster: pg-meta
    pg_conf: crit.yml
    pg_users:
      - { name: dbuser_meta , password: DBUser.Meta   , pgbouncer: true , roles: [ dbrole_admin ] , comment: pigsty admin user }
      - { name: dbuser_view , password: DBUser.Viewer , pgbouncer: true , roles: [ dbrole_readonly ] , comment: read-only viewer for meta database }
    pg_databases:
      - {name: meta ,baseline: cmdb.sql ,comment: pigsty meta database ,schemas: [pigsty] ,extensions: [{name: postgis, schema: public}, {name: timescaledb}]}
    pg_default_service_dest: postgres
    pg_services:
      - { name: standby ,src_ip: "*" ,port: 5435 , dest: default ,selector: "[]" , backup: "[? pg_role == `primary`]" }
    pg_vip_enabled: true
    pg_vip_address: 10.10.10.2/24
    pg_vip_interface: eth1
    pg_listen: '${ip},${vip},${lo}'
    patroni_ssl_enabled: true
    pgbouncer_sslmode: require
    pgbackrest_method: minio
    pg_libs: 'timescaledb, $libdir/passwordcheck, pg_stat_statements, auto_explain' # add passwordcheck extension to enforce strong password
    pg_default_roles:                 # default roles and users in postgres cluster
      - { name: dbrole_readonly  ,login: false ,comment: role for global read-only access     }
      - { name: dbrole_offline   ,login: false ,comment: role for restricted read-only access }
      - { name: dbrole_readwrite ,login: false ,roles: [dbrole_readonly]               ,comment: role for global read-write access }
      - { name: dbrole_admin     ,login: false ,roles: [pg_monitor, dbrole_readwrite]  ,comment: role for object creation }
      - { name: postgres     ,superuser: true  ,expire_in: 7300                        ,comment: system superuser }
      - { name: replicator ,replication: true  ,expire_in: 7300 ,roles: [pg_monitor, dbrole_readonly]   ,comment: system replicator }
      - { name: dbuser_dba   ,superuser: true  ,expire_in: 7300 ,roles: [dbrole_admin]  ,pgbouncer: true ,pool_mode: session, pool_connlimit: 16 , comment: pgsql admin user }
      - { name: dbuser_monitor ,roles: [pg_monitor] ,expire_in: 7300 ,pgbouncer: true ,parameters: {log_min_duration_statement: 1000 } ,pool_mode: session ,pool_connlimit: 8 ,comment: pgsql monitor user }
    pg_default_hba_rules:             # postgres host-based auth rules by default
      - {user: '${dbsu}'    ,db: all         ,addr: local     ,auth: ident ,title: 'dbsu access via local os user ident'  }
      - {user: '${dbsu}'    ,db: replication ,addr: local     ,auth: ident ,title: 'dbsu replication from local os ident' }
      - {user: '${repl}'    ,db: replication ,addr: localhost ,auth: ssl   ,title: 'replicator replication from localhost'}
      - {user: '${repl}'    ,db: replication ,addr: intra     ,auth: ssl   ,title: 'replicator replication from intranet' }
      - {user: '${repl}'    ,db: postgres    ,addr: intra     ,auth: ssl   ,title: 'replicator postgres db from intranet' }
      - {user: '${monitor}' ,db: all         ,addr: localhost ,auth: pwd   ,title: 'monitor from localhost with password' }
      - {user: '${monitor}' ,db: all         ,addr: infra     ,auth: ssl   ,title: 'monitor from infra host with password'}
      - {user: '${admin}'   ,db: all         ,addr: infra     ,auth: ssl   ,title: 'admin @ infra nodes with pwd & ssl'   }
      - {user: '${admin}'   ,db: all         ,addr: world     ,auth: cert  ,title: 'admin @ everywhere with ssl & cert'   }
      - {user: '+dbrole_readonly',db: all    ,addr: localhost ,auth: ssl   ,title: 'pgbouncer read/write via local socket'}
      - {user: '+dbrole_readonly',db: all    ,addr: intra     ,auth: ssl   ,title: 'read/write biz user via password'     }
      - {user: '+dbrole_offline' ,db: all    ,addr: intra     ,auth: ssl   ,title: 'allow etl offline tasks from intranet'}
    pgb_default_hba_rules:            # pgbouncer host-based authentication rules
      - {user: '${dbsu}'    ,db: pgbouncer   ,addr: local     ,auth: peer  ,title: 'dbsu local admin access with os ident'}
      - {user: 'all'        ,db: all         ,addr: localhost ,auth: pwd   ,title: 'allow all user local access with pwd' }
      - {user: '${monitor}' ,db: pgbouncer   ,addr: intra     ,auth: ssl   ,title: 'monitor access via intranet with pwd' }
      - {user: '${monitor}' ,db: all         ,addr: world     ,auth: deny  ,title: 'reject all other monitor access addr' }
      - {user: '${admin}'   ,db: all         ,addr: intra     ,auth: ssl   ,title: 'admin access via intranet with pwd'   }
      - {user: '${admin}'   ,db: all         ,addr: world     ,auth: deny  ,title: 'reject all other admin access addr'   }
      - {user: 'all'        ,db: all         ,addr: intra     ,auth: ssl   ,title: 'allow all user intra access with pwd' }

# OPTIONAL delayed cluster for pg-meta
pg-meta-delay:                    # delayed instance for pg-meta (1 hour ago)
  hosts: { 10.10.10.13: { pg_seq: 1, pg_role: primary, pg_upstream: 10.10.10.10, pg_delay: 1h } }
  vars: { pg_cluster: pg-meta-delay }

Citus Distributed Cluster

This configuration declares a four-node Citus cluster:

all:
  children:
    pg-citus0: # citus coordinator, pg_group = 0
      hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus0 , pg_group: 0 }
    pg-citus1: # citus data node 1
      hosts: { 10.10.10.11: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus1 , pg_group: 1 }
    pg-citus2: # citus data node 2
      hosts: { 10.10.10.12: { pg_seq: 1, pg_role: primary } }
      vars: { pg_cluster: pg-citus2 , pg_group: 2 }
    pg-citus3: # citus data node 3, with an extra replica
      hosts:
        10.10.10.13: { pg_seq: 1, pg_role: primary }
        10.10.10.14: { pg_seq: 2, pg_role: replica }
      vars: { pg_cluster: pg-citus3 , pg_group: 3 }
  vars:                               # global parameters for all citus clusters
    pg_mode: citus                    # pgsql cluster mode: citus
    pg_shard: pg-citus                # citus shard name: pg-citus
    patroni_citus_db: meta            # citus distributed database name
    pg_dbsu_password: DBUser.Postgres # all dbsu password access for citus cluster
    pg_users: [ { name: dbuser_meta ,password: DBUser.Meta ,pgbouncer: true ,roles: [ dbrole_admin ] } ]
    pg_databases: [ { name: meta ,extensions: [ { name: citus }, { name: postgis }, { name: timescaledb } ] } ]
    pg_hba_rules:
      - { user: 'all' ,db: all  ,addr: 127.0.0.1/32 ,auth: ssl ,title: 'all user ssl access from localhost' }
      - { user: 'all' ,db: all  ,addr: intra        ,auth: ssl ,title: 'all user ssl access from intranet'  }

Redis Clusters

These examples declare Redis primary-replica, sentinel, and Redis Cluster deployments:

redis-ms: # redis classic primary & replica
  hosts: { 10.10.10.10: { redis_node: 1 , redis_instances: { 6379: { }, 6380: { replica_of: '10.10.10.10 6379' } } } }
  vars: { redis_cluster: redis-ms ,redis_password: 'redis.ms' ,redis_max_memory: 64MB }

redis-meta: # redis sentinel x 3
  hosts: { 10.10.10.11: { redis_node: 1 , redis_instances: { 26379: { } ,26380: { } ,26381: { } } } }
  vars:
    redis_cluster: redis-meta
    redis_password: 'redis.meta'
    redis_mode: sentinel
    redis_max_memory: 16MB
    redis_sentinel_monitor: # primary list for redis sentinel, use cls as name, primary ip:port
      - { name: redis-ms, host: 10.10.10.10, port: 6379 ,password: redis.ms, quorum: 2 }

redis-test: # redis native cluster: 3m x 3s
  hosts:
    10.10.10.12: { redis_node: 1 ,redis_instances: { 6379: { } ,6380: { } ,6381: { } } }
    10.10.10.13: { redis_node: 2 ,redis_instances: { 6379: { } ,6380: { } ,6381: { } } }
  vars: { redis_cluster: redis-test ,redis_password: 'redis.test' ,redis_mode: cluster, redis_max_memory: 32MB }

ETCD Cluster

This configuration declares a three-node Etcd cluster:

etcd: # dcs service for postgres/patroni ha consensus
  hosts:  # 1 node for testing, 3 or 5 for production
    10.10.10.10: { etcd_seq: 1 }  # etcd_seq required
    10.10.10.11: { etcd_seq: 2 }  # assign from 1 ~ n
    10.10.10.12: { etcd_seq: 3 }  # three-member cluster keeps an odd voter count
  vars: # cluster level parameter override roles/etcd
    etcd_cluster: etcd  # mark etcd cluster name etcd
    etcd_safeguard: false # safeguard against purging
    etcd_clean: true # purge etcd during init process

MINIO (Silo) Cluster

This configuration declares a three-node Silo cluster. Inventory groups and parameters retain MINIO compatibility names:

minio:
  hosts:
    10.10.10.10: { minio_seq: 1 }
    10.10.10.11: { minio_seq: 2 }
    10.10.10.12: { minio_seq: 3 }
  vars:
    minio_cluster: minio
    minio_type: silo
    minio_data: '/data{1...2}'          # use two disks per node
    minio_node: '${minio_cluster}-${minio_seq}.pigsty' # node name pattern
    haproxy_services:
      - name: minio                     # [required] service name, must be unique
        port: 9002                      # [required] service port, must be unique
        options:
          - option httpchk
          - option http-keep-alive
          - http-check send meth OPTIONS uri /minio/health/live
          - http-check expect status 200
        servers:
          - { name: minio-1 ,ip: 10.10.10.10 , port: 9000 , options: 'check-ssl ca-file /etc/pki/ca.crt check port 9000' }
          - { name: minio-2 ,ip: 10.10.10.11 , port: 9000 , options: 'check-ssl ca-file /etc/pki/ca.crt check port 9000' }
          - { name: minio-3 ,ip: 10.10.10.12 , port: 9000 , options: 'check-ssl ca-file /etc/pki/ca.crt check port 9000' }

3.1 - Inventory

Describe your infrastructure and clusters using declarative configuration files

Each Pigsty deployment has an inventory that describes the infrastructure and database clusters.


Configuration File

By default, Pigsty uses Ansible YAML configuration format. The inventory is a single file, pigsty.yml.

~/pigsty
  ^---- pigsty.yml   # <---- Default configuration file

Edit this file to customize the deployment. Alternatively, use the configure wizard to generate it automatically.


Configuration Structure

The inventory uses Ansible YAML configuration format. It contains global parameters in all.vars and groups in all.children.

Define clusters in all.children. Define infrastructure settings in all.vars, as this example shows:

all:                  # Top-level object: all
  vars: {...}         # Global parameters
  children:           # Group definitions
    infra:            # Group definition: 'infra'
      hosts: {...}        # Group members: 'infra'
      vars:  {...}        # Group parameters: 'infra'
    etcd:    {...}    # Group definition: 'etcd'
    pg-meta: {...}    # Group definition: 'pg-meta'
    pg-test: {...}    # Group definition: 'pg-test'
    redis-test: {...} # Group definition: 'redis-test'
    # ...

Cluster Definition

Each Ansible group can represent a cluster. Examples include node, PostgreSQL, Redis, Etcd, and Silo clusters.

A cluster definition contains members (hosts) and parameters (vars). Define members in <cls>.hosts. Set configuration parameters in <cls>.vars. This example defines a three-node HA PostgreSQL cluster:

all:
  children:    # Ansible group list
    pg-test:   # Ansible group name
      hosts:   # Ansible group instances (cluster members)
        10.10.10.11: { pg_seq: 1, pg_role: primary } # Host 1
        10.10.10.12: { pg_seq: 2, pg_role: replica } # Host 2
        10.10.10.13: { pg_seq: 3, pg_role: offline } # Host 3
      vars:    # Ansible group variables (cluster parameters)
        pg_cluster: pg-test

Cluster vars override global parameters. Instance vars override both cluster and global parameters.


Splitting Configuration

For a large deployment, you can split the inventory into multiple files. This can also help organize and maintain configuration.

inventory/
├── hosts.yml              # Host and cluster definitions
├── group_vars/
│   ├── all.yml            # Global default variables (corresponds to all.vars)
│   ├── infra.yml          # infra group variables
│   ├── etcd.yml           # etcd group variables
│   └── pg-meta.yml        # pg-meta cluster variables
└── host_vars/
    ├── 10.10.10.10.yml    # Specific host variables
    └── 10.10.10.11.yml

Put cluster members in hosts.yml. Put cluster configuration parameters in the corresponding files under group_vars.


Switching Configuration

When you run a playbook, use -i to temporarily select a different inventory.

./pgsql.yml -i another_config.yml
./infra.yml -i nginx_config.yml

Ansible also accepts local yaml|ini configuration files, a CMDB, or other dynamic configuration scripts.

The ansible.cfg file in the Pigsty home directory sets the default inventory to pigsty.yml in that directory. You can change this setting.

[defaults]
inventory = pigsty.yml

You can store the inventory in a CMDB metabase to integrate it with existing systems.

3.2 - Configure

Use the configure script to automatically generate recommended configuration files based on your environment.

The configure configuration wizard generates pigsty.yml for your current environment.

This script is optional. If you know how to configure Pigsty, edit pigsty.yml directly and skip the wizard.


Quick Start

Enter the Pigsty source directory and run ./configure to start the wizard. Without arguments, it uses the meta single-node template:

cd ~/pigsty
./configure          # Interactive configuration wizard, auto-detect environment and generate config

The command starts with the selected template. It detects the current node IP address and region, then generates pigsty.yml for that environment.

demo/configure.cast — /demo/configure.cast

Features

The configure script makes these adjustments from your environment and input. By default, it generates pigsty.yml in the Pigsty directory.

  • Detect the current node IP address. If the node has multiple addresses, ask you to select a primary IP address as its identity.
  • Replace 10.10.10.10 in the template with this address. Set admin_ip to the same value.
  • Detect the region. Set region to default for global software repositories or china for Chinese mirrors.
  • For instances with fewer than four vCPUs, use the tiny template for node_tune and pg_conf. This reduces resource use.
  • With -v, change pg_version and pg18-* package-group aliases to the specified major version. Exclude the fixed-kernel templates mssql, polar, and pg19.
  • With -g, replace recognized default passwords with strong random passwords. This is strongly recommended. Review other values against the Default Credentials Checklist.
  • For PG major versions ≥ 17, prefer the built-in C.UTF-8 locale, or the OS-supported C.UTF-8.
  • Check that the core deployment dependency ansible is available.
  • Check that the deployment target accepts SSH connections and can run commands with sudo. Use -s to skip these checks.

Usage Examples

# Basic usage
./configure                       # Interactive configuration wizard
./configure -i 10.10.10.10        # Specify primary IP address

# Specify configuration template
./configure -c meta               # Use default single-node template (default)
./configure -c rich               # Use feature-rich single-node template
./configure -c slim               # Use minimal template (PGSQL + ETCD only)
./configure -c ha/full            # Use 4-node HA sandbox template
./configure -c ha/trio            # Use 3-node HA template
./configure -c supabase           # Use Supabase self-hosted template
./configure -c app/immich         # Use Immich photo-management template

# Specify PostgreSQL version
./configure -v 18                 # Use PostgreSQL 18
./configure -v 16                 # Use PostgreSQL 16
./configure -c rich -v 15         # rich template + PG 15
./configure -c pg19               # Use the dedicated PostgreSQL 19 Beta template

# Region and proxy
./configure -r china              # Use Chinese mirrors
./configure -r europe             # Use European mirrors
./configure -x                    # Import current proxy environment variables

# Skip and automation
./configure -s                    # Skip IP detection, keep placeholder
./configure -n -i 10.10.10.10     # Non-interactive mode with specified IP
./configure -c ha/full -s         # 4-node template, skip IP replacement

# Security enhancement
./configure -g                    # Generate random passwords
./configure -c meta -g -i 10.10.10.10  # Complete production configuration

# Specify output and SSH port
./configure -o prod.yml           # Output to prod.yml
./configure -p 2222               # Use SSH port 2222

Command Arguments

./configure
    [-c|--conf <template>]      # Configuration template name (meta|rich|slim|ha/full|...)
    [-i|--ip <ipaddr>]          # Specify primary IP address
    [-v|--version <pgver>]      # PostgreSQL major version (14|15|16|17|18|19)
    [-r|--region <region>]      # Upstream software repo region (default|china|europe)
    [-o|--output <file>]        # Output configuration file path (default: pigsty.yml)
    [-s|--skip]                 # Skip IP address detection and replacement
    [-x|--proxy]                # Import proxy settings from environment variables
    [-n|--non-interactive]      # Non-interactive mode (don't ask any questions)
    [-p|--port <port>]          # Specify SSH port
    [-g|--generate]             # Generate random passwords
    [-h|--help]                 # Display help information

Argument Details

ArgumentDescription
-c, --confGenerate config from conf/<template>.yml, supports subdirectories like ha/full
-i, --ipReplace placeholder 10.10.10.10 in config template with specified IP
-v, --versionSpecify PostgreSQL major version (14-19); PG19 is Beta, so prefer the dedicated pg19 template
-r, --regionSet software repo mirror region: default, china (Chinese mirrors), europe (European)
-o, --outputOutput path, default pigsty.yml; relative paths use Pigsty home, absolute paths are used as given
-s, --skipSkip IP probing, target SSH/Sudo checks, and effective IP replacement; keep 10.10.10.10
-x, --proxyWrite current environment proxy variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY) to config
-n, --non-interactiveNon-interactive mode; a single/demo IP is auto-selected, while ambiguous multi-IP hosts require -i
-p, --portSSH port used by readiness checks only; it does not write ansible_port into the generated config
-g, --generateGenerate random values for passwords in config file, improving security (strongly recommended)

Execution Flow

The configure script detects and configures settings in this order:

┌─────────────────────────────────────────────────────────────┐
│                  configure Execution Flow                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. check_region          Detect network region (GFW check) │
│         ↓                                                   │
│  2. check_version         Validate PostgreSQL version       │
│         ↓                                                   │
│  3. check_kernel          Detect OS kernel (Linux/Darwin)   │
│         ↓                                                   │
│  4. check_machine         Detect CPU arch (x86_64/aarch64)  │
│         ↓                                                   │
│  5. check_package_manager Detect package manager (dnf/yum/apt) │
│         ↓                                                   │
│  6. check_vendor_version  Detect OS distro and version      │
│         ↓                                                   │
│  7. check_sudo            Detect passwordless sudo          │
│         ↓                                                   │
│  8. check_ssh             Detect passwordless SSH to self   │
│         ↓                                                   │
│  9. check_proxy_env       Handle proxy environment vars     │
│         ↓                                                   │
│ 10. check_ipaddr          Detect/input primary IP address   │
│         ↓                                                   │
│ 11. check_admin           Validate admin SSH + Sudo access  │
│         ↓                                                   │
│ 12. check_conf            Select configuration template     │
│         ↓                                                   │
│ 13. check_config          Generate configuration file       │
│         ↓                                                   │
│ 14. check_utils           Check if Ansible etc. installed   │
│         ↓                                                   │
│     ✓ Configuration complete, output pigsty.yml             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Automatic Behaviors

Region Detection

The script tests network access to detect mainland China behind the GFW:

# The actual probe uses HTTPS with a two-second total timeout
curl -I -s --max-time 2 https://www.google.com
  • If Google is reachable, use region: default repositories.
  • If Google is unreachable but https://pigsty.cc is reachable, set region: china.
  • If neither endpoint is reachable, use region: default and warn that internet access is unavailable.
  • To set the region manually, use -r.

IP Address Handling

The script selects the primary IP address in this order:

  1. Command line argument: Use the address specified with -i.
  2. Single IP detection: If the node has only one IP address, select it automatically.
  3. Demo IP detection: If 10.10.10.10 exists, select it automatically for the sandbox environment.
  4. Interactive input: If multiple addresses remain, ask you to select or enter one.
[WARN] Multiple IP address candidates found:
    (1) 192.168.1.100   inet 192.168.1.100/24 scope global eth0
    (2) 10.10.10.10     inet 10.10.10.10/24 scope global eth1
[ IN ] INPUT primary_ip address (of current meta node, e.g 10.10.10.10):
=> 10.10.10.10

Low-End Hardware Optimization

If the script detects fewer than four CPU cores (1–3), it changes these settings:

[WARN] replace oltp template with tiny due to cpu < 4

These settings support operation on virtual machines with limited resources.

Locale Settings

The script enables C.UTF-8 as the default locale if either condition applies:

  • PostgreSQL version is ≥ 17, with built-in Locale Provider support.
  • The operating system supports the C.UTF-8 or C.utf8 locale.
pg_locale: C.UTF-8
pg_lc_collate: C.UTF-8
pg_lc_ctype: C.UTF-8

China Region Special Handling

For region china, the script automatically enables:

  • docker_registry_mirrors for faster Docker downloads through mirrors.
  • PIP_MIRROR_URL for faster Python downloads through mirrors.

Password Generation

With -g, the script generates a random 24-character string for each of these passwords:

Password ParameterDescription
grafana_admin_passwordGrafana admin password
pg_admin_passwordPostgreSQL admin password
pg_monitor_passwordPostgreSQL monitor user password
pg_replication_passwordPostgreSQL replication user password
patroni_passwordPatroni API password
haproxy_admin_passwordHAProxy admin password
minio_secret_keySilo Root Secret
etcd_root_passwordETCD Root password

It also replaces the following placeholder passwords:

  • DBUser.Meta → random password
  • DBUser.Viewer → random password
  • S3User.Backup → random password
  • S3User.Meta → random password
  • S3User.Data → random password
  • DBUser.Supa → random password
  • Vibe.Coding → random password
$ ./configure -g
[INFO] generating random passwords...
    grafana_admin_password   : xK9mL2nP4qR7sT1vW3yZ5bD8
    pg_admin_password        : aB3cD5eF7gH9iJ1kL2mN4oP6
    ...
[INFO] random passwords generated, check and save them

Configuration Templates

The script reads templates from conf/. Set -c to a relative path without the .yml suffix, such as ha/full or app/immich.

Core Templates

TemplateDescription
metaDefault template: Single-node installation with INFRA + NODE + ETCD + PGSQL
richFeature-rich version: Includes almost all extensions, Silo, local repo
slimMinimal version: PostgreSQL + ETCD only, no monitoring infrastructure
fatComplete version: rich base with more extensions installed
pgsqlPure PostgreSQL template
pg19Single-node PostgreSQL 19 Beta evaluation template
infraPure infrastructure template

HA Templates (ha/)

TemplateDescription
ha/dual2-node HA cluster
ha/trio3-node HA cluster
ha/full4-node complete sandbox environment
ha/safeSecurity-hardened HA configuration
ha/octoCompact 8-node HA simulation
ha/simu20-node production simulation environment
ha/citus13-node Citus distributed cluster

Application Templates

TemplateDescription
supabaseSupabase self-hosted configuration
app/difyDify AI platform configuration
app/odooOdoo ERP configuration
app/electricElectric sync engine configuration
app/insforgeInsforge backend platform configuration
app/hindsightHindsight application configuration
app/teableTeable table database configuration
app/mattermostMattermost collaboration platform configuration
app/maybeMaybe finance application configuration
app/registryDocker Registry configuration
app/immichImmich photo and video management
app/jumpserverJumpServer bastion host

Special Kernel Templates

TemplateDescription
ivoryIvorySQL: Oracle-compatible PostgreSQL
mssqlBabelfish: SQL Server-compatible PostgreSQL
polarPolarDB: Alibaba Cloud open-source distributed PostgreSQL
ha/citusCitus: Distributed PostgreSQL HA cluster
mysqlOpenHalo: MySQL protocol-compatible PostgreSQL
pgtdePercona PostgreSQL Server: transparent encryption
orioleOrioleDB: Next-generation storage engine
agensAgensGraph: graph database kernel
pgedgepgEdge: distributed PostgreSQL kernel
mongoMongoDB-compatible stack template

Demo and Build Templates

TemplateDescription
vibeVibe Coding development environment
dockerRun Pigsty inside a Docker container
demo/bareMinimal readable single-node example
demo/elFull parameter example for EL distributions
demo/debianFull parameter example for Debian/Ubuntu
demo/demoMulti-module demo environment
demo/kernelTen-node database-kernel matrix
demo/redisRedis replica, Sentinel, and native Cluster demo
demo/minioMulti-node, multi-drive Silo demo (source default)
demo/kafkaKafka KRaft development and secure-cluster demo
demo/mysqlNative MySQL 8.4 pilot demo
demo/remoteRemote PostgreSQL/RDS monitoring example
demo/saasLegacy single-node SaaS component bundle
demo/woolSmall cloud-instance example for China
build/ossCross-distribution open-source package build env
build/devThree-node development and build environment

Output Example

$ ./configure
configure pigsty v4.5.0 begin
[ OK ] region = china
[ OK ] kernel  = Linux
[ OK ] machine = x86_64
[ OK ] package = rpm,dnf
[ OK ] vendor  = rocky (Rocky Linux)
[ OK ] version = 9 (9.5)
[ OK ] sudo = vagrant ok
[ OK ] ssh = [email protected] ok
[WARN] Multiple IP address candidates found:
    (1) 192.168.121.193	    inet 192.168.121.193/24 brd 192.168.121.255 scope global dynamic noprefixroute eth0
    (2) 10.10.10.10	    inet 10.10.10.10/24 brd 10.10.10.255 scope global noprefixroute eth1
[ OK ] primary_ip = 10.10.10.10 (from demo)
[ OK ] admin = [email protected] ok
[ OK ] mode = meta (el9)
[ OK ] locale  = C.UTF-8
[ OK ] ansible = ready
[ OK ] pigsty configured
[WARN] don't forget to check it and change passwords!
proceed with ./deploy.yml

Environment Variables

The script supports the following environment variables:

Environment VariableDescriptionDefault
PIGSTY_HOMEPigsty installation directory~/pigsty
METADB_URLMetabase connection URLservice=meta
HTTP_PROXYHTTP proxy-
HTTPS_PROXYHTTPS proxy-
ALL_PROXYUniversal proxy-
NO_PROXYProxy whitelistBuilt-in default

Notes

  1. Passwordless access: Before you run configure, the current user must have passwordless sudo and passwordless SSH access to localhost. The bootstrap script can configure both.

  2. IP address selection: Select an internal IP as the primary address. Do not select a public IP or 127.0.0.1.

  3. Password security: In production, always change default passwords. Use -g to randomize recognized credentials. Review the Default Credentials Checklist for remaining values.

  4. Configuration review: After the script completes, review the generated pigsty.yml. Check that it meets your requirements.

  5. Multiple executions: You can run configure again to regenerate configuration. Each run overwrites the existing pigsty.yml.

  6. macOS limitations: On macOS, the script skips some Linux checks and uses placeholder IP 10.10.10.10. macOS can serve only as an administration node.


FAQ

How to use a custom configuration template?

Place your configuration file in the conf/ directory, then specify it with the -c argument:

cp my-config.yml ~/pigsty/conf/myconf.yml
./configure -c myconf

How to generate different configurations for multiple clusters?

Use the -o argument to specify different output files:

./configure -c ha/full -o cluster-a.yml
./configure -c ha/trio -o cluster-b.yml

Then specify the configuration file when running playbooks:

./deploy.yml -i cluster-a.yml

How to handle multiple IPs in non-interactive mode?

You must explicitly specify the IP address using the -i argument:

./configure -n -i 10.10.10.10

How to keep the placeholder IP in the template?

Use the -s argument to skip IP replacement:

./configure -c ha/full -s   # Keep 10.10.10.10 placeholder

  • Inventory: Understand the Ansible inventory structure
  • Parameters: Understand Pigsty parameter hierarchy and priority
  • Templates: View all available configuration templates
  • Installation: Understand the complete installation process
  • Metabase: Use PostgreSQL as a dynamic configuration source

3.3 - Parameters

Fine-tune Pigsty customization using configuration parameters

Use inventory parameters to customize infrastructure settings and database configuration.


Parameter List

The current source and reference pages list 373 public parameters across 10 official modules. See Reference - Parameter List for the complete list. The native MySQL 8.4 pilot module has 13 additional public parameters. They are listed separately and excluded from this total.

ModuleGroupsParamsDescription
PGSQL9124PostgreSQL high-availability cluster configuration
INFRA1073Software repositories and Victoria observability infrastructure
NODE1173Node initialization, system tuning, and operations baseline
ETCD213ETCD cluster and removal protection parameters
MINIO222Silo deployment, observability, and removal parameters
REDIS222Redis/Valkey deployment and removal parameters
DOCKER18Docker engine parameters
JUICE12JuiceFS instance and cache parameters
VIBE118Code/Jupyter/Node.js/Claude/Codex configuration
KAFKA218Kafka deployment and removal-protection parameters

Parameter Form

Parameters are key-value pairs that describe entities. A key is a string. A value can be a boolean, string, number, array, or object.

all:                            # <------- Top-level object: all
  vars:
    admin_ip: 10.10.10.10       # <------- Global configuration parameter
  children:
    pg-meta:                    # <------- pg-meta group
      vars:
        pg_cluster: pg-meta     # <------- Cluster-level parameter
      hosts:
        10.10.10.10:            # <------- Host node IP
          pg_seq: 1
          pg_role: primary      # <------- Instance-level parameter

Parameter Priority

Set parameters at these levels, listed from highest to lowest priority:

LevelLocationDescriptionPriority
CLI-e command line argumentPassed via command lineHighest (5)
Host/Instance<group>.hosts.<host>Parameters specific to a single hostHigher (4)
Group/Cluster<group>.varsParameters shared by hosts in group/clusterMedium (3)
Globalall.varsParameters shared by all hostsLower (2)
Default<roles>/default/main.ymlRole implementation defaultsLowest (1)

These examples show parameter priority:

  • Set -e grafana_clean=true on the playbook command line to erase Grafana data.
  • Set host variable pg_role to override the PostgreSQL instance role.
  • Set group variable pg_cluster to override the PostgreSQL cluster name.
  • Set global variable node_ntp_servers to specify NTP servers.
  • If you do not set pg_version, Pigsty uses the pgsql role default, 18.

Except for identity parameters, all parameters have defaults. You do not need to set them explicitly.


Identity Parameters

Identity parameters identify entities. They have no defaults and must be set explicitly, with the exception below.

ModuleIdentity Parameters
PGSQLpg_cluster, pg_seq, pg_role, …
NODEnodename, node_cluster
ETCDetcd_cluster, etcd_seq
MINIOminio_cluster, minio_seq
REDISredis_cluster, redis_node, redis_instances
INFRAinfra_seq

etcd_cluster is an exception. It defaults to etcd. Set minio_cluster explicitly in each object storage cluster’s variables. It no longer has a default. Do not set it in all.vars: this marks every host as a MINIO module member.

3.4 - Conf Templates

Use pre-made configuration templates to quickly generate configuration files adapted to your environment

The inventory defines the deployment through declarative configuration. Customize its pigsty.yml file for your requirements.

New users can start with ready-to-use templates for common deployment scenarios.

Each template is a predefined pigsty.yml file with defaults for a specific scenario.

Select a template, then change it to meet your requirements.


Using Templates

The optional configure wizard generates an inventory from your environment and input, with suitable defaults.

Use ./configure -c <conf> to select a template. <conf> is relative to conf; the .yml suffix is optional.

./configure                     # Default to meta.yml configuration template
./configure -c meta             # Explicitly specify meta.yml single-node template
./configure -c rich             # Use feature-rich template with all extensions and Silo
./configure -c slim             # Use minimal single-node template

# Use different database kernels
./configure -c pgsql            # Native PostgreSQL kernel, basic features (14~18)
./configure -c pg19             # PostgreSQL 19 Beta trial template
./configure -c mssql            # Babelfish kernel, SQL Server protocol compatible (17/18)
./configure -c polar            # PolarDB PG kernel, Aurora/RAC style (17)
./configure -c ivory            # IvorySQL kernel, Oracle syntax compatible (18)
./configure -c mysql            # OpenHalo kernel, MySQL compatible (14)
./configure -c pgtde            # Percona PostgreSQL Server transparent encryption (18)
./configure -c oriole           # OrioleDB kernel, OLTP enhanced (16~18)
./configure -c agens            # AgensGraph graph database kernel (17)
./configure -c pgedge           # pgEdge distributed database kernel (15~18, default 18)
./configure -c ha/citus         # Citus distributed HA PostgreSQL (14~18)
./configure -c supabase         # Supabase self-hosted configuration (15~18)

# Use multi-node HA templates
./configure -c ha/dual          # Use 2-node HA template
./configure -c ha/trio          # Use 3-node HA template
./configure -c ha/full          # Use 4-node HA template

If no template is specified, Pigsty defaults to the meta.yml single-node configuration template.


Template List

Main Templates

Use these templates to install Pigsty on one server:

TemplateDescription
meta.ymlDefault template, single-node PostgreSQL online installation
rich.ymlFeature-rich template with local repo, Silo, and more examples
slim.ymlMinimal template, PostgreSQL only without monitoring and infrastructure

Database Kernel Templates

Templates for various database management systems and kernels:

TemplateDescription
pgsql.ymlNative PostgreSQL kernel, basic features (14~18)
pg19.ymlPostgreSQL 19 Beta trial template
mssql.ymlBabelfish kernel, SQL Server protocol compatible (17/18)
polar.ymlPolarDB PG kernel, Aurora/RAC style (17)
ivory.ymlIvorySQL kernel, Oracle syntax compatible (18)
mysql.ymlOpenHalo kernel, MySQL compatible (14)
pgtde.ymlPercona PostgreSQL Server transparent encryption (18)
oriole.ymlOrioleDB kernel, OLTP enhanced (16~18)
agens.ymlAgensGraph graph database kernel (17)
pgedge.ymlpgEdge distributed database kernel (15~18, default 18)
supabase.ymlSupabase self-hosted configuration (15~18)

You can add more nodes later or use HA templates to plan your cluster from the start.


HA Templates

Use these templates for a deployment with multiple nodes and high availability:

TemplateDescription
dual.yml2-node semi-HA deployment
trio.yml3-node standard HA deployment
full.yml4-node standard deployment
safe.yml4-node security-enhanced deployment with delayed replica
octo.ymlCompact 8-node HA simulation
simu.yml20-node production environment simulation
ha/citus.ymlCitus distributed HA PostgreSQL (14~18)

Application Templates

Use these templates to run applications with Docker:

TemplateDescription
supabase.ymlStart single-node Supabase
odoo.ymlStart Odoo ERP system
dify.ymlStart Dify AI workflow system
electric.ymlStart Electric sync engine
insforge.ymlStart Insforge backend platform
hindsight.ymlStart Hindsight application
mattermost.ymlStart Mattermost collaboration platform
teable.ymlStart Teable spreadsheet database
maybe.ymlStart Maybe finance app
registry.ymlStart Docker Registry

Demo Templates

Pigsty also provides these demonstration templates:

TemplateDescription
el.ymlFull-parameter config file for EL 8/9 systems
debian.ymlFull-parameter config file for Debian/Ubuntu systems
remote.ymlExample config for monitoring remote PostgreSQL clusters or RDS
redis.ymlRedis cluster example configuration
minio.yml4-node multi-drive Silo cluster example (source default)
kafka.ymlKafka dynamic KRaft example with a single-node dev cluster and a three-node secure cluster
mysql.ymlNative MySQL 8.4 single-node/three-node pilot example; distinct from OpenHalo conf/mysql.yml
demo.ymlConfiguration file for Pigsty public demo site
fat.ymlSingle-node config with local repo and full feature set
infra.ymlDeploy only the infrastructure modules
vibe.ymlVibe Coding / AI application development template
mongo.ymlFerretDB / MongoDB-compatible example
docker.ymlDocker application host template

Build Templates

Use these templates for development and testing:

TemplateDescription
build/oss.ymlOpen source build config for EL 9/10, Debian 12/13, Ubuntu 22.04/24.04/26.04
build/dev.ymlDevelopment and testing build config

3.5 - Use CMDB as Config Inventory

Use PostgreSQL as a CMDB metabase to store Ansible inventory.

Use a PostgreSQL metabase as a dynamic configuration source instead of static YAML files.


Overview

A CMDB (Configuration Management Database) stores configuration in database tables.

By default, Pigsty uses static pigsty.yml as the Ansible inventory.

A static file is simple. Larger deployments can need finer control, concurrent editing, and external integration beyond one file’s practical limits.

FeatureStatic YAML FileCMDB Metabase
QueryingManual search/grepSQL queries with any conditions, aggregation analysis
VersioningDepends on Git or manual backupDatabase transactions, audit logs, time-travel snapshots
Access ControlFile system permissions, coarse-grainedPostgreSQL fine-grained access control
Concurrent EditingRequires file locking or merge conflictsDatabase transactions naturally support concurrency
External IntegrationRequires YAML parsingStandard SQL interface, easy integration with any language
ScalabilityDifficult to maintain when file becomes too largeScales to physical limits
Dynamic GenerationStatic file, changes require manual applicationImmediate effect, real-time configuration changes

The sample pg-meta.meta database baseline defines the CMDB schema.


How It Works

CMDB replaces the static inventory with an executable script that returns JSON, as Ansible supports. Enabling CMDB creates inventory.sh:

#!/bin/bash
psql ${METADB_URL} -AXtwc 'SELECT text FROM pigsty.inventory;'

Whenever Ansible reads the inventory, this script queries pigsty.inventory in PostgreSQL and returns JSON.

The architecture is:

flowchart LR
    conf["bin/inventory_conf"]
    tocmdb["bin/inventory_cmdb"]
    load["bin/inventory_load"]
    ansible["🚀 Ansible"]

    subgraph static["📄 Static Config Mode"]
        yml[("pigsty.yml")]
    end

    subgraph dynamic["🗄️ CMDB Dynamic Mode"]
        sh["inventory.sh"]
        cmdb[("PostgreSQL CMDB")]
    end

    conf -->|"switch"| yml
    yml -->|"load config"| load
    load -->|"write"| cmdb
    tocmdb -->|"switch"| sh
    sh --> cmdb

    yml --> ansible
    cmdb --> ansible

Data Model

The CMDB database schema is defined in files/cmdb.sql, with all objects in the pigsty schema.

Core Tables

TableDescriptionPrimary Key
pigsty.groupCluster/group definitions, corresponds to Ansible groupscls
pigsty.hostHost definitions, belongs to a group(cls, ip)
pigsty.global_varGlobal variables, corresponds to all.varskey
pigsty.group_varGroup variables, corresponds to all.children.<cls>.vars(cls, key)
pigsty.host_varHost variables, host-level variables(cls, ip, key)
pigsty.default_varDefault variable definitions, stores parameter metadatakey
pigsty.jobJob records table, records executed tasksid

Table Structure Details

Cluster Table pigsty.group

CREATE TABLE pigsty.group (
    cls     TEXT PRIMARY KEY,        -- Cluster name, primary key
    ctime   TIMESTAMPTZ DEFAULT now(), -- Creation time
    mtime   TIMESTAMPTZ DEFAULT now()  -- Modification time
);

Host Table pigsty.host

CREATE TABLE pigsty.host (
    cls    TEXT NOT NULL REFERENCES pigsty.group(cls),  -- Parent cluster
    ip     INET NOT NULL,                               -- Host IP address
    ctime  TIMESTAMPTZ DEFAULT now(),
    mtime  TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (cls, ip)
);

Global Variables Table pigsty.global_var

CREATE TABLE pigsty.global_var (
    key   TEXT PRIMARY KEY,           -- Variable name
    value JSONB NULL,                 -- Variable value (JSON format)
    mtime TIMESTAMPTZ DEFAULT now()   -- Modification time
);

Group Variables Table pigsty.group_var

CREATE TABLE pigsty.group_var (
    cls   TEXT NOT NULL REFERENCES pigsty.group(cls),
    key   TEXT NOT NULL,
    value JSONB NULL,
    mtime TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (cls, key)
);

Host Variables Table pigsty.host_var

CREATE TABLE pigsty.host_var (
    cls   TEXT NOT NULL,
    ip    INET NOT NULL,
    key   TEXT NOT NULL,
    value JSONB NULL,
    mtime TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (cls, ip, key),
    FOREIGN KEY (cls, ip) REFERENCES pigsty.host(cls, ip)
);

Core Views

These views expose configuration data:

ViewDescription
pigsty.inventoryCore view: Generates Ansible dynamic inventory JSON
pigsty.raw_configRaw configuration in JSON format
pigsty.global_configGlobal config view, merges defaults and global vars
pigsty.group_configGroup config view, includes host list and group vars
pigsty.host_configHost config view, merges group and host-level vars
pigsty.pg_clusterPostgreSQL cluster view
pigsty.pg_instancePostgreSQL instance view
pigsty.pg_databasePostgreSQL database definition view
pigsty.pg_usersPostgreSQL user definition view
pigsty.pg_servicePostgreSQL service definition view
pigsty.pg_hbaPostgreSQL HBA rules view
pigsty.pg_remoteRemote PostgreSQL instance view

pigsty.inventory converts database configuration to Ansible inventory JSON:

SELECT text FROM pigsty.inventory;

Utility Scripts

Use these three scripts to manage the CMDB:

ScriptFunction
bin/inventory_loadLoad YAML configuration file into PostgreSQL database
bin/inventory_cmdbSwitch configuration source to CMDB (dynamic inventory script)
bin/inventory_confSwitch configuration source to static config file pigsty.yml

inventory_load

Parse a YAML configuration file and import it into the CMDB:

bin/inventory_load                     # Load default pigsty.yml to default CMDB
bin/inventory_load -p /path/to/conf.yml  # Specify configuration file path
bin/inventory_load -d "postgres://..."   # Specify database connection URL
bin/inventory_load -n myconfig           # Specify configuration name

The script performs these operations:

  1. Clears existing data in the pigsty schema
  2. Parses the YAML configuration file
  3. Writes global variables to the global_var table
  4. Writes cluster definitions to the group table
  5. Writes cluster variables to the group_var table
  6. Writes host definitions to the host table
  7. Writes host variables to the host_var table

Environment Variables

  • PIGSTY_HOME: Pigsty installation directory, defaults to ~/pigsty
  • METADB_URL: Database connection URL, defaults to service=meta

inventory_cmdb

Switch Ansible to use CMDB as the configuration source:

bin/inventory_cmdb

The script performs these operations:

  1. Creates dynamic inventory script ${PIGSTY_HOME}/inventory.sh
  2. Modifies ansible.cfg to set inventory to inventory.sh

The generated inventory.sh contains:

#!/bin/bash
psql ${METADB_URL} -AXtwc 'SELECT text FROM pigsty.inventory;'

inventory_conf

Switch back to a static YAML configuration file:

bin/inventory_conf

The script modifies ansible.cfg to set inventory back to pigsty.yml.


Usage Workflow

First-time CMDB Setup

  1. Initialize the CMDB schema. Pigsty installation usually does this automatically:
psql -f ~/pigsty/files/cmdb.sql
  1. Load configuration into the database:
bin/inventory_load
  1. Switch to CMDB mode:
bin/inventory_cmdb
  1. Verify configuration:
ansible all --list-hosts          # List all hosts
ansible-inventory --list          # View complete inventory

Query Configuration

After you enable CMDB, use SQL to query the configuration:

-- View all clusters
SELECT cls FROM pigsty.group;

-- View all hosts in a cluster
SELECT ip FROM pigsty.host WHERE cls = 'pg-meta';

-- View global variables
SELECT key, value FROM pigsty.global_var;

-- View cluster variables
SELECT key, value FROM pigsty.group_var WHERE cls = 'pg-meta';

-- View all PostgreSQL clusters
SELECT cls, name, pg_databases, pg_users FROM pigsty.pg_cluster;

-- View all PostgreSQL instances
SELECT cls, ins, ip, seq, role FROM pigsty.pg_instance;

-- View all database definitions
SELECT cls, datname, owner, encoding FROM pigsty.pg_database;

-- View all user definitions
SELECT cls, name, login, superuser FROM pigsty.pg_users;

Modify Configuration

Use SQL to change the configuration directly:

-- Add new cluster
INSERT INTO pigsty.group (cls) VALUES ('pg-new');

-- Add cluster variable
INSERT INTO pigsty.group_var (cls, key, value)
VALUES ('pg-new', 'pg_cluster', '"pg-new"');

-- Add host
INSERT INTO pigsty.host (cls, ip) VALUES ('pg-new', '10.10.10.20');

-- Add host variables
INSERT INTO pigsty.host_var (cls, ip, key, value)
VALUES ('pg-new', '10.10.10.20', 'pg_seq', '1'),
       ('pg-new', '10.10.10.20', 'pg_role', '"primary"');

-- Modify global variable
UPDATE pigsty.global_var SET value = '"new-value"' WHERE key = 'some_param';

-- Delete cluster (cascades to hosts and variables)
DELETE FROM pigsty.group WHERE cls = 'pg-old';

Configuration changes are immediately available. You do not need to reload or restart a service.

Switch Back to Static Configuration

Switch back to a static configuration file:

bin/inventory_conf

Advanced Usage

Export Configuration

Export CMDB configuration to YAML format:

psql service=meta -AXtwc "SELECT jsonb_pretty(jsonb_build_object('all', jsonb_build_object('children', children, 'vars', vars))) FROM pigsty.raw_config;"

Or use the ansible-inventory command:

ansible-inventory --list --yaml > exported_config.yml

Configuration Auditing

Track configuration changes using the mtime field:

-- View recently modified global variables
SELECT key, value, mtime FROM pigsty.global_var
ORDER BY mtime DESC LIMIT 10;

-- View changes after a specific time
SELECT * FROM pigsty.group_var
WHERE mtime > '2024-01-01'::timestamptz;

Integration with External Systems

CMDB uses standard PostgreSQL. You can integrate it with these systems:

  • Web Management Interface: Expose configuration data through a REST API, such as PostgREST.
  • CI/CD Pipelines: Read and write database data directly from deployment scripts.
  • Monitoring & Alerting: Generate monitoring rules from configuration data.
  • ITSM Systems: Synchronize data with enterprise CMDB systems.

Considerations

  1. Data Consistency: After you change configuration, run the corresponding Ansible playbooks again to apply it to the environment.

  2. Backup: CMDB configuration data is critical. Back it up regularly.

  3. Permissions: Set appropriate CMDB database access permissions to prevent accidental changes.

  4. Transactions: Make batch configuration changes in a transaction. This permits rollback if an error occurs.

  5. Connection Pooling: inventory.sh opens a new connection each time it runs. Consider connection pooling if Ansible runs frequently.


Summary

Use CMDB for large cluster deployments, complex queries, external integration, or fine-grained access control. PostgreSQL stores the configuration and provides database functions to manage it.

FeatureDescription
StoragePostgreSQL pigsty schema
Dynamic Inventoryinventory.sh script
Config Loadbin/inventory_load
Switch to CMDBbin/inventory_cmdb
Switch to YAMLbin/inventory_conf
Core Viewpigsty.inventory

4 - High Availability

Pigsty uses Patroni to implement PostgreSQL high availability, ensuring automatic failover when the primary becomes unavailable.

Overview

Pigsty PostgreSQL clusters include HA through Patroni, Etcd, and HAProxy.

A cluster with two or more instances gains automatic HA without extra configuration. A surviving instance can provide the complete database service. Clients can connect through any cluster node without tracking primary-replica changes.

Default norm mode targets RTO below 45 seconds. With asynchronous replication, pg_rpo=1MiB limits sampled lag for failover candidates. It does not bound actual data loss. Strict synchronous mode with crit.yml gives acknowledged transactions RPO = 0 during failover. Configure these settings for your hardware and reliability requirements.

Built-in HAProxy load balancers switch traffic automatically. Clients can use DNS, VIP, LVS, and other access methods. Failover and switchover cause brief interruptions but require no connection-string changes or application restarts. These short maintenance windows support sequential cluster maintenance and upgrades without application coordination. Automatic recovery lets staff defer hardware repairs until the next day.

pigsty-ha

Large organizations and core institutions have run Pigsty in production for extended periods. The largest deployment has 25K CPU cores and more than 220 PostgreSQL instances. Each large instance uses 64 cores, 512GB memory, and 3TB NVMe SSD storage. Over five years, this deployment maintained more than 99.999% availability despite dozens of hardware failures and other incidents.


What problems does High Availability solve?

  • Improve availability in the data security C/IA model: RPO ≈ 0 and RTO < 45s.
  • Perform sequential maintenance with short maintenance windows.
  • Recover automatically from hardware failures without immediate staff intervention.
  • Use replicas for read-only requests to reduce primary load and use available resources.

What are the costs of High Availability?

  • Infrastructure: HA needs DCS, such as etcd, zk, or consul, for consensus.
  • Minimum deployment: Meaningful HA requires at least three nodes.
  • Resources: Each replica consumes more resources, although this is usually manageable.
  • Complexity: Backup costs and operational complexity increase substantially, requiring management tools.

Limitations of High Availability

Streaming replicas receive changes in real time, including accidental or defective changes such as DROP TABLE and DELETE. HA therefore cannot recover data lost through human error or software defects. Use delayed clusters or PITR with earlier base backups and archived WAL.

Configuration StrategyRTORPO
Standalone + Nothing Data permanently lost, unrecoverable All data lost
Standalone + Base Backup Depends on backup size and bandwidth (hours) Lose data since last backup (hours to days)
Standalone + Base Backup + WAL Archive Depends on backup size and bandwidth (hours) Lose unarchived data (tens of MB)
Primary-Replica + Manual Failover ~10 minutes Lose data in replication lag (~100KB)
Primary-Replica + Auto Failover Within 1 minute Lose data in replication lag (~100KB)
Primary-Replica + Auto Failover + Sync Commit Within 1 minute No data loss

How It Works

The HA components work together as follows:

  • PostgreSQL uses standard streaming replication to build physical replicas; replicas take over when the primary fails.
  • Patroni manages PostgreSQL server processes and handles high availability matters.
  • Etcd provides distributed configuration storage (DCS) capability and is used for leader election after failures.
  • Patroni relies on Etcd to reach cluster leader consensus and provides health check interfaces externally.
  • HAProxy exposes cluster services externally and uses Patroni health check interfaces to automatically distribute traffic to healthy nodes.
  • vip-manager provides an optional Layer 2 VIP, retrieves leader information from Etcd, and binds the VIP to the node where the cluster primary resides.

Primary failure triggers an election. The healthiest replica, with the highest LSN and least potential data loss, becomes primary. Read/write traffic then moves to it. From failure until promotion, writes block or fail. This write service interruption typically lasts 15–30 seconds and usually remains below 1 minute.

After a replica fails, read-only traffic moves to other replicas. If all replicas fail, the primary handles it. Queries running on the failed replica abort because their connections reset. Other available replicas immediately take over read-only service.

Patroni and Etcd detect failures together. The leader holds a lease and must renew it before its TTL expires. Default norm mode uses 30 seconds. Expiration triggers Failover and a new election.

Use Switchover to change primary without a failure. Writes experience a brief interruption before routing to the new primary. Use this for sequential server maintenance or upgrades.

4.1 - RPO Trade-offs

Trade-off analysis for RPO (Recovery Point Objective), finding the optimal balance between availability and data loss.

RPO (Recovery Point Objective) defines the maximum amount of data loss allowed when the primary fails.

Critical data, such as financial transactions, typically requires RPO = 0: no data loss.

Stricter RPO targets increase write latency and reduce throughput. Replica failure can also make the primary unavailable. Some workloads accept limited data loss for higher performance and availability.


Trade-offs

Asynchronous replicas normally lag behind the primary by about 10KB–100KB or 100µs–10ms, depending on network and throughput. At primary failure, a replica might not have received the latest data. Promotion can lose those unreplicated changes.

pg_rpo sets Patroni maximum_lag_on_failover. The default is 1048576 (1MiB). This is a sampled candidate-lag threshold. It does not set a hard bound on actual data loss.

After primary failure, Pigsty can promote a replica whose lag is within the threshold. If every replica exceeds it, Pigsty refuses [automatic failover] to avoid data loss. An operator must choose between waiting for primary recovery, which might never occur, and accepting loss through forced promotion.

Patroni does not sample primary WAL position continuously. Worst-case asynchronous loss can include WAL from the latest ttl window. On average, sampling adds roughly loop_wait/2 of WAL. Account for write rate when setting the threshold. A higher threshold allows more candidates and increases the chance of automatic failover.

Set pg_rpo = 0 to enable synchronous replication. The primary acknowledges writes only after at least one replica persists them. This removes replication lag for acknowledged writes, but increases latency and reduces throughput.

flowchart LR
    A([Primary Failure]) --> B{Synchronous<br/>Replication?}

    B -->|No| C{Lag < RPO?}
    B -->|Yes| D{Sync Replica<br/>Available?}

    C -->|Yes| E[Lossy Auto Failover<br/>Sampled candidate lag is within threshold]
    C -->|No| F[Refuse Auto Failover<br/>Wait for Primary Recovery<br/>or Manual Intervention]

    D -->|Yes| G[Lossless Auto Failover<br/>RPO = 0]
    D -->|No| H{Strict Mode?}

    H -->|No| C
    H -->|Yes| F

    style A fill:#dc3545,stroke:#b02a37,color:#fff
    style E fill:#F0AD4E,stroke:#146c43,color:#fff
    style G fill:#198754,stroke:#146c43,color:#fff
    style F fill:#BE002F,stroke:#565e64,color:#fff

Protection Modes

Pigsty offers three protection modes for different RPO requirements. They resemble Oracle Data Guard protection modes.

Maximum Performance
  • Default asynchronous mode: Commit after local WAL persistence without waiting for replicas. Replica failures do not affect the primary.
  • Primary failure can lose unsent or unreceived WAL. The 1MiB default sampled candidate threshold does not bound actual loss.
  • Use for performance-sensitive workloads that tolerate limited failure-related data loss.
Maximum Availability
  • Set pg_rpo = 0 to enable synchronous_mode: true.
  • Normally, wait for at least one replica acknowledgement to prevent data loss. If all synchronous replicas fail, automatically switch to asynchronous operation.
  • This balances data protection and availability. It is recommended for critical production workloads.
Maximum Protection
  • Use crit.yml to enable synchronous_mode: true and synchronous_mode_strict: true.
  • A transaction must persist on at least one replica before success. If all synchronous replicas fail, the primary refuses writes.
  • Use for financial transactions, medical records, and other workloads with very strict integrity requirements.
NameMaximum PerformanceMaximum AvailabilityMaximum Protection
ReplicationAsynchronousSynchronousStrict Synchronous
Data LossPossible (replication lag)Zero normally, minor when degradedZero
Write LatencyLowestMedium (+1 network RTT)Medium (+1 network RTT)
ThroughputHighestReducedReduced
Replica Failure ImpactNoneAuto degrade, service continuesPrimary stops writes
RPOPossible loss; 1MiB default candidate threshold= 0 normally / possible loss after degradation= 0
Use CaseTypical business, performance firstCritical business, safety firstFinancial core, compliance first
ConfigurationDefault configpg_rpo = 0pg_conf: crit.yml

Implementation

These modes use two Patroni parameters: synchronous_mode and synchronous_mode_strict.

  • synchronous_mode enables synchronous replication. When enabled, synchronous_mode_strict controls strict behavior.
  • synchronous_mode_strict = false is the default. If replicas fail, allow asynchronous operation so the primary continues service: Maximum Availability.
  • synchronous_mode_strict = true prevents asynchronous fallback. The primary stops writes until a synchronous replica recovers: Maximum Protection.
Modesynchronous_modesynchronous_mode_strictReplication ModeReplica Failure Behavior
Max Performancefalse-AsyncNo impact
Max AvailabilitytruefalseSynchronousAuto degrade to async
Max ProtectiontruetrueStrict SynchronousPrimary refuses writes

Set pg_rpo to 0 to enable synchronous_mode and Maximum Availability. Select pg_conf = crit.yml to enable synchronous_mode_strict and Maximum Protection. You can also enable watchdog to fence a frozen primary node or Patroni process instead of degrading. This provides behavior equivalent to Oracle Maximum Protection.

You can configure Patroni directly. Consult Patroni and PostgreSQL documentation for stronger protection options:

  • Configure the synchronous replica list. Add synchronous replicas for greater disaster tolerance, use quorum commit, or require acknowledgements from all replicas.
  • Set synchronous_commit to 'remote_apply' for consistent primary/replica reads and writes. Oracle Maximum Protection corresponds to remote_write.

Recommendations

Maximum Performance is the default asynchronous mode and suits most workloads. It accepts some failure-related loss for higher throughput and availability. pg_rpo sets sampled candidate lag. Actual worst-case loss also depends on write rate, ttl, and sampling time.

Maximum Availability suits high-integrity workloads. Acknowledged transactions are protected while a synchronous replica is healthy. If all synchronous replicas fail, the cluster can switch to asynchronous operation. At least two PostgreSQL nodes are required: one primary and one replica. Set pg_rpo to 0.

Maximum Protection suits financial transactions, medical records, and other workloads requiring strict integrity. Use at least three nodes: one primary and two replicas. With only two nodes, replica failure stops primary writes and reduces service availability. With three, the primary can continue after one replica fails.

4.2 - Failure Model

Detailed analysis of worst-case, best-case, and average RTO calculation logic and results across three classic failure detection/recovery paths

Patroni failures have 10 categories by failure target. Detection paths group them into five categories, described here.

#Failure ScenarioDescriptionFinal Path
1PG process crashcrash, OOM killedActive Detection
2PG connection refusedmax_connectionsActive Detection
3PG zombieProcess alive but unresponsiveActive Detection (timeout)
4Patroni process crashkill -9, OOMPassive Detection
5Patroni zombieProcess alive but stuckWatchdog
6Node downPower outage, hardware failurePassive Detection
7Node zombieIO hang, CPU starvationWatchdog
8Primary ↔ DCS network failureFirewall, switch failureNetwork Partition
9Storage failureDisk failure, disk full, mount failureActive Detection or Watchdog
10Manual switchoverSwitchover/FailoverManual Trigger

For RTO calculations, all failures follow two paths. This section calculates their minimum, maximum, and average recovery times.

flowchart LR
    A([Primary Failure]) --> B{Patroni<br/>Detected?}

    B -->|PG Crash| C[Attempt Local Restart]
    B -->|Node Down| D[Wait TTL Expiration]

    C -->|Success| E([Local Recovery])
    C -->|Fail/Timeout| F[Release Leader Lock]

    D --> F
    F --> G[Replica Election]
    G --> H[Execute Promote]
    H --> I[HAProxy Detects]
    I --> J([Service Restored])

    style A fill:#dc3545,stroke:#b02a37,color:#fff
    style E fill:#198754,stroke:#146c43,color:#fff
    style J fill:#198754,stroke:#146c43,color:#fff

4.2.1 - Model of Patroni Passive Failure

Failover path triggered by node crash causing leader lease expiration and cluster election
infographic list-row-simple-horizontal-arrow
data

  desc Lease Expiration Stages
  items
    - label Lease Expiration
    - label Replica Detect
    - label Elect & Promote
    - label Haproxy Up
theme light
  palette antv

RTO Timeline

tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 12, data: [Lease Expiration, Replica Detection, Lock Contest & Promote, Health Check] }
grid: { left: 64, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: Seconds, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 10, fontFamily: monospace }, data: [wide-max, wide-avg, wide-min, "", safe-max, safe-avg, safe-min, "", norm-max, norm-avg, norm-min, "", fast-max, fast-avg, fast-min] }
series:
  - { name: Lease Expire, type: bar, stack: main, barWidth: 20, z: 2, emphasis: { focus: series }, itemStyle: { color: "#e15759" }, data: [120, 110, 100, "-", 60, 55, 50, "-", 30, 27, 25, "-", 20, 17, 15] }
  - { name: Replica Detect, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
  - { name: Elect & Promote, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0] }
  - { name: HAProxy Check, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, "-", 6, 5, 3, "-", 4, 3, 2, "-", 2, 2, 1] }
  - { name: Total RTO, type: bar, barGap: "-100%", barWidth: 20, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [150, 127, 104, "-", 78, 66, 53, "-", 41, 34, 27, "-", 29, 23, 16] }
  - { name: RTO Budget, type: bar, barGap: "-100%", barWidth: 20, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, "-", 90, 90, 90, "-", 45, 45, 45, "-", 30, 30, 30] }

Failure Model

PhaseBestWorstAverageDescription
Lease Expirationttl - loopttlttl - loop/2Best: crash just before refresh
Worst: crash right after refresh
Replica Detect0looploop / 2Best: exactly at check point
Worst: just missed check point
Election Promote021Best: direct lock and promote
Worst: API timeout + Promote
HAProxy Check(rise-1) × fastinter(rise-1) × fastinter + inter(rise-1) × fastinter + inter/2Best: state change before check
Worst: state change right after check

Key Difference Between Passive and Active Failover:

ScenarioPatroni StatusLease HandlingPrimary Wait Time
Active Failover (PG crash)Alive, healthyActively tries to restart PG, releases lease on timeoutprimary_start_timeout
Passive Failover (Node crash)Dies with nodeCannot actively release, must wait for TTL expirationttl

During passive failure, Patroni stops with the node and cannot release the Leader Key. An election waits until the DCS lease TTL expires.


Timeline Analysis

Phase 1: Lease Expiration

Primary Patroni renews the Leader Key each loop_wait cycle and resets its TTL.

Timeline:
     t-loop        t          t+ttl-loop    t+ttl
       |           |              |           |
    Last Refresh  Failure      Best Case   Worst Case
       |←── loop ──→|              |           |
       |←──────────── ttl ─────────────────────→|
  • Best case: Failure occurs just before lease refresh (elapsed loop since last refresh), remaining TTL = ttl - loop
  • Worst case: Failure occurs right after lease refresh, must wait full ttl
  • Average case: ttl - loop/2
Texpire={ttl−loopBestttl−loop/2AveragettlWorstT_{expire} = \begin{cases} ttl - loop & \text{Best} \\ ttl - loop/2 & \text{Average} \\ ttl & \text{Worst} \end{cases}

Phase 2: Replica Detection

Each loop_wait cycle, replicas check the DCS Leader Key.

Timeline:
    Lease Expired   Replica Wakes
       |            |
       |←── 0~loop ─→|
  • Best case: Replica happens to wake when lease expires, wait 0
  • Worst case: Replica just entered sleep when lease expires, wait loop
  • Average case: loop/2
Tdetect={0Bestloop/2AverageloopWorstT_{detect} = \begin{cases} 0 & \text{Best} \\ loop/2 & \text{Average} \\ loop & \text{Worst} \end{cases}

Phase 3: Lock Contest & Promote

After detecting Leader Key expiration, replicas begin an election. The winner acquires the key and runs pg_ctl promote.

  1. Query replica replication positions in parallel through REST APIs. Typical response time is 10ms; the hardcoded timeout is 2s.
  2. Compare WAL positions and select the best candidate. Replicas attempt atomic CAS creation of the Leader Key.
  3. Run pg_ctl promote. Its duration is usually negligible.
Election Flow:
  ReplicaA ──→ Query replication position ──→ Compare ──→ Contest lock ──→ Success
  ReplicaB ──→ Query replication position ──→ Compare ──→ Contest lock ──→ Fail
  • Best case: Single replica or immediate lock acquisition and promotion, constant overhead 0.1s
  • Worst case: DCS API call timeout: 2s
  • Average case: 1s constant overhead
Telect={0.1Best1Average2WorstT_{elect} = \begin{cases} 0.1 & \text{Best} \\ 1 & \text{Average} \\ 2 & \text{Worst} \end{cases}

Phase 4: Health Check

HAProxy requires rise consecutive successful checks before routing traffic to the new primary.

Detection Timeline:
  New Primary    First Check   Second Check  Third Check (UP)
     |          |           |           |
     |←─ 0~inter ─→|←─ fast ─→|←─ fast ─→|
  • Best case: New primary promoted just before check, (rise-1) × fastinter
  • Worst case: New primary promoted right after check, (rise-1) × fastinter + inter
  • Average case: (rise-1) × fastinter + inter/2
Thaproxy={(rise−1)×fastinterBest(rise−1)×fastinter+inter/2Average(rise−1)×fastinter+interWorstT_{haproxy} = \begin{cases} (rise-1) \times fastinter & \text{Best} \\ (rise-1) \times fastinter + inter/2 & \text{Average} \\ (rise-1) \times fastinter + inter & \text{Worst} \end{cases}

RTO Formula

Add the phase durations to calculate total RTO:

Best Case

RTOmin=ttl−loop+0.1+(rise−1)×fastinterRTO_{min} = ttl - loop + 0.1 + (rise-1) \times fastinter

Average Case

RTOavg=ttl+1+inter/2+(rise−1)×fastinterRTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinter

Worst Case

RTOmax=ttl+loop+2+inter+(rise−1)×fastinterRTO_{max} = ttl + loop + 2 + inter + (rise-1) \times fastinter

Model Calculation

Apply the four RTO presets to these formulas:

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'500ms'  ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'     ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1500ms' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'     ,'4s' ,3 ,3 ]  # rto < 150s

Four Mode Calculation Results (unit: seconds, format: min / avg / max)

Phasefastnormsafewide
Lease Expiration15 / 17 / 2025 / 27 / 3050 / 55 / 60100 / 110 / 120
Replica Detection0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
Lock Contest & Promote0 / 1 / 20 / 1 / 20 / 1 / 20 / 1 / 2
Health Check1 / 2 / 22 / 3 / 43 / 5 / 64 / 6 / 8
Total16 / 23 / 2927 / 34 / 4153 / 66 / 78104 / 127 / 150

4.2.2 - Model of Patroni Active Failure

PostgreSQL primary process crashes while Patroni stays alive and attempts restart, triggering failover after timeout
infographic list-row-simple-horizontal-arrow
data
  desc When Patroni is healthy but PostgreSQL crashes
  items
    - label Crash Found
    - label Restart Timeout
    - label Replica Detect
    - label Elect Promote
    - label HAProxy Check
theme light
  palette antv

RTO Timeline

tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 12, data: [ Crash Found, Restart Timeout, Replica Detection, Elect Promote, HAProxy Check] }
grid: { left: 64, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: Seconds, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 10, fontFamily: monospace }, data: [wide-max, wide-avg, wide-min, "", safe-max, safe-avg, safe-min, "", norm-max, norm-avg, norm-min, "", fast-max, fast-avg, fast-min] }
series:
  - { name: Crash Found, type: bar, stack: main, barWidth: 20, z: 2, emphasis: { focus: series }, itemStyle: { color: "#b07aa1" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
  - { name: Restart Timeout, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#f28e2c" }, data: [95, 95, 0, "-", 45, 45, 0, "-", 25, 25, 0, "-", 15, 15, 0] }
  - { name: Replica Detect, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
  - { name: Elect Promote, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0] }
  - { name: HAProxy Check, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, "-", 6, 5, 3, "-", 4, 3, 2, "-", 2, 2, 1] }
  - { name: RTO Total, type: bar, barGap: "-100%", barWidth: 20, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [145, 122, 4, "-", 73, 61, 3, "-", 41, 35, 2, "-", 29, 24, 1] }
  - { name: RTO Budget, type: bar, barGap: "-100%", barWidth: 20, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, "-", 90, 90, 90, "-", 45, 45, 45, "-", 30, 30, 30] }

Failure Model

ItemBestWorstAverageDescription
Crash Found0looploop/2Best: PG crashes right before check
Worst: PG crashes right after check
Restart Timeout0startstartBest: PG recovers instantly
Worst: Wait full start timeout before releasing lease
Replica Detect0looploop/2Best: Right at check point
Worst: Just missed check point
Elect Promote021Best: Acquire lock and promote directly
Worst: API timeout + Promote
HAProxy Check(rise-1) × fastinter(rise-1) × fastinter + inter(rise-1) × fastinter + inter/2Best: State changes before check
Worst: State changes right after check

Key Difference Between Active and Passive Failure:

ScenarioPatroni StatusLease HandlingMain Wait Time
Active Failure (PG crash)Alive, healthyActively tries to restart PG, releases lease after timeoutprimary_start_timeout
Passive Failure (node down)Dies with nodeCannot actively release, must wait for TTL expiryttl

During active failure, Patroni remains available. It detects a PostgreSQL crash and attempts a restart. If restart succeeds, service recovers locally. If recovery times out, Patroni releases the Leader Key and triggers an election.


Timing Analysis

Phase 1: Failure Detection

Each loop_wait cycle, Patroni checks PostgreSQL through pg_isready or a process check.

Timeline:
    Last check      PG crash      Next check
       |              |              |
       |←── 0~loop ──→|              |
  • Best case: PG crashes right before Patroni check, detected immediately, wait 0
  • Worst case: PG crashes right after check, wait for next cycle, wait loop
  • Average case: loop/2
Tdetect={0Bestloop/2AverageloopWorstT_{detect} = \begin{cases} 0 & \text{Best} \\ loop/2 & \text{Average} \\ loop & \text{Worst} \end{cases}

Phase 2: Restart Timeout

After detecting a crash, Patroni attempts to restart PostgreSQL. There are two possible results:

Timeline:
  Crash detected     Restart attempt     Success/Timeout
      |                  |                    |
      |←──── 0 ~ start ─────────────────────→|

Path A: Self-healing Success (Best case)

  • PostgreSQL restarts and service recovers.
  • No failover occurs, so RTO is very short.
  • Wait: 0 relative to the failover path.

Path B: Failover Required (Average/Worst case)

  • PostgreSQL does not recover before primary_start_timeout.
  • Patroni releases the Leader Key.
  • Wait: start.
Trestart={0Best (self-healing success)startAverage (failover required)startWorstT_{restart} = \begin{cases} 0 & \text{Best (self-healing success)} \\ start & \text{Average (failover required)} \\ start & \text{Worst} \end{cases}

Note: The average assumes failover is necessary. A quick local restart can substantially reduce RTO.

Phase 3: Standby Detection

Standbys check the DCS Leader Key each loop_wait cycle. After the primary releases it, they detect its absence and begin an election.

Timeline:
    Lease released    Standby wakes
       |                  |
       |←── 0~loop ──────→|
  • Best case: Standby wakes right when lease is released, wait 0
  • Worst case: Standby just went to sleep when lease released, wait loop
  • Average case: loop/2
Tstandby={0Bestloop/2AverageloopWorstT_{standby} = \begin{cases} 0 & \text{Best} \\ loop/2 & \text{Average} \\ loop & \text{Worst} \end{cases}

Phase 4: Lock & Promote

When the Leader Key is absent, standbys begin an election. The winner acquires the key and runs pg_ctl promote.

  1. Query standby replication positions in parallel through REST APIs. Typical response time is 10ms; the hardcoded timeout is 2s.
  2. Compare WAL positions and select the best candidate. Standbys attempt atomic CAS creation of the Leader Key.
  3. Run pg_ctl promote. Its duration is usually negligible.
Election process:
  StandbyA ──→ Query replication position ──→ Compare ──→ Try lock ──→ Success
  StandbyB ──→ Query replication position ──→ Compare ──→ Try lock ──→ Fail
  • Best case: Single standby or direct lock acquisition and promote, constant overhead 0.1s
  • Worst case: DCS API call timeout: 2s
  • Average case: 1s constant overhead
Telect={0.1Best1Average2WorstT_{elect} = \begin{cases} 0.1 & \text{Best} \\ 1 & \text{Average} \\ 2 & \text{Worst} \end{cases}

Phase 5: Health Check

HAProxy requires rise consecutive successful checks before it marks the new primary healthy.

Check timeline:
  New primary    First check    Second check   Third check (UP)
     |              |               |               |
     |←─ 0~inter ──→|←─── fast ────→|←─── fast ────→|
  • Best case: New primary comes up right at check time, (rise-1) × fastinter
  • Worst case: New primary comes up right after check, (rise-1) × fastinter + inter
  • Average case: (rise-1) × fastinter + inter/2
Thaproxy={(rise−1)×fastinterBest(rise−1)×fastinter+inter/2Average(rise−1)×fastinter+interWorstT_{haproxy} = \begin{cases} (rise-1) \times fastinter & \text{Best} \\ (rise-1) \times fastinter + inter/2 & \text{Average} \\ (rise-1) \times fastinter + inter & \text{Worst} \end{cases}

RTO Formula

Add the phase durations to calculate total RTO:

Best Case (PG instant self-healing)

RTOmin=0+0+0+0.1+(rise−1)×fastinter≈(rise−1)×fastinterRTO_{min} = 0 + 0 + 0 + 0.1 + (rise-1) \times fastinter \approx (rise-1) \times fastinter

Average Case (Failover required)

RTOavg=loop+start+1+inter/2+(rise−1)×fastinterRTO_{avg} = loop + start + 1 + inter/2 + (rise-1) \times fastinter

Worst Case

RTOmax=loop×2+start+2+inter+(rise−1)×fastinterRTO_{max} = loop \times 2 + start + 2 + inter + (rise-1) \times fastinter

Model Calculation

Apply the four RTO presets to these formulas:

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'500ms'  ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'     ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1500ms' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'     ,'4s' ,3 ,3 ]  # rto < 150s

Calculation Results for Four Modes (unit: seconds, format: min / avg / max)

Phasefastnormsafewide
Failure Detection0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
Restart Timeout0 / 15 / 150 / 25 / 250 / 45 / 450 / 95 / 95
Standby Detection0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
Lock & Promote0 / 1 / 20 / 1 / 20 / 1 / 20 / 1 / 2
Health Check1 / 2 / 22 / 3 / 43 / 5 / 64 / 6 / 8
Total1 / 24 / 292 / 35 / 413 / 61 / 734 / 122 / 145

Comparison with Passive Failure

PhaseActive Failure (PG crash)Passive Failure (node down)Description
Detection MechanismPatroni active detectionTTL passive expiryActive detection discovers failure faster
Core Waitstartttlstart is usually less than ttl, but requires additional failure detection time
Lease HandlingActive releasePassive expiryActive release is more timely
Self-healing PossibleYesNoActive detection can attempt local recovery

RTO Comparison (Average case):

ModeActive Failure (PG crash)Passive Failure (node down)Difference
fast24s23s+1s
norm35s34s+1s
safe61s66s-5s
wide122s127s-5s

Analysis: In fast and norm, active failures wait for primary_start_timeout (start), making RTO slightly longer than for passive failures. In safe and wide, start < ttl - loop, so active recovery is faster. A successful local restart can make the best-case active RTO much shorter.

4.2.3 - Network Partition

Primary loses DCS connectivity, causing lease expiration and triggering split-brain protection and failover
infographic list-row-simple-horizontal-arrow
data
  title Network Partition Failover Flow
  desc Primary partitioned from DCS, Patroni proactively demotes to prevent split-brain, waits for TTL expiration before switchover
  items
    - label Primary Demote
      desc Patroni demotes PG after retry timeout
      icon mingcute/shield-fill
    - label Lease Expiration
      desc Leader Key TTL expires
      icon mingcute/close-circle-fill
    - label Replica Detection
      desc Replica detects lease expiration, starts election
      icon mingcute/key-2-fill
    - label Lock & Promote
      desc Replica acquires lock and promotes to new primary
      icon mingcute/radar-fill
    - label Health Check
      desc HAProxy detects new primary online
      icon mingcute/arrow-up-circle-fill
theme light
  palette antv

RTO Timeline

tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 12, data: [Primary Demote, Lease Expiration, Replica Detection, Lock & Promote, Health Check] }
grid: { left: 64, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: sec, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 10, fontFamily: monospace }, data: [wide-max, wide-avg, wide-min, "", safe-max, safe-avg, safe-min, "", norm-max, norm-avg, norm-min, "", fast-max, fast-avg, fast-min] }
series:
  - { name: Primary Demote, type: bar, stack: main, barWidth: 20, z: 2, emphasis: { focus: series }, itemStyle: { color: "#76b7b2" }, data: [50, 40, 30, "-", 30, 25, 20, "-", 15, 13, 10, "-", 10, 8, 5] }
  - { name: Lease Expiration, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#e15759" }, data: [70, 70, 70, "-", 30, 30, 30, "-", 15, 15, 15, "-", 10, 10, 10] }
  - { name: Replica Detection, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, "-", 10, 5, 0, "-", 5, 3, 0, "-", 5, 3, 0] }
  - { name: Lock & Promote, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0, "-", 2, 1, 0] }
  - { name: Health Check, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, "-", 6, 5, 3, "-", 4, 3, 2, "-", 2, 2, 1] }
  - { name: RTO Total, type: bar, barGap: "-100%", barWidth: 20, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [150, 127, 104, "-", 78, 66, 53, "-", 41, 34, 27, "-", 29, 23, 16] }
  - { name: RTO Budget, type: bar, barGap: "-100%", barWidth: 20, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, "-", 90, 90, 90, "-", 45, 45, 45, "-", 30, 30, 30] }

Failure Model

PhaseBestWorstAverageNotes
Demoteretryloop + retryloop/2 + retryPatroni retries after detecting partition, demotes after timeout
Lease Expirationttl - loop - retryttl - loop - retryttl - loop - retryRemaining TTL time after demotion (approximately constant)
Replica Detection0looploop/2Best: Right at detection point
Worst: Just missed detection
Lock & Promote021Best: Direct lock and promote
Worst: API timeout + Promote
Health Check(rise-1) × fastinter(rise-1) × fastinter + inter(rise-1) × fastinter + inter/2Best: State changes before check
Worst: State changes right after check

Key difference between network partition and node crash:

ScenarioPatroni StatePostgreSQL StateLease HandlingSplit-brain Risk
Node Crash (Expire)Dies with nodeCompletely unavailablePassive wait for TTL expirationNone
Network Partition (This scenario)Alive but cannot access DCSMay still be running (needs active demotion)Passive wait for TTL expirationYes, needs protection

During a network partition, the primary can continue accepting writes, creating split-brain risk. If Patroni cannot renew the Leader Key, it actively demotes PostgreSQL to read-only or shuts it down.


Timeline Analysis

Phase 1: Primary Demotion

If a network partition blocks DCS access, primary Patroni retries its failed Leader Key renewal.

Timeline:
  Partition      Detect partition      Retry timeout      Primary demotes
     |               |                    |                    |
     |←── loop ──→|←── retry ──→|
  • Detection: The next loop_wait cycle detects the partition.
  • Retry: Patroni retries DCS operations during retry_timeout.
  • Demotion: After retries time out, Patroni demotes PostgreSQL to prevent split brain.
Tdemote={retrybest (partition right before detection)loop/2+retryaverageloop+retryworst (partition right after refresh)T_{demote} = \begin{cases} retry & \text{best (partition right before detection)} \\ loop/2 + retry & \text{average} \\ loop + retry & \text{worst (partition right after refresh)} \end{cases}

Design constraint: loop_wait + 2 × retry_timeout ≤ ttl lets the primary demote before the lease expires.

Phase 2: Lease Expiration

After demotion, the Leader Key remains in DCS until its TTL expires.

Timeline:
  Primary demoted                   TTL expires
     |                                 |
     |←── ttl - (loop + retry) ──→|

After demotion, the cluster waits for the remaining TTL. Partition detection and remaining TTL are negatively correlated in this model. An earlier partition means slower detection but more remaining TTL. Their sum is constant:

Texpire=ttl−loop−retry(approximately constant)T_{expire} = ttl - loop - retry \quad \text{(approximately constant)}

Note: Demotion plus lease expiration takes approximately ttl, as with an expired lease after node failure.

Phase 3: Replica Detection

Each loop_wait cycle, a replica checks the DCS Leader Key.

Timeline:
    Lease expired      Replica wakes
       |                  |
       |←── 0~loop ─→|
  • Best case: Replica wakes right when lease expires, wait 0
  • Worst case: Replica just entered sleep when lease expires, wait loop
  • Average case: loop/2
Tdetect={0bestloop/2averageloopworstT_{detect} = \begin{cases} 0 & \text{best} \\ loop/2 & \text{average} \\ loop & \text{worst} \end{cases}

Phase 4: Lock & Promote

After detecting Leader Key expiration, replicas begin an election.

Election flow:
  ReplicaA ──→ Query replication position ──→ Compare ──→ Try lock ──→ Success
  ReplicaB ──→ Query replication position ──→ Compare ──→ Try lock ──→ Fail
  • Best case: Single replica or directly acquires lock and promotes, ≈ 0
  • Worst case: DCS API call timeout, 2s
  • Average case: 1s
Telect={0best1average2worstT_{elect} = \begin{cases} 0 & \text{best} \\ 1 & \text{average} \\ 2 & \text{worst} \end{cases}

Phase 5: Health Check

HAProxy requires rise consecutive successful checks before it marks the new primary healthy.

Detection timeline:
  New primary    First check    Second check   Third check (UP)
     |              |               |               |
     |←─ 0~inter ─→|←─ fast ─→|←─ fast ─→|
  • Best case: (rise-1) × fastinter
  • Worst case: (rise-1) × fastinter + inter
  • Average case: (rise-1) × fastinter + inter/2
Thaproxy={(rise−1)×fastinterbest(rise−1)×fastinter+inter/2average(rise−1)×fastinter+interworstT_{haproxy} = \begin{cases} (rise-1) \times fastinter & \text{best} \\ (rise-1) \times fastinter + inter/2 & \text{average} \\ (rise-1) \times fastinter + inter & \text{worst} \end{cases}

RTO Formula

Add the phase durations to calculate total RTO.

Demotion plus lease expiration is approximately ttl. Thus, the network-partition RTO formula matches the expired-lease failure formula:

Best Case

RTOmin=ttl−loop+0.1+(rise−1)×fastinterRTO_{min} = ttl - loop + 0.1 + (rise-1) \times fastinterRTOmin≈ttl−loop+(rise−1)×fastinterRTO_{min} \approx ttl - loop + (rise-1) \times fastinter

Average Case

RTOavg=ttl+1+inter/2+(rise−1)×fastinterRTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinterRTOavg=ttl+1+inter/2+(rise−1)×fastinterRTO_{avg} = ttl + 1 + inter/2 + (rise-1) \times fastinter

Worst Case

RTOmax=ttl+loop+2+inter+(rise−1)×fastinterRTO_{max} = ttl + loop + 2 + inter + (rise-1) \times fastinterRTOmax=ttl+loop+2+inter+(rise−1)×fastinterRTO_{max} = ttl + loop + 2 + inter + (rise-1) \times fastinter

Model Calculation

Apply the four RTO presets to these formulas:

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'500ms'  ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'     ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1500ms' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'     ,'4s' ,3 ,3 ]  # rto < 150s

Patroni constraint validation (loop + 2×retry ≤ ttl):

ModeloopretryTTLloop + 2×retryMeets constraint?
fast5520s15s✓ Safe
norm51030s25s✓ Safe
safe102060s50s✓ Safe
wide2030120s80s✓ Safe

Four mode calculation results (seconds, format: min / avg / max)

Phasefastnormsafewide
Primary Demote5 / 8 / 1010 / 13 / 1520 / 25 / 3030 / 40 / 50
Lease Expiration10153070
Replica Detection0 / 3 / 50 / 3 / 50 / 5 / 100 / 10 / 20
Lock & Promote0 / 1 / 20 / 1 / 20 / 1 / 20 / 1 / 2
Health Check1 / 2 / 22 / 3 / 43 / 5 / 64 / 6 / 8
Total16 / 23 / 2927 / 34 / 4153 / 66 / 78104 / 127 / 150

Network partitions and node crashes have the same RTO in this model because both wait for TTL expiration.


Split-brain Protection

Split brain is the main partition risk: the old primary might still accept writes. Patroni provides these protections:

1. Primary Self-Demotion

If Patroni cannot renew the Leader Key, it actively demotes PostgreSQL.

# Patroni pseudo-code logic
if not can_refresh_leader_key():
    retry_until(retry_timeout)
    if still_cannot_refresh():
        demote_postgresql()  # Demote to read-only or shut down

2. Linux Watchdog

If Patroni hangs and cannot demote PostgreSQL, Linux watchdog forces a system restart.

# patroni.yml configuration
watchdog:
  mode: required  # Require watchdog available
  device: /dev/watchdog
  safety_margin: 5

3. Fencing Mechanism

Configure fencing scripts to isolate the old primary forcibly, for example by disabling its network interface or stopping services.


Special Scenarios

Scenario A: Primary partitioned from DCS, replicas normal

The following is the most common partition scenario and the main subject of this page.

┌─────────┐         ╳         ┌─────────┐
│ Primary │ ←── Partition ──→ │   DCS   │
│ Patroni │                   │  etcd   │
└─────────┘                   └─────────┘
                                  ↑
                              Normal connection
                                  ↓
                              ┌─────────┐
                              │ Replica │
                              │ Patroni │
                              └─────────┘
  • Primary Patroni cannot refresh Leader Key → Active demotion
  • Replica normally detects TTL expiration → Elected as new primary
  • RTO ≈ Expire failure RTO

Scenario B: Primary normal, replica partitioned from DCS

┌─────────┐                   ┌─────────┐
│ Primary │ ←── Normal ──→    │   DCS   │
│ Patroni │                   │  etcd   │
└─────────┘                   └─────────┘
                                  ╳
                              Partition
                                  ╳
                              ┌─────────┐
                              │ Replica │
                              │ Patroni │
                              └─────────┘
  • Primary normally refreshes Leader Key
  • Replica cannot participate in election (but replication can continue)
  • No failover triggered, service continues normally

Scenario C: All nodes partitioned from DCS

┌─────────┐         ╳         ┌─────────┐
│ Primary │ ←── Partition ──→ │   DCS   │
│ Patroni │                   │  etcd   │
└─────────┘                   └─────────┘
                                  ╳
┌─────────┐         ╳             │
│ Replica │ ←── Partition ────────┘
│ Patroni │
└─────────┘
  • Primary demotes, replica cannot elect
  • Cluster completely unavailable
  • Requires manual intervention to restore DCS connectivity

Comparison with Other Failures

Failure TypePrimary StateLease HandlingRTOSplit-brain Risk
Expire FailureNode crashPassive wait TTL expiration16s ~ 150sNone
Crash FailurePG crash, Patroni aliveRelease after restart timeout1s ~ 111sNone
Network PartitionAlive but isolated from DCSPassive wait TTL expiration16s ~ 150sYes, needs protection
Manual SwitchoverNormal or failedDirect release/acquire1s ~ 11sNone

A network partition has the same RTO as an expired-lease failure, but also needs split-brain protection. The constraint loop_wait + 2 × retry_timeout ≤ ttl is central to that protection.

4.3 - RTO Trade-offs

Trade-off analysis for RTO (Recovery Time Objective), finding the optimal balance between recovery speed and false failover risk.

RTO (Recovery Time Objective) defines the maximum time required for the system to restore write capability when the primary fails.

Critical transaction systems typically need the shortest practical RTO, often below one minute.

Shorter RTO increases false failover risk. Network jitter can appear to be a failure. Cross-datacenter or cross-region deployments often allow 1–2 minutes to reduce this risk.


Trade-offs

pg_rto controls the failover unavailability limit. Presets fast, norm, safe, and wide support different network and deployment conditions. The default is norm, approximately 45 seconds.

Recovery includes detection, DCS lock expiration, election, promotion, and HAProxy detection of the new primary. Shorter phase timeouts reduce RTO. They also increase sensitivity to network jitter and false failover risk.

Choose a preset that balances recovery speed and false failover risk for your network. Poor networks need longer timeouts. Stable networks can use shorter timeouts.

flowchart LR
    A([Primary Failure]) --> B{Patroni<br/>Detected?}

    B -->|PG Crash| C[Attempt Local Restart]
    B -->|Node Down| D[Wait TTL Expiration]

    C -->|Success| E([Local Recovery])
    C -->|Fail/Timeout| F[Release Leader Lock]

    D --> F
    F --> G[Replica Election]
    G --> H[Execute Promote]
    H --> I[HAProxy Detects]
    I --> J([Service Restored])

    style A fill:#dc3545,stroke:#b02a37,color:#fff
    style E fill:#198754,stroke:#146c43,color:#fff
    style J fill:#198754,stroke:#146c43,color:#fff

Four Modes

Select one of four RTO presets for the network conditions.

Namefastnormsafewide
Use CaseSame rackSame datacenter (default)Same region, cross-DCCross-region/continent
Network< 1ms, very stable1-5ms, normal10-50ms, cross-DC100-200ms, public network
Target RTO30s45s90s150s
False Failover RiskHigherMediumLowerVery Low
Configurationpg_rto: fastpg_rto: normpg_rto: safepg_rto: wide
fast: Same Rack/Switch
  • Use for stable networks below 1ms latency, such as one rack or switch.
  • Average RTO: 14s; worst case: 29s. TTL is 20s; check interval is 5s.
  • This needs the best network quality. Jitter can trigger failover, so false failover risk is higher.
norm: Same Datacenter (Default)
  • Default mode for normal same-datacenter networks, 1–5ms latency, and reasonable packet loss.
  • Average RTO: 21s; worst case: 43s. TTL is 30s, allowing some network variation.
  • This balances recovery speed and stability for most production environments.
safe: Same Region, Cross-Datacenter
  • Use for same-region or same-area datacenters with 10–50ms latency and occasional jitter.
  • Average RTO: 43s; worst case: 91s. TTL is 60s.
  • The 60s primary restart wait allows more local recovery time and reduces false failover risk.
wide: Cross-Region/Continent
  • Use across regions or continents with 100–200ms latency and possible public-network packet loss.
  • Average RTO: 92s; worst case: 207s. TTL is 120s.
  • Slower recovery provides a very low false failover rate for geographic disaster recovery.

RTO Timeline

Patroni HA has two main paths. Active detection finds a PostgreSQL crash and attempts a restart. Passive lease expiration waits for the TTL after node failure, then starts an election.

tooltip: { trigger: axis, axisPointer: { type: shadow }, formatter: $fn:fmt }
legend: { top: 0, itemGap: 10, data: [Lease Expiration, Failure Detection, Restart Timeout, Replica Detection, Lock & Promote, Health Check] }
grid: { left: 110, right: 24, bottom: 32, top: 40 }
xAxis: { type: value, name: Seconds, nameLocation: end, max: 160, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: true, lineStyle: { type: dashed, opacity: 0.5 } }, minorTick: { show: true, splitNumber: 5 }, minorSplitLine: { show: true, lineStyle: { type: dotted, opacity: 0.2 } } }
yAxis: { type: category, axisLine: { show: true }, axisTick: { show: true }, splitLine: { show: false }, axisLabel: { fontSize: 9, fontFamily: monospace }, data: [wide-passive-max, wide-passive-avg, wide-passive-min, wide-active-max, wide-active-avg, wide-active-min, "", safe-passive-max, safe-passive-avg, safe-passive-min, safe-active-max, safe-active-avg, safe-active-min, "", norm-passive-max, norm-passive-avg, norm-passive-min, norm-active-max, norm-active-avg, norm-active-min, "", fast-passive-max, fast-passive-avg, fast-passive-min, fast-active-max, fast-active-avg, fast-active-min] }
series:
  - { name: Lease Expiration, type: bar, stack: main, barWidth: 16, z: 2, emphasis: { focus: series }, itemStyle: { color: "#e15759" }, data: [120, 110, 100, "-", "-", "-", "-", 60, 55, 50, "-", "-", "-", "-", 30, 27, 25, "-", "-", "-", "-", 20, 17, 15, "-", "-", "-"] }
  - { name: Failure Detection, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#b07aa1" }, data: ["-", "-", "-", 20, 10, 0, "-", "-", "-", "-", 10, 5, 0, "-", "-", "-", "-", 5, 3, 0, "-", "-", "-", "-", 5, 3, 0] }
  - { name: Restart Timeout, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#f28e2c" }, data: ["-", "-", "-", 95, 95, 0, "-", "-", "-", "-", 45, 45, 0, "-", "-", "-", "-", 25, 25, 0, "-", "-", "-", "-", 15, 15, 0] }
  - { name: Replica Detection, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#edc949" }, data: [20, 10, 0, 20, 10, 0, "-", 10, 5, 0, 10, 5, 0, "-", 5, 3, 0, 5, 3, 0, "-", 5, 3, 0, 5, 3, 0] }
  - { name: Lock & Promote, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#59a14f" }, data: [2, 1, 0, 2, 1, 0, "-", 2, 1, 0, 2, 1, 0, "-", 2, 1, 0, 2, 1, 0, "-", 2, 1, 0, 2, 1, 0] }
  - { name: Health Check, type: bar, stack: main, z: 2, emphasis: { focus: series }, itemStyle: { color: "#4e79a7" }, data: [8, 6, 4, 8, 6, 4, "-", 6, 5, 3, 6, 5, 3, "-", 4, 3, 2, 4, 3, 2, "-", 2, 2, 1, 2, 2, 1] }
  - { name: RTO Total, type: bar, barGap: "-100%", barWidth: 16, z: 1, itemStyle: { color: "#888", opacity: 0 }, emphasis: { itemStyle: { opacity: 0 } }, data: [150, 127, 104, 145, 122, 4, "-", 78, 66, 53, 73, 61, 3, "-", 41, 34, 27, 41, 35, 2, "-", 29, 23, 16, 29, 24, 1] }
  - { name: RTO Budget, type: bar, barGap: "-100%", barWidth: 16, z: 0, itemStyle: { color: "rgba(0,0,0,0.08)" }, emphasis: { itemStyle: { color: "rgba(0,0,0,0.12)" } }, data: [150, 150, 150, 150, 150, 150, "-", 90, 90, 90, 90, 90, 90, "-", 45, 45, 45, 45, 45, 45, "-", 30, 30, 30, 30, 30, 30] }

Implementation

Each preset selects values for these 10 Patroni and HAProxy parameters:

ComponentParameterfastnormsafewideDescription
patronittl203060120Leader lock TTL (seconds)
loop_wait551020HA loop check interval (seconds)
retry_timeout5102030DCS operation retry timeout (seconds)
primary_start_timeout15254595Primary restart wait time (seconds)
safety_margin551015Watchdog safety margin (seconds)
haproxyinter1s2s3s4sNormal state check interval
fastinter500ms1s1500ms2sState transition check interval
downinter1s2s3s4sDOWN state check interval
rise3333Consecutive successes to mark UP
fall3333Consecutive failures to mark DOWN

Patroni Parameters

  • ttl: Leader lease lifetime. Failure to renew triggers expiration and election. It sets passive detection delay.
  • loop_wait: Main loop interval. Each cycle checks health and synchronizes state.
  • retry_timeout: DCS retry period. After partition-related retries fail, the primary demotes to prevent split brain.
  • primary_start_timeout: Time allowed for local restart after a PostgreSQL crash. On expiration, release the Leader lock and fail over.
  • safety_margin: Watchdog time allowance for restarting the system before split brain can occur.

HAProxy Parameters

  • inter: Check interval while service state is stable.
  • fastinter: Shorter check interval during state changes.
  • downinter: Check interval for recovery after a service is marked DOWN.
  • rise: Consecutive successes required for UP. A new primary must pass this many checks before receiving traffic.
  • fall: Consecutive failures required for DOWN.

Key Constraint

Patroni constraint: The primary must complete demotion before TTL expiration to prevent split brain.

loop_wait+2×retry_timeout≤ttlloop\_wait + 2 \times retry\_timeout \leq ttl

Data Summary


Recommendations

fast requires latency below 1ms and very low packet loss. Use it only within one rack or switch for strict RTO targets. Test it thoroughly in production before enabling it.

norm is the default for most same-datacenter deployments. This page’s model gives passive and active averages of about 34 and 35 seconds. Its timeout allowance reduces false failovers from jitter.

safe suits same-city datacenters with higher latency or occasional jitter. Longer timeouts reduce false failovers. It is recommended for cross-datacenter recovery.

wide suits high-latency, cross-region or cross-continent networks, including public-network packet loss. It favors stability over speed. Its long timeouts keep false failover rates very low.

ModeTarget RTOPassive RTOActive RTOScenario
fast3016 / 23 / 291 / 24 / 29Same switch, high-quality network
norm4527 / 34 / 412 / 35 / 41Default, same DC, standard network
safe9053 / 66 / 783 / 61 / 73Same-city active-active / cross-DC DR
wide150104 / 127 / 1504 / 122 / 145Geo-DR / cross-country
default32622 / 34 / 462 / 314 / 326Patroni default params

Set pg_rto to a preset name to configure Patroni and HAProxy automatically. The template checks pg_rto in pg_rto_plan. A numeric or unknown key falls back to norm. This fallback does not support an RTO expressed as seconds.

Presets come from pg_rto_plan. Override this mapping for a custom RTO strategy.

pg_rto_plan:  # [ttl, loop, retry, start, margin, inter, fastinter, downinter, rise, fall]
  fast: [ 20  ,5  ,5  ,15 ,5  ,'1s' ,'500ms'  ,'1s' ,3 ,3 ]  # rto < 30s
  norm: [ 30  ,5  ,10 ,25 ,5  ,'2s' ,'1s'     ,'2s' ,3 ,3 ]  # rto < 45s
  safe: [ 60  ,10 ,20 ,45 ,10 ,'3s' ,'1500ms' ,'3s' ,3 ,3 ]  # rto < 90s
  wide: [ 120 ,20 ,30 ,95 ,15 ,'4s' ,'2s'     ,'4s' ,3 ,3 ]  # rto < 150s

4.4 - Service Access

Pigsty uses HAProxy to provide service access, with optional pgBouncer for connection pooling, and optional L2 VIP and DNS access.

Split read and write operations, route traffic correctly, and deliver PostgreSQL cluster capabilities reliably.

A service exposes database cluster capabilities through a stable interface that hides the underlying cluster details.

Services provide stable production access, especially during HA failover. Personal users usually do not need this abstraction.


Personal Users

Services are mainly for production. Personal users with one node can connect directly through the instance name or IP address.

For example, connect to the default single-node pg-meta.meta database with any of these three users:

psql postgres://dbuser_dba:[email protected]/meta     # Connect directly with DBA superuser
psql postgres://dbuser_meta:[email protected]/meta   # Connect with default business admin user
psql postgres://dbuser_view:DBUser.Viewer@pg-meta/meta     # Connect with default read-only user via instance domain name

Service Overview

Production clusters commonly use primary-replica replication. Exactly one primary accepts writes. Replicas continuously receive its change logs and can serve read-only requests. This reduces primary load for read-heavy workloads. Separate write requests from read-only requests to use this capacity.

Pgbouncer reduces connection and backend-process overhead for frequent short connections. ETL and change operations can bypass the pool for direct access. Failover changes the primary, so write traffic must follow the new leader automatically. The Service abstraction combines read/write separation, pooled or direct access, and automatic failover routing.

Every database cluster needs this basic service:

  • Read-write service (primary): Can read from and write to the database

Production clusters should provide at least these two services:

  • Read-write service (primary): Write data: Can only be served by the primary.
  • Read-only service (replica): Read data: Can be served by replicas; falls back to primary when no replicas are available

Depending on the workload, add services such as:

  • Default direct service (default): Let administrators bypass the connection pool.
  • Offline replica service (offline): Use a dedicated replica for ETL and analytics, separate from online reads.
  • Sync replica service (standby): Serve reads without replication delay from a synchronous standby or primary.
  • Delayed replica service (delayed): Read earlier cluster data from delayed replicas.

Access Services

Pigsty provides service access up to the cluster HAProxy endpoints. Clients can reach these load balancers in several ways.

Typically, DNS or a VIP points to one or more cluster load balancers.

pigsty-access.jpg

Choose a host and port combination for the required access method.

Host

TypeSampleDescription
Cluster Domain Namepg-testResolved by dnsmasq on INFRA nodes; with pg_dns_target: auto, points to the VIP when enabled, otherwise to the primary IP
Cluster VIP Address10.10.10.3When pg_vip_enabled is enabled, an L2 VIP managed by vip-manager and bound to the primary node
Instance Hostnamepg-test-1Access via any instance hostname (resolved by dnsmasq @ infra nodes)
Instance IP Address10.10.10.11Access any instance’s IP address

Port

Different ports select different PostgreSQL services.

PortServiceTypeDescription
5432postgresDatabaseDirect access to postgres server
6432pgbouncerMiddlewareAccess postgres through connection pool middleware
5433primaryServiceAccess primary pgbouncer (or postgres)
5434replicaServiceAccess replica pgbouncer (or postgres)
5436defaultServiceAccess primary postgres
5438offlineServiceAccess offline postgres

Combinations

# Access via cluster domain (this example assumes a cluster VIP; without one, DNS resolves to the primary IP by default)
postgres://test@pg-test:5432/test # DNS -> L2 VIP -> primary direct connection
postgres://test@pg-test:6432/test # DNS -> L2 VIP -> primary connection pool -> primary
postgres://test@pg-test:5433/test # DNS -> L2 VIP -> HAProxy -> primary connection pool -> primary
postgres://test@pg-test:5434/test # DNS -> L2 VIP -> HAProxy -> replica connection pool -> replica
postgres://dbuser_dba@pg-test:5436/test # DNS -> L2 VIP -> HAProxy -> primary direct connection (for admin)
postgres://dbuser_stats@pg-test:5438/test # DNS -> L2 VIP -> HAProxy -> offline direct connection (for ETL/personal queries)

# Access via cluster VIP directly
postgres://[email protected]:5432/test # L2 VIP -> primary direct access
postgres://[email protected]:6432/test # L2 VIP -> primary connection pool -> primary
postgres://[email protected]:5433/test # L2 VIP -> HAProxy -> primary connection pool -> primary
postgres://[email protected]:5434/test # L2 VIP -> HAProxy -> replica connection pool -> replica
postgres://[email protected]:5436/test # L2 VIP -> HAProxy -> primary direct connection (for admin)
postgres://[email protected]:5438/test # L2 VIP -> HAProxy -> offline direct connection (for ETL/personal queries)

# Directly specify any cluster instance name
postgres://test@pg-test-1:5432/test # DNS -> database instance direct connection (singleton access)
postgres://test@pg-test-1:6432/test # DNS -> connection pool -> database
postgres://test@pg-test-1:5433/test # DNS -> HAProxy -> connection pool -> database read/write
postgres://test@pg-test-1:5434/test # DNS -> HAProxy -> connection pool -> database read-only
postgres://dbuser_dba@pg-test-1:5436/test # DNS -> HAProxy -> database direct connection
postgres://dbuser_stats@pg-test-1:5438/test # DNS -> HAProxy -> database offline read/write

# Directly specify any cluster instance IP access
postgres://[email protected]:5432/test # Database instance direct connection (directly specify instance, no automatic traffic distribution)
postgres://[email protected]:6432/test # Connection pool -> database
postgres://[email protected]:5433/test # HAProxy -> connection pool -> database read/write
postgres://[email protected]:5434/test # HAProxy -> connection pool -> database read-only
postgres://[email protected]:5436/test # HAProxy -> database direct connection
postgres://[email protected]:5438/test # HAProxy -> database offline read-write

# Smart client: read/write separation via URL
postgres://[email protected]:6432,10.10.10.12:6432,10.10.10.13:6432/test?target_session_attrs=primary
postgres://[email protected]:6432,10.10.10.12:6432,10.10.10.13:6432/test?target_session_attrs=prefer-standby

5 - Point-in-Time Recovery — A Time Machine for PostgreSQL

High availability handles machine failure; point-in-time recovery handles incorrect data. Pigsty uses pgBackRest to provide PITR out of the box, allowing a cluster to return to any recoverable point covered by its backup and WAL history.

Point-in-Time Recovery (PITR) can restore an earlier cluster state after accidental deletion of data, a table, or a database.

Pigsty enables PITR in its standard PostgreSQL configuration. This work once required specialist DBA knowledge.


Replication Is Not Backup

High availability can fail over to another instance after hardware failure. However, replication is not backup.

Streaming replication sends every primary change to replicas within milliseconds. This includes DELETE without WHERE or DROP TABLE against the wrong database. Failover handles machine failure. If the data is wrong, every replica can contain the same error.

Database disasters fall into two main classes. Multiple copies and automatic failover handle physical service failures. Logical errors require history: a base backup and continuous WAL archives. PostgreSQL uses them to reconstruct the state before an error.

ThreatHigh AvailabilityDelayed ClusterPITR
Hardware or instance failure✔ Automatic failover✘✔, with a longer RTO
Accidental DML, table drop, or database drop✘ The error is replicated✔ Within the delay✔ At any recoverable point
Defective software corrupts data over time✘ The error is replicated✔ Within the delay✔ Try different recovery targets
Entire cluster or site is lost✘✘✔ Only if the repository survives that failure domain

These mechanisms work together. HA restores service quickly. A delayed cluster provides a short recovery window. PITR provides the final recovery path through historical data.


How the Time Machine Works

A database is a state machine. A base backup is a complete physical snapshot at one point. WAL (Write-Ahead Log) records later state changes. With the snapshot and unbroken WAL history, PostgreSQL can replay to any covered target. The backup sets the earliest recovery point. The latest archived WAL sets the most recent recovery point.

Base backup + WAL archive = point-in-time recovery

Pigsty manages both inputs. By default, cluster initialization attempts an initial full backup. The primary continuously sends completed WAL segments to the selected repository. See How PITR Works for backups, archives, targets, and timelines.


Available Out of the Box

The standard PostgreSQL configuration enables PITR. Pigsty prepares each cluster with a backup repository, WAL archiving, and pgBackRest recovery tools. Customize the declarative policy with these parameters:

pg-meta:
  hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary } }
  vars:
    pg_cluster: pg-meta
    pgbackrest_method: minio       # Silo / S3-compatible storage; local is the default
    pg_crontab: [ '00 01 * * * /pg/bin/pg-backup full' ]  # daily full backup at 01:00

The default local method stores backups under /pg/backup and retains two full backups. One successful daily full backup gives a recovery window of roughly 24–48 hours. The remote minio preset uses Silo or compatible S3 storage, AES-256-CBC repository encryption, and time-based retention. With 14-day retention and weekly full backups, its steady-state window is roughly 14–21 days. Both ranges are policy estimates. Actual coverage starts at the oldest usable backup and ends at the latest WAL in the repository.

For declarative recovery, specify a target. The playbook stops the cluster, restores files, replays WAL, and rebuilds HA. An operator must verify the recovered business state.

./pgsql-pitr.yml -l pg-meta -e '{"pg_pitr": { "time": "2026-07-11 10:00:00+08", "action": "promote" }}'

This follows Pigsty’s declarative configuration model. The cluster definition includes backup policy. The recovery target is another declared parameter.


Benefits and Costs

PITR improves data integrity and availability:

  • RPO (maximum data loss) usually decreases to minutes. WAL that has not reached a surviving repository sets this limit.
  • RTO (time to restore service) becomes tens of minutes to hours, instead of permanent loss. It depends on backup size, WAL replay distance, and disk or network throughput.
Standalone strategyEventRTORPO
No backupHost and local data are lostPermanent lossAll data
Base backups onlyHost and local data are lostBackup size and bandwidth, often hoursChanges since the latest backup
Base backups + WAL archivesHost and local data are lostBackup size, replay distance, and bandwidthWAL not yet present in the surviving repository

The costs fall mainly into three areas:

  • Confidentiality: Backups copy business data and need encryption and access control. The remote preset enables repository encryption. Change its default password.
  • Resources: Backups consume storage; archiving consumes bandwidth. Compression, bundling, and block incremental backup reduce these costs. Capacity planning remains necessary.
  • Operations: Monitor backup status and rehearse recovery. A successful backup job does not prove that recovery can meet the required RTO.

PITR does not replace HA. Production designs usually combine HA for physical failures with PITR for logical errors and site recovery.


Next Steps

  • How PITR Works: snapshots, WAL history, recovery windows, targets, and timelines
  • PITR Architecture: pgBackRest, repository selection, archive flow, scheduling, and failover behavior
  • PITR Tradeoffs: failure domains, capacity, retention, and backup frequency
  • Declarative Recovery: the pg_pitr parameter, pgsql-pitr.yml, and pig pitr
  • PITR Scenarios: accidental deletion, bad releases, investigation, and site loss

For the operational runbooks, see PGSQL Backup and Recovery.

5.1 - How PITR Works

Snapshots, WAL history, recovery windows, recovery targets, and timelines: the five concepts needed to reason accurately about PostgreSQL PITR.

A database is a state machine. WAL (Write-Ahead Log) records its changes in order. PostgreSQL writes each change to WAL before applying it to data files. Keep a physical snapshot and all later WAL to replay the database to a selected consistent state.

PITR combines three elements: a snapshot (base backup), history (WAL archive), and a target where replay stops.


Snapshot: Base Backup

A base backup is a physical snapshot of the whole PostgreSQL cluster. It provides the recovery starting point. Pigsty uses pgBackRest to manage three backup types:

TypeContentsRecovery characteristics
FullAll database-cluster filesSelf-contained, shortest chain, largest backup
DifferentialChanges since the latest full backupRestore uses the full plus the differential
IncrementalChanges since the latest backup of any typeSmallest backup, restore depends on its complete chain

Run pg-backup [full|diff|incr] to start a backup. With no argument, it requests incr. If no valid full backup exists, pgBackRest creates one instead. pg_crontab declares recurring jobs and installs them in the postgres user’s crontab.

Backup frequency affects recovery time. A newer usable backup requires less WAL replay to reach the same target. See PITR Tradeoffs.


WAL History

A snapshot contains only its own state. WAL archiving preserves later changes needed to recover beyond it. Standard Patroni templates enable archiving and send each completed WAL segment to pgBackRest:

archive_mode: 'on'
archive_command: 'pgbackrest --stanza=pg-meta archive-push %p'
archive_timeout: 300

Two implementation details matter:

  • archive_timeout: 300: PostgreSQL can force a segment switch after five minutes on a cluster with few writes. A partially filled segment therefore does not wait indefinitely. When WAL is generated, this usually keeps the latest recovery point within minutes. It does not guarantee that every commit is already remote.
  • Asynchronous archive: pgBackRest batches transfers with /pg/spool and archive-async=y. Pigsty sets archive-push-queue-max=4GiB. If repository failure causes the queue to exceed this limit, pgBackRest can discard queued WAL to protect local disk space. The resulting archive gap requires a new full backup to establish a recoverable chain.

Expiration is automatic. When the repository policy expires old backups, pgBackRest also expires WAL that no remaining backup needs. Explicit archive-retention settings can override this behavior.


Recovery Window

The backup and its continuous WAL history form a recovery window:

  • Left boundary: The start of the oldest usable remaining backup chain. Time-based descriptions usually use the oldest retained full backup’s time.
  • Right boundary: The latest WAL successfully archived to a repository that survives the incident.

The window advances as new backups arrive and old chains expire. The local preset keeps two full backups. One successful daily full backup gives roughly one to two days of coverage. The minio preset sets retention_full_type: time and retention_full: 14. With weekly full backups, the oldest retained chain normally gives roughly 14–21 days of steady-state coverage. These are estimates, not SLAs.

Missed backups, archive gaps, archive-retention overrides, and repository loss can change actual coverage. Check pig pb info and perform restore drills to verify the window.

See PITR Tradeoffs and Backup Policy.


Targets: Where Replay Stops

PostgreSQL can identify states within the recovery window in several ways. pg_pitr exposes six target types:

pg_pitr typeMeaningTypical use
defaultReplay through all WAL available from the repositoryRestore the newest archived state after total loss
timeStop at a timestampRecover from accidental DML or DDL
xidStop at a transaction IDExclude a precisely identified bad transaction
lsnStop at a WAL locationLow-level exact targeting
nameStop at a restore point created with pg_create_restore_point()Planned change checkpoint
immediateStop as soon as the selected backup becomes consistentValidate or expose the selected backup state quickly

set selects the backup set that pgBackRest uses as the starting snapshot. It does not specify where replay stops.

Boundary Semantics

Targets are inclusive by default, retaining the transaction at the target. To stop before a known bad target, set exclusive: true. This maps to recovery_target_inclusive = false.

Transactions remain atomic. Recovery retains transactions committed before the effective target and rolls back transactions not yet committed. The resulting state is consistent, without partial transactions.


Timelines

Restoring an earlier state and accepting new writes creates a history branch. PostgreSQL assigns a timeline to each branch. PITR promotion, replica promotion, and failover can create new timelines. New WAL does not overwrite files from old timelines.

gitGraph
    commit id: "Full backup"
    commit id: "Normal writes"
    commit id: "Bad change"
    commit id: "More writes"
    branch Timeline-2
    checkout Timeline-2
    commit id: "PITR before bad change"
    commit id: "New writes"

Retained history permits another attempt if the first target was wrong. Use timeline to select a timeline. Pigsty defaults to latest.

Continue with PITR Architecture to see how these concepts map to Pigsty components and configuration.

5.2 - PITR Architecture

Pigsty implements PITR with pgBackRest: repository selection, archive flow, scheduling, primary-aware backup execution, performance defaults, and observability.

The PITR principle is simple, but implementation must address several constraints. WAL archiving must not block production writes. Object storage backups need encryption. Backup jobs must follow the primary after failover, and shared repositories must isolate clusters. Large numbers of small objects can also limit throughput.

Pigsty uses pgBackRest with production defaults that address these constraints. This page describes the engine, repository abstraction, archive path, scheduler, and execution on the current primary.


Backup Engine: pgBackRest

Pigsty uses pgBackRest to create base backups with backup, receive WAL with archive-push, and restore data with restore and archive-get.

Relevant capabilities include:

  • Parallelism: Backup, archive, and restore operations can use multiple processes.
  • Backup chains: Full, differential, incremental, and block incremental backups reduce repeated transfers and storage.
  • Compression and encryption: pgBackRest includes zstd compression and AES-256-CBC repository encryption.
  • Repository backends: pgBackRest supports POSIX filesystems, S3-compatible services such as Silo and MinIO, Azure, GCS, and SFTP.
  • Bundling: Pack small files into larger repository objects to reduce object storage overhead.

pgBackRest separates cluster histories by stanza. Pigsty uses pg_cluster as the stanza name. Multiple clusters can share storage while retaining separate backup identities:

repository
├── backup/
│   ├── pg-meta/          # base backups for pg-meta
│   └── pg-test/          # base backups for pg-test
└── archive/
    ├── pg-meta/          # archived WAL for pg-meta
    └── pg-test/          # archived WAL for pg-test

Repository Abstraction

pgbackrest_method selects a repository name. pgbackrest_repo contains a dictionary of candidate definitions. Pigsty v4.5.0 renders only the selected pgbackrest_repo[pgbackrest_method] entry as repo1. Listing local and minio does not activate both repositories.

pgbackrest_method: local          # local, minio, or a custom key below
pgbackrest_repo:
  local:
    path: /pg/backup
    retention_full_type: count
    retention_full: 2             # retain two full backups; a third may exist before expiration
  minio:
    type: s3
    s3_endpoint: sss.pigsty
    s3_region: us-east-1
    s3_bucket: pgsql
    s3_key: pgbackrest
    s3_key_secret: S3User.Backup
    s3_uri_style: path
    path: /pgbackrest
    storage_port: 9000
    storage_ca_file: /etc/pki/ca.crt
    block: y
    bundle: y
    bundle_limit: 20MiB
    bundle_size: 128MiB
    cipher_type: aes-256-cbc
    cipher_pass: pgBackRest       # replace this default secret in production
    retention_full_type: time
    retention_full: 14

The local preset provides simple backups and fast local restores. It has no encryption or bundling and retains backups by full-backup count. The minio preset uses remote Silo or compatible S3 storage. It enables encryption, bundling, block incremental backup, and time-based retention.

Pigsty converts underscores in repository keys to hyphens. It adds a repo1- prefix and writes the keys to /etc/pgbackrest/pgbackrest.conf. A custom cloud repository can therefore use pgBackRest options directly:

pgbackrest_method: s3
pgbackrest_repo:
  s3:
    type: s3                         # repo1-type=s3
    s3_endpoint: s3.us-west-1.amazonaws.com
    s3_region: us-west-1
    s3_bucket: <your_bucket>
    s3_key: <your_access_key>
    s3_key_secret: <your_secret>
    s3_uri_style: host
    path: /pgbackrest
    cipher_type: aes-256-cbc
    cipher_pass: <your_password>
    retention_full_type: time
    retention_full: 90

See Backup Repository for Silo, external S3-compatible storage, versioning, object locking, TLS, and credential details.


Archiving and Scheduling

When pgbackrest_enabled is true, the Patroni templates set the following configuration. This option defaults to true.

archive_mode: 'on'
archive_timeout: 300
archive_command: 'pgbackrest --stanza=<cluster> archive-push %p'

Base backups enter the system in two ways:

  • Initial backup: After bootstrapping a top-level primary, Pigsty attempts a backup if pgbackrest_init_backup is true. The task ignores backup failures. It writes /etc/pgbackrest/initial.done only after success, so the marker confirms completion.
  • Scheduled backup: pg_crontab installs jobs in the database superuser’s crontab. The role default is an empty list. Standard examples usually add a daily full backup at 01:00.
pg_crontab: [ '00 01 * * * /pg/bin/pg-backup full' ]

pg-backup [full|diff|incr] wraps pgbackrest backup. Without an argument, it requests an incremental backup. If no usable full backup exists, pgBackRest makes a full backup instead.


Backups Follow the Primary

Every PostgreSQL node receives pgBackRest and the same scheduled job. However, pg-backup checks /pg/bin/pg-role and proceeds only on the current primary. Replicas exit immediately without creating competing backups.

The backup schedule therefore operates independently of HA topology:

  • All members receive the same repository configuration and crontab.
  • After failover, the new primary can run subsequent backups and archive WAL. The schedule needs no change.
  • The current primary alone controls authoritative writes to a stanza.

For a non-local repository, Pigsty adds pgBackRest after basebackup in Patroni’s create_replica_methods. Patroni tries basebackup first. If it fails, Patroni can restore the replica with pgbackrest --delta restore. This moves the copy load from the primary to the repository.


Performance Defaults

The pgBackRest template limits overhead during normal operation and prioritizes restore throughput:

Settingv4.5.0 behaviorRationale
Compressioncompress-type=zstBalance compression ratio and throughput
Backup/archive workersOne quarter of CPU, clamped to 2–4Limit competition with the database
Restore workersAll detected CPU, capped at 8Minimize restore time
Asynchronous archivearchive-async=y, spool under /pg/spoolBatch transfer without synchronous object-store latency
Archive queue limitarchive-push-queue-max=4GiBBound local spool growth
Fast backup startstart-fast=yRequest an immediate checkpoint
Incremental restoredelta=yReuse destination files that already match

The 4 GiB queue limit has a tradeoff. If the repository remains unavailable and the queue exceeds this limit, pgBackRest can discard queued archive files. PostgreSQL continues running, but the WAL archive has a gap. Create a new full backup to establish a new recovery chain. See How PITR Works.


Observability

When backup and exporter settings are enabled, pgbackrest_exporter runs on each PostgreSQL node. It exposes metrics on port 9854. Monitoring uses these metrics to show backup age, type, size, duration, and errors.

Useful diagnostic entry points include:

EntryPurpose
pb infoShell helper for pgbackrest info using the configured stanza
/pg/log/pgbackrest/pgBackRest backup, archive, and restore logs
pg-backupManually request a backup on the primary: full / diff / incr

See Backup Administration for operational checks, then PITR Tradeoffs for policy design.

5.3 - PITR Tradeoffs

Repository location determines the failure domain, retention determines the recovery window, and backup frequency shapes restore time. Together they define a backup policy.

Backups require storage, network traffic, and operational work. They determine how much data you can recover and how quickly service returns. More history usually requires more capacity. A shorter RTO usually requires newer backups and tested procedures.

A backup policy must define repository location, history retention, and backup frequency.


Where: Choose the Failure Domain

Repository location is the most important choice. It determines which disasters the backup can survive.

A local repository (pgbackrest_method: local) uses the primary’s local filesystem. It is simple and fast, with no remote service dependency. Data and backups usually share one failure domain. Machine, disk, or filesystem loss can destroy both. Local backup protects against logical errors. For protection against total host loss, /pg/backup must use independent storage.

An object storage repository (pgbackrest_method: minio or a custom S3 definition) sends backups to Silo or S3. For an independent disaster recovery copy, deploy it outside the database host or site failure domain. The minio preset enables AES-256-CBC repository encryption, bundling, and block incremental backup. Recovery throughput depends on the network and storage service. Operating that service adds responsibility.

ScenarioRecommended repositoryReason
Development, test, demolocalMinimal dependencies; rebuild is acceptable
ProductionDedicated Silo or compatible S3 storageIndependent failure domain and encrypted repository
Cloud deploymentManaged S3-compatible or cloud object storage supported by pgBackRestIndependent storage and lower operational burden
Ransomware/complianceVersioned storage plus correctly configured object lock/retentionPrevent privileged database-host access from deleting protected versions

The repository contains sensitive business data. Change default access keys and cipher_pass. Restrict access and protect credentials separately from database hosts. Verify any object-lock policy. See Backup Repository.


How Long: Capacity and Recovery Window

Longer history generally requires more storage. Actual use depends on compression, deduplication, block incremental backup, database change rate, and the full/differential/incremental backup mix. Measure backup and WAL growth instead of using a fixed multiplier.

For an illustrative 100 GB database changing by 10 GB per day, before compression:

  • Daily full, retain two (local preset): Full backups use about 200 GB, plus WAL. Successful daily jobs commonly provide roughly one to two days of recovery history.
  • Weekly full, daily incremental, retain 14 days (minio preset): The oldest surviving weekly chain commonly provides roughly 14–21 days of coverage. Allow capacity for multiple full backups, their incrementals, archived WAL, and temporary retention-plus-one behavior during expiration.

The retention setting alone does not define the recovery window. Coverage starts at the oldest usable chain and ends at the latest WAL in the surviving repository. Time retention removes an old full backup only when another qualifying full backup satisfies the period. Related incremental backups and WAL follow the retained full chains. Check pig pb info, monitor archive health, and perform a restore to prove coverage.

Retain enough history to cover the delay between an error and its detection. A dropped table may be noticed within minutes. Slow corruption or month-end reconciliation failure can take weeks to detect.


How Often: Backup Frequency and RTO

Recovery first restores a backup chain, then replays WAL to the target. Backup size and storage throughput control restore time. The distance from backup to target controls replay time.

WAL replay is mostly serial. Restoring a weekly full backup just before the next full can require nearly a week of replay on a write-heavy database. Daily incremental backups shorten replay distance and transfer only changes since the preceding backup. They still require a valid chain. Monitor and test the entire chain, not only the latest file.

Choose a backup frequency that meets the measured RTO within the available backup window and production load budget.


Pigsty Presets

Pigsty provides two candidate repository definitions. pgbackrest_method selects one for the generated repo1 configuration.

Standard policy: local repository and daily full backup. This policy is simple and restores through local I/O. Use it for development or environments with separate protection against host loss:

pgbackrest_method: local
pg_crontab: [ '00 01 * * * /pg/bin/pg-backup full' ]
# Local preset retains two full backups; actual coverage depends on successful jobs and WAL continuity.

Production policy: remote Silo/S3 repository, weekly full, daily incremental. Place the repository in a separate failure domain and use the encrypted minio preset:

pgbackrest_method: minio
pg_crontab:
  - '00 01 * * 1 /pg/bin/pg-backup full'
  - '00 01 * * 2,3,4,5,6,7 /pg/bin/pg-backup'
# The preset retains full history by 14 days; weekly fulls commonly yield about 14–21 days.

local and minio entries in pgbackrest_repo are alternatives, not two active repositories. The template renders only pgbackrest_repo[pgbackrest_method] as repo1. Multiple active pgBackRest repositories require explicit advanced configuration. They also require independently tested backup, expiration, and restore procedures. The two presets alone do not provide this setup.

Use Backup Policy for capacity modelling and schedule details.


A Backup Is Proven by Restore

Monitor backup job success, and add clone recovery drills to routine operations. Use them to answer:

  1. Is the chain usable? Restore it end to end and validate data.
  2. What is the measured RTO? Database size and WAL volume change over time.
  3. Can the on-call operator execute the runbook? The first full exercise should not happen during an incident.

Clone recovery leaves the source online and overwrites the selected destination. Verify the exact destination and use disposable infrastructure. See Declarative Recovery for the recovery interface.

5.4 - Declarative Recovery

Declare the desired pg_pitr recovery target and let pgsql-pitr.yml or pig orchestrate the recovery workflow.

A backup must support recovery when an incident occurs. Traditional PITR requires connected manual steps: pause HA, stop PostgreSQL, prepare recovery settings, restore the backup, and replay WAL. Then validate the target, rebuild metadata, and start the cluster again.

Pigsty uses declarative configuration for recovery. Declare the recovery target. The tools stop the cluster, restore data, replay WAL, and return control to the operator.


Declare a Recovery Target

Set pg_pitr to describe the target. Run pgsql-pitr.yml to restore it. The most common form selects a specific time:

./pgsql-pitr.yml -l pg-meta -e '{"pg_pitr": { "time": "2026-07-11 10:00:00+08", "action": "promote" }}'

Fields in this parameter define the six recovery target types and other recovery behavior:

pg_pitr:                           # Recovery declaration; every field is optional
  cluster: pg-meta                 # Source backup stanza; defaults to this cluster
  type: time                       # default | time | xid | lsn | name | immediate
  time: '2026-07-11 10:00:00+08'   # Mutually exclusive with xid, lsn, and name
  exclusive: false                 # Stop before the target; inclusive by default
  action: promote                  # Explicit promotion; a targeted restore defaults to pause
  timeline: latest                 # Target timeline; latest by default
  set: latest                      # Starting backup set; selected automatically by default
  repo: { ... }                    # Temporary repository definition when not using local config
  backup: false                    # Move the old data directory to /pg/data-backup first
  archive: true                    # Preserve archiving; exploratory recovery can set false
  db_include: [ ... ]              # Restore only selected databases
  data: /pg/data                   # Destination data directory

See Restore Operations for the complete field reference and examples.


What the Playbook Does

pgsql-pitr.yml divides recovery into six stages. Ansible tags let you run these stages separately:

StageAction
printPrint the source cluster, target, and restore command; this stage reports the plan and does not prompt for confirmation
pauseRun patronictl pause so Patroni does not intervene during maintenance
stopStop Patroni and PostgreSQL on replicas, then on the primary
pitrRender recovery settings, run an incremental pgBackRest restore, start PostgreSQL to replay WAL, wait for consistency, and print control data
etcdRemove stale cluster metadata from etcd so old and new timelines are not mixed
startStart Patroni again, resume HA management, and rebuild replicas

Keep these details in mind:

  • Incremental restore: pgBackRest uses delta to rewrite only files that differ from the backup. This can substantially reduce RTO for large databases.
  • Verification: The playbook prints checkpoint LSN, timeline, and NextXID from pg_controldata. An operator must still verify the recovered business state.
  • Rollback copy: backup: true moves the original data directory to /pg/data-backup before recovery. A later run with this option removes any existing /pg/data-backup. It does not retain multiple snapshot versions.
  • Staged execution: Run -t down, -t pitr, and -t up separately for an operator checkpoint between phases. The pitr phase completes when PostgreSQL reaches a consistent recovery state. For time, XID, LSN, or named targets, also confirm that WAL replay reached the target.

action controls behavior at the target. promote opens a new timeline, pause waits for inspection, and shutdown stops PostgreSQL. Targeted recovery defaults to pause if you omit action. Run stages separately to retain a manual checkpoint for pause or shutdown. For a single-run recovery, explicitly select promote. The playbook performs recovery steps but cannot determine whether the business data is correct.


Command-Line Recovery with pig

The pig CLI can perform single-instance PITR directly on a database node. It needs neither an administration node nor Ansible:

pig pitr -t "2026-07-11 10:00:00+08"    # Recover to a point in time
pig pitr --xid 250000 -X                # Stop before transaction 250000
pig pitr -d                             # Replay through the WAL archive
pig pitr -I --no-restart                # Prepare immediate recovery and leave PostgreSQL stopped

pig pitr validates the target, stanza, and available backups. It stops Patroni and PostgreSQL, restores data, optionally starts PostgreSQL, and prints further instructions. For a Patroni-managed directory, Patroni stays stopped afterward. Validate the data, then run pig pt start to resume HA management. This workflow does not clear etcd, rebuild replicas, or automatically rejoin the cluster. Destructive forced shutdown requires explicit --force-stop.

pig pb wraps lower-level pgBackRest commands. pb info lists backups, pb backup creates them, and pb restore runs a raw restore. pig pb restore refuses to run while Patroni manages the instance. Patroni could otherwise restart PostgreSQL during restoration. For Patroni-managed instances, use pig pitr or pgsql-pitr.yml.


In-Place and Clone Recovery

The same mechanism supports two different workflows:

DimensionIn-place recoveryClone recovery
MethodRoll the production cluster backRestore a source backup into a different cluster
DowntimeRequired during recoveryThe source production cluster remains online
EffectDiscards all writes after the targetDoes not affect the source; the destination is overwritten and can be retried
Best forWhole-cluster corruption or disaster recoveryRecovering deleted objects, audit work, and recovery drills

For clone recovery, cluster names the source backup stanza. This example restores historical pg-meta data into pg-test while the source remains online:

./pgsql-pitr.yml -l pg-test -e '{"pg_pitr": { "cluster": "pg-meta", "time": "2026-07-11 10:00:00+08", "archive": false, "action": "promote" }}'

To recover a deleted table, exporting it from a clone and importing it into production is generally safer than restoring the whole cluster. See Clone a Database Cluster for the complete workflow and cleanup.


After Recovery

After recovery, complete these incident closeout steps:

  1. New timeline, new backup: After promotion, run pg-backup full to create a recovery window on the new timeline.
  2. Archiving state: If you used archive: false for investigation, restore normal archiving. See Post-Recovery.
  3. Clone cleanup: A clone’s cluster identity differs from its source stanza. Recreate the destination stanza before enabling its backups. See Clone a Database Cluster.

Operators must select the target and choose in-place or clone recovery. They must also verify that the recovered data is correct. The tools execute the selected procedure. See PITR Scenarios for guidance on these decisions.

5.5 - PITR Scenarios

How to choose a recovery target and workflow for accidental DML, dropped objects, defective releases, investigations, and site loss — and why recovery drills must be routine.

Decisions can consume much of the available incident response time. Pigsty automates recovery steps, but the operator must decide three things. Select the target, choose in-place or clone recovery, and define how to validate the result.

Read and rehearse this framework before an incident.


Decision Framework

ScenarioTypical problemRecommended workflowTarget
Accidental DMLDELETE or UPDATE affects the wrong rowsClone, validate, then copy back datatime / xid
Dropped table, schema, or databaseDROP or an incorrect migrationClone, validate, then copy back objectstime / name
Defective release or batch corruptionSoftware writes incorrect data for a periodClone and compare before choosing repair or cutovertime / xid
Audit, investigation, or forensicsInspect historical stateClone and hold at the target for inspectiontime / lsn
Whole-cluster or site lossHosts or storage are gone or encryptedRecover in place on replacement infrastructuredefault / time

Two principles apply throughout:

  • Stop the damage first. Before selecting a target, pause the defective application or remove its write access. The recovery window keeps moving. However, restoring the wrong cluster in haste can cause another incident.
  • Prefer a clone while production is usable. A clone leaves the source unchanged. You can try multiple targets and validate before export or cutover. Recovery overwrites the selected destination cluster. Use in-place recovery when the whole cluster is unusable or the business explicitly accepts rolling back every database.
flowchart TD
    A["Data error detected"] --> B["Contain the source of bad writes"]
    B --> C{"Can production still serve?"}
    C -->|Yes| D["Clone recovery<br/>validate and copy back or cut over"]
    C -->|No| E["In-place recovery<br/>or rebuild on new infrastructure"]
    D --> F["Validate, take a new backup, review the incident"]
    E --> F

Accidental DML

Common PITR cases include DELETE without WHERE, an incorrect UPDATE, and defective batch jobs.

Find the error in application logs, PostgreSQL logs, metrics, or audit records. A timestamp is usually sufficient. If you know the exact transaction ID, use xid with exclusive: true to stop immediately before it.

# If the deletion occurred around 10:15, clone the state from 10:14
./pgsql-pitr.yml -l pg-test -e '{"pg_pitr": { "cluster": "pg-meta", "time": "2026-07-11 10:14:00+08", "archive": false, "action": "promote" }}'

# If the deleting transaction was 250000, stop immediately before it
./pgsql-pitr.yml -l pg-test -e '{"pg_pitr": { "cluster": "pg-meta", "xid": "250000", "exclusive": true, "archive": false, "action": "promote" }}'

Validate the recovered rows. Copy only required data back with pg_dump, COPY, or an application-specific reconciliation procedure. A configured delayed cluster may provide faster access if it remains within its delay window.


Dropped Objects

Use the same approach for DROP TABLE, DROP DATABASE, or a migration in the wrong environment. Prefer a clone more strongly in these cases. Whole-cluster rollback to recover one object also discards every valid write after the target.

Restore a separate destination to before the DDL. Validate the object, export it with pg_dump, and import it into production. Before planned high-risk changes, create a restore point with pg_create_restore_point(). A name target avoids timestamp ambiguity.


Defective Release or Batch Corruption

A faulty release can corrupt data for hours. Identify the last correct state and the full impact. Restore a clone to candidate times and compare it with production. Then decide whether to copy corrected rows back or switch to a recovered cluster.

The application owner must validate this decision. Successful PostgreSQL recovery proves consistency at the target. It does not prove that the business state is correct.


Audit and Investigation

Historical questions, such as a month-end balance, require historical data. Restore a separate destination to a time, LSN, XID, or named point. Inspect it without changing the source.

Targeted recovery defaults to action: pause, which holds recovery at the target for inspection. This option does not configure read-only access or create a separate cluster. The inventory limit and cluster source field determine the destination workflow. For an operator checkpoint before promotion, run -t down, -t pitr, and -t up separately. Explicitly enforce read-only access if required for investigation. immediate stops at the first consistent point; it does not select a historical timestamp.


Site Loss

HA cannot help if every database host is destroyed or encrypted. The repository and other control-plane assets must survive outside that failure domain. Possible repositories include Silo/S3, another protected host or filesystem, and other tested pgBackRest backends. A remote object store is recommended. The essential requirement is survival in an independent failure domain.

Rebuild the hosts and restore the inventory, credentials, and PKI. Select the surviving repository, then restore through the latest archived WAL:

./pgsql-pitr.yml -l pg-meta -e '{"pg_pitr": {"action": "promote"}}'

Inventory and backups alone are insufficient. Preserve installation media or package repositories, repository credentials, encryption passwords, CA material, custom files, and DNS dependencies. Keep an independently accessible runbook. Encrypt secrets and store them separately from database hosts and ordinary source control.


Make Recovery a Routine Drill

Do not run these procedures for the first time during a production incident. Rehearse clone recovery regularly on a disposable destination. Repeat after major architecture changes. Measure these outcomes:

  1. Usability: can the backup and complete WAL chain be restored and validated?
  2. RTO: how long does the actual restore and replay take now?
  3. Operator readiness: can the on-call engineer identify source and destination, select a target, and follow the safety gates?

See Restore Operations and Clone a Database Cluster for the task-level runbooks.

6 - Monitoring System

How Pigsty’s monitoring system is architected and how monitored targets are automatically managed.

The built-in monitoring system provides metrics, logs, and alerting. Logs and alerts also support audit and traceability. It can monitor Pigsty-managed clusters, existing PostgreSQL clusters, and external RDS services.


Monitoring Targets

Pigsty monitoring covers these core targets:

  • PostgreSQL clusters and instances (SQL performance, connections, replication, transactions, checkpoints, WAL)
  • Infrastructure components (Grafana, VictoriaMetrics, Alertmanager, Nginx, etc.)
  • Host nodes (CPU, memory, disk, network, kernel)
  • Key middleware (ETCD, MINIO, REDIS, JUICE, VIBE, etc.)

Technology Stack

ComponentPurpose
GrafanaVisualization dashboards, unified entry point, alert views
VictoriaMetricsTime-series metric ingestion, storage, and query
VictoriaLogsStructured log ingestion, indexing, and search
VMAlert + AlertmanagerAlert rule evaluation and notification delivery
Exporter / AgentDatabase/system metric exposure and log forwarding

Onboarding Modes

Pigsty supports three ways to add monitoring:

ModeUse CaseEntry
FULLDatabase is deployed and managed directly by PigstyPGSQL Monitoring System
MANAGEDExisting PostgreSQL cluster with SSH-manageable nodesMonitor Existing Cluster
RDSCloud database accessible only by connection stringMonitor RDS

Continue Reading

7 - Security and Compliance

Pigsty manages authentication, authorization, encryption, audit, backup, and recovery as code, with a clear path from the default baseline to production hardening.

Databases usually store the most valuable and sensitive data in an information system. Attacks and failures can therefore have severe consequences. Security requires multiple controls: who can connect, what they can do, and whether traffic is protected. It must also record operations and support recovery of damaged, lost, or deleted data.

Pigsty provides a default security baseline through declarative configuration. Declare HBA rules, roles and privileges, certificates, encryption, backups, and audit policies as parameters in the inventory. Idempotent playbooks render and apply these settings.

Security as Code supports policy versioning, review, and traceability. One inventory provides a consistent baseline across instances. To audit database access, start with the YAML declaration. Then verify generated HBA rules and database grants against the running system.


Security as Code

Manual operations can scatter security settings across pg_hba.conf, DBA-issued GRANT statements, and temporary incident firewall rules. Documentation can then diverge from the running configuration. This makes each instance’s effective rules difficult to determine.

Pigsty stores security policy in the cluster definition alongside other properties.

pg-meta:
  hosts: { 10.10.10.10: { pg_seq: 1, pg_role: primary } }
  vars:
    pg_cluster: pg-meta
    pg_users:                     # Who may log in: account, role, and expiration
      - { name: dbuser_app ,password: '<unique-random-password>' ,roles: [dbrole_readwrite] ,expire_in: 365 }
    pg_databases:                 # Databases and their isolation policy
      - { name: app ,owner: dbuser_app ,revokeconn: true }
    pg_hba_rules:                 # Who may connect, from where, and how
      - { user: dbuser_app ,db: app ,addr: 10.1.0.0/16 ,auth: ssl ,order: 50 ,title: 'app access via ssl' }

Declare users, privileges, and HBA rules. Playbooks apply them idempotently to each instance, including new instances. Git history records configuration changes. Manual GRANT statements, runtime parameter changes, and node file edits can still cause drift. In production, regularly compare declared and actual state.


Default Security Baseline

The default configuration enables these controls to reduce omissions:

CapabilityDefault BehaviorRelated Parameter
Password hashingNew or updated PostgreSQL passwords use SCRAM-SHA-256pg_pwd_enc
Data checksumsPage checksums are enabled during cluster initialization to detect silent corruptionpg_checksum
Server-side TLSPostgreSQL server certificates are installed and ssl is enabled, so TLS connections are accepted—
Local CAA self-signed CA is created automatically for managed component certificatesca_create
etcd encryption and authenticationTLS for client and peer traffic, plus RBAC password authenticationetcd_root_password
MINIO object storage HTTPSSilo backup traffic uses HTTPS by defaultminio_https
Nginx HTTPSWeb ingress listens on both ports 80 and 443 by defaultnginx_sslmode
HBA rulesLayered access: local ident, intranet password authentication, and SSL required for public administrator accesspg_default_hba_rules
Roles and privilegesA four-tier role model and default privilege templates provide a least-privilege baselinepg_default_roles
Backup and recoverypgBackRest is enabled by default, with two full backups retained in the local repositorypgbackrest_enabled
FirewallZone mode trusts intranet CIDRs and exposes only required ports to public networksnode_firewall_mode
Restricted sudoSudo access for the database OS user is limited to the required command setpg_dbsu_sudo

Hardening with Trade-offs

Defaults target a trusted intranet. Some controls need explicit enablement because of performance costs, compatibility limits, or required operator decisions:

  • Examples contain public default passwords for quick starts and local tests. Before production, run ./configure -g to randomize recognized credentials. Then check the pgBackRest passphrase, Silo users in ha/safe, and all custom values.
  • The Patroni REST API and PgBouncer disable TLS by default. Explicitly enable patroni_ssl_enabled and pgbouncer_sslmode with the issued certificates.
  • Password checks (passwordcheck) and auditing (pgaudit) are disabled by default. Confirm package availability, then configure preloading and policy.
  • SELinux defaults to permissive. Remove the demo firewall exception for port 5432 in production.
  • Local backups have no encryption by default. The remote minio preset enables AES-256 encryption. Change its default encryption passphrase.

The ha/safe template combines TLS, certificate authentication, password checks, and backup encryption. Use it with the consistency-first CRIT parameter template as a starting point. Review public credentials, audit extensions, and the failure model explicitly. See Security Model for the full hardening path.


This Chapter

SectionQuestion Answered
Security ModelWhere is the root of trust? How many defensive layers exist? How should the baseline be hardened?
AuthenticationWho can connect? How is identity proven? How are HBA rules declared and applied?
Access ControlWhat can a connected user do? How does least privilege become the default?
Encrypted CommunicationHow is traffic encrypted? Who issues, distributes, and rotates certificates?
Data SecurityHow is data kept intact, recoverable, confidential, and traceable?
ComplianceHow do security capabilities map to MLPS and SOC 2 controls?

These pages provide operational security guidance:

7.1 - Security Model

Pigsty trust boundaries and defense in depth, with the admin node as a high-trust control plane and a path from the default baseline to production hardening.

Identify the root of trust and defensive layers before selecting security features. The root of trust needs the strongest protection. Multiple layers preserve some protection if one fails.


Trust Boundaries

Pigsty uses Ansible for declarative deployment. Its administration node is the control plane and needs the strongest protection.

RoleAssets and Privileges
Admin nodeThe pigsty.yml inventory, which normally contains system and application credentials; the CA private key; SSH administration access to every node
INFRA nodesMonitoring and alerts, DNS, Nginx ingress, and software repositories
Database nodesDatabase instances, local dbsu, and restricted sudo
ClientsDatabase credentials or client certificates; access through service ports, HBA, and authentication

These roles have different capabilities, not a simple hierarchy. Protect these three assets in particular:

  1. The pigsty.yml inventory contains component passwords and credentials. Strictly restrict administration node access and access to any Git configuration repository.
  2. The CA private key, files/pki/ca/ca.key, anchors deployment trust. Its holder can issue any trusted certificate. The file uses mode 0600 inside a 0700 directory. Keep an offline backup.
  3. The administration user’s SSH private key provides passwordless sudo access to all enrolled nodes. It effectively grants root access throughout the managed deployment.

Pigsty’s security policy defines this trust boundary. Attacks requiring administration node access, or both pigsty.yml and the CA private key, are not product vulnerabilities. These control-plane assets have high trust by design. Protect them accordingly.


Seven Defensive Layers

Defense in depth combines controls so that one failure does not remove all protection. Pigsty provides seven security layers:

#LayerMechanismsDetails
1Network boundaryFirewall zones, constrained listen addresses, centralized ingressThis page
2Transport encryptionLocal CA and TLS between componentsEncrypted Communication
3AuthenticationHBA rules, SCRAM passwords, client certificatesAuthentication
4Access controlRole model, default privileges, database isolationAccess Control
5Host securitySELinux, restricted sudo, dedicated OS usersThis page
6Data securityChecksums, backup and encryption, PITR, deletion safeguardsData Security
7Audit trailDDL and connection logs, audit extensions, centralized logsData Security

Layers 2, 3, 4, 6, and 7 have dedicated chapters. The following sections cover the network and host layers.

Network Boundaries

Node provisioning enables firewalld or ufw, depending on the OS. node_firewall_mode defaults to zone. node_firewall_intranet places 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 in the trusted zone. Public networks can reach only node_firewall_public_port ports. Defaults are 22 for SSH and 80/443 for web traffic.

The demo pigsty.yml also exposes 5432 for local evaluation. Remove this exception in production. If direct database access is needed, restrict sources to explicit CIDRs through security groups, host firewalls, and HBA.

PostgreSQL listens on all addresses by default: pg_listen is 0.0.0.0. Listen addresses, firewall rules, and HBA jointly control access. For stricter access, constrain the listener:

pg_listen: '${ip},${vip},${lo}'   # Host IP, cluster VIP, and loopback only

The default firewall blocks direct public access to Grafana, VictoriaMetrics, and other web infrastructure. External web traffic normally uses the Nginx portal. Database traffic uses HAProxy service ports. Fewer entry points simplify hardening and auditing.

Host Security

Give each OS user only the privileges its job requires.

  • The database superuser postgres (pg_dbsu) has no password by default. It can enter the database only through local ident. pg_dbsu_sudo defaults to limit. This permits passwordless database service systemctl operations and log viewing, without unrestricted root access.
  • Operators and playbooks use node_admin_username, default dba. It receives passwordless sudo (nopass) by default. For stricter control, set node_admin_sudo to all to require a password, or limit to restrict commands.
  • node_selinux_mode defaults to permissive. SELinux records violations without blocking them, providing a baseline before enforcing.

Pigsty does not manage SSH server configuration. Put password-login restrictions, remote-root restrictions, and similar OS hardening in your host security baseline.


Hardening Levels

Harden the deployment in stages. Each level adds to the previous level:

Level 1: default baseline. Defaults provide SCRAM passwords, data checksums, a local CA, component certificates, layered HBA, four-tier roles, backups, and firewall zones. This suits development, testing, and evaluation on a trusted intranet. For production, review credentials and network boundaries, and configure client verification.

Level 2: randomized credentials. Change all public default passwords before any network-exposed deployment. Add -g to randomize recognized built-in parameters and example credentials:

./configure -g    # --generate: randomize recognized default credentials

This option does not replace pgBackRest cipher_pass, all Silo examples in ha/safe, or custom values. See the Default Credentials Checklist for coverage.

Level 3: policy hardening with ha/safe. conf/ha/safe.yml combines controls for further customization:

  • TLS and certificates: Main TCP HBA rules use ssl. Public administrator access requires a client certificate. PgBouncer uses require, and Patroni API uses HTTPS. Local ident and selected localhost password rules remain.
  • Password policy: The template preloads passwordcheck and sets expire_in for built-in users. Review and replace example passwords.
  • Reduced exposure: Listen addresses are limited to ${ip},${vip},${lo}. Public connection-pool access is explicitly denied for monitoring and administration accounts.
  • Backup encryption: pgBackRest uses the remote minio preset with AES-256-CBC. Replace the predictable example pgBR.${pg_cluster}.
  • Security extensions: The template installs passwordcheck, credcheck, pgaudit, pgsodium, anonymizer, and related extensions. Installation does not preload, create, or configure them.

Level 4: database hardening with crit.yml. The safe template selects CRIT for workloads that prioritize consistency. Compared with oltp, CRIT:

  • Requires checksums regardless of pg_checksum.
  • Enables strict synchronous replication (synchronous_mode_strict). Writes needing synchronous acknowledgment block if no synchronous replica is available.
  • Logs connections and disconnections. PostgreSQL 18 separates connection receipt, authentication, and authorization.
  • Sets watchdog to automatic. It activates only if a usable device exists.

Strict synchronous mode aims to preserve acknowledged transactions. It depends on synchronous_commit, synchronous replica state, and failover eligibility. Verify RPO with failure exercises on the target topology.

You can select individual controls instead of the whole template:

pg-meta:
  hosts:
    10.10.10.10: { pg_seq: 1 , pg_role: primary }
    10.10.10.11: { pg_seq: 2 , pg_role: replica }
    10.10.10.12: { pg_seq: 3 , pg_role: replica }
  vars:
    pg_cluster: pg-meta
    pg_conf: crit.yml                    # Use the CRIT database parameter template
    patroni_ssl_enabled: true            # Enable HTTPS for the Patroni API
    pgbouncer_sslmode: require           # Require TLS for PgBouncer
    pg_listen: '${ip},${vip},${lo}'      # Constrain listen addresses
    pg_libs: '$libdir/passwordcheck, pg_stat_statements, auto_explain'  # Password strength checks

Next

7.2 - Authentication

Pigsty manages PostgreSQL and PgBouncer HBA rules declaratively, combining SCRAM passwords and client certificates to define who may connect and how identity is proven.

pg_hba.conf defines PostgreSQL Host-Based Authentication. It controls who can connect, the source address, the database, and the required identity check.

Manual HBA maintenance across a cluster requires care. Primary and replica rules can differ, and each instance stores its configuration in its data directory. Without a shared declaration and refresh process, instance rules can diverge.

Pigsty declares HBA rules in the inventory through declarative configuration. Playbooks render and distribute them consistently.


HBA as Code

Cluster HBA combines global defaults in pg_default_hba_rules with cluster additions in pg_hba_rules. PgBouncer has separate equivalents: pgb_default_hba_rules and pgb_hba_rules.

Rules support two forms. The recommended alias form expresses one rule per line:

pg_hba_rules:
  - { user: dbuser_app ,db: app ,addr: 10.1.0.0/16 ,auth: ssl ,order: 50 ,title: 'app user access via ssl' }

Use the raw form for cases aliases cannot express. It contains a literal pg_hba.conf line.

In addition to user, address, database, and authentication method, each rule has two control fields:

  • order sets render order. HBA uses the first matching rule, so order sets priority. By convention, 0-99 is for high-priority user rules and 100-999 for defaults. Rules without order come last.
  • role filters by instance role. common and default apply everywhere. primary, replica, offline, standby, and delayed apply only to matching instances. role: offline also applies to instances marked with pg_offline_query. One declaration therefore generates appropriate rules without separate manual primary and replica files.

After editing the declaration, run the wrapper script to render and reload the rules:

bin/pgsql-hba pg-meta          # Render and apply HBA rules for pg-meta

pg_hba_rules adds rules but does not automatically restrict broader defaults. For stricter access, also review pg_default_hba_rules. Inspect the resulting pg_hba.conf on every instance.


Address and Authentication Aliases

Aliases name common cases. Each addr value expands into address blocks:

AliasExpands ToMeaning
localUnix socketLocal socket only
localhostUnix socket, 127.0.0.1/32, and ::1/128Local host
admin<admin_ip>/32Admin node
infra/32 address of each INFRA nodeInfrastructure nodes
cluster/32 address of every cluster memberCluster-internal traffic
intra10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16Intranet CIDRs, customizable with node_firewall_intranet
world0.0.0.0/0 and ::/0Any address
CIDRUnchangedCustom network

Values in auth select the authentication method and whether TLS is mandatory:

AliasAuthentication MethodNotes
denyrejectExplicit rejection
trusttrustUnconditional access; use with care
pwdscram-sha-256 or md5Follows pg_pwd_enc; SCRAM by default
shascram-sha-256Force SCRAM
md5md5Compatibility for legacy clients
sslhostssl with password authenticationPassword authentication over mandatory TLS
ssl-shahostssl with scram-sha-256Mandatory TLS and SCRAM
certhostssl with certClient certificate authentication
ident, osident (peer in PgBouncer)OS user mapping
peerpeerLocal OS user

The user field supports four placeholders: ${dbsu} (superuser), ${repl} (replication user), ${monitor} (monitoring user), and ${admin} (administration user). Rendering replaces them with actual names. +role matches all members of that role.

auth: ssl requires TLS but does not require client verification of the server identity. Security-sensitive clients should use sslmode=verify-full with a trusted CA. See Encrypted Communication.


Default Rules Explained

The default HBA policy requires stronger authentication for more distant sources. These PostgreSQL defaults come from the source configuration:

pg_default_hba_rules:             # postgres default host-based authentication rules, order by `order`
  - {user: '${dbsu}'    ,db: all         ,addr: local     ,auth: ident ,title: 'dbsu access via local os user ident'  ,order: 100}
  - {user: '${dbsu}'    ,db: replication ,addr: local     ,auth: ident ,title: 'dbsu replication from local os ident' ,order: 150}
  - {user: '${repl}'    ,db: replication ,addr: localhost ,auth: pwd   ,title: 'replicator replication from localhost',order: 200}
  - {user: '${repl}'    ,db: replication ,addr: intra     ,auth: pwd   ,title: 'replicator replication from intranet' ,order: 250}
  - {user: '${repl}'    ,db: postgres    ,addr: intra     ,auth: pwd   ,title: 'replicator postgres db from intranet' ,order: 300}
  - {user: '${monitor}' ,db: all         ,addr: localhost ,auth: pwd   ,title: 'monitor from localhost with password' ,order: 350}
  - {user: '${monitor}' ,db: all         ,addr: infra     ,auth: pwd   ,title: 'monitor from infra host with password',order: 400}
  - {user: '${admin}'   ,db: all         ,addr: infra     ,auth: ssl   ,title: 'admin @ infra nodes with pwd & ssl'   ,order: 450}
  - {user: '${admin}'   ,db: all         ,addr: world     ,auth: ssl   ,title: 'admin @ everywhere with ssl & pwd'    ,order: 500}
  - {user: '+dbrole_readonly',db: all    ,addr: localhost ,auth: pwd   ,title: 'pgbouncer read/write via local socket',order: 550}
  - {user: '+dbrole_readonly',db: all    ,addr: intra     ,auth: pwd   ,title: 'read/write biz user via password'     ,order: 600}
  - {user: '+dbrole_offline' ,db: all    ,addr: intra     ,auth: pwd   ,title: 'allow etl offline tasks from intranet',order: 650}

Layer by layer:

  • Local access: postgres can connect only through a local Unix socket with ident. It needs no password and cannot log in remotely. Thus, dbsu has no default password.
  • Intranet access: Replication and application accounts use SCRAM password authentication. Remote monitoring and administration connections mainly come from INFRA nodes.
  • Public access: Only the administrator can connect from any address by default. Both a password and TLS are required.

PgBouncer explicitly denies public access for monitoring and administration accounts. Application users can connect only from localhost and intranet sources.

The default +dbrole_offline rule has no role filter and applies to every instance. Add role: offline to restrict it to pg_role: offline or pg_offline_query: true instances.

The default policy lets application accounts connect from the intranet with passwords. ha/safe changes the main TCP rules to ssl. Administrators outside the intranet must present a client certificate (cert). Local ident and selected localhost password rules remain.


Password Policy

Pigsty defaults to PostgreSQL’s recommended scram-sha-256 password storage through pg_pwd_enc. Use md5 only for legacy clients.

Before ALTER USER ... PASSWORD, the workflow runs SET log_statement TO 'none' to temporarily disable statement logging. This keeps passwords out of PostgreSQL logs. The inventory still contains plaintext passwords. Rendered user SQL is stored at /pg/tmp/pg-user-<name>.sql with mode 0640. Related Ansible tasks do not consistently use no_log. Restrict access to the administration node, configuration repository, and automation output.

Avoid --diff for tasks that contain credentials.

Password strength checks are disabled by default. If required, preload passwordcheck or the more configurable credcheck:

pg_libs: '$libdir/passwordcheck, pg_stat_statements, auto_explain'   # Reject weak passwords

ha/safe sets this pg_libs value explicitly. Selecting CRIT alone does not load passwordcheck.

Set account lifetime with expire_in (days after creation) or expire_at (absolute date). Combine this with your organization’s rotation process:

pg_users:
  - { name: dbuser_app ,password: '<unique-random-password>' ,roles: [dbrole_readwrite] ,expire_in: 365 }

Certificate Authentication

Passwords can be phished, reused, or guessed. For privileged accounts, such as administrators, set HBA auth: cert to require client certificates. The local CA must sign the certificate, and its CN must match the database user name. If the rule accepts only cert, a leaked password alone cannot authenticate.

Issue client certificates with the built-in cert.yml playbook:

./cert.yml -e cn=dbuser_dba                  # Issue a 20-year client certificate by default
./cert.yml -e cn=dbuser_dba -e expire=365d  # Or specify a shorter lifetime

The files are files/pki/misc/<cn>.crt and files/pki/misc/<cn>.key. Deliver the private key through a controlled channel. The client should use verify-full to verify the database server. See Encrypted Communication.


Connection Pool and Component APIs

The database is not the only authenticated entry point.

PgBouncer has separate HBA rules and a separate user list. pgbouncer_auth_query is disabled by default. Only users with pgbouncer: true enter userlist.txt and can authenticate through the pool. Review permitted logins before enabling dynamic authentication queries.

The Patroni REST API controls operations such as restart, switchover, and configuration reload. Writes require HTTP Basic authentication through patroni_username and patroni_password. Source-address allowlists also restrict writes. Enable patroni_ssl_enabled to use HTTPS throughout.

The inventory also declares credentials for Grafana, HAProxy administration, the MINIO-selected object storage backend, etcd, and other components. See the Default Credentials Checklist for the full list and update instructions.


Next

7.3 - Access Control

Pigsty turns least privilege into reusable declarative cluster configuration through a built-in four-tier role model and default privilege templates.

Authentication verifies identity. Authorization determines what that identity can do.

PostgreSQL GRANT and REVOKE provide precise control. Privilege problems usually arise from missing default conventions. Examples include making an application account the owner, retaining temporary superuser access after troubleshooting, and omitting grants on new tables.

Pigsty provides four role tiers, default privileges, and database isolation. This starting model reduces manual grants per database. Operators must still assign roles to match business boundaries and regularly review effective privileges.

pigsty-acl.jpg


Role System

Pigsty creates four business roles by default. They form privilege groups and cannot log in:

RoleAttributeInheritsPurpose
dbrole_readonlyNOLOGIN—Global read-only access
dbrole_readwriteNOLOGINdbrole_readonlyGlobal DML access; the default choice for application accounts
dbrole_adminNOLOGINdbrole_readwrite, pg_monitorObject creation and DDL for administration and release workflows
dbrole_offlineNOLOGIN—Independent read-only role that can be restricted to offline instances through HBA

Pigsty also creates four system users, each with a specific responsibility:

UserAttributePurpose
postgresSUPERUSERDatabase superuser; no password and local ident login only
replicatorREPLICATIONStreaming replication and backup, with pg_monitor and read-only privileges
dbuser_dbaSUPERUSERRoutine administration user that inherits dbrole_admin
dbuser_monitor—Monitoring user with only pg_monitor and read-only privileges

Application accounts join role groups through the roles field and inherit their privileges:

pg_users:
  - { name: dbuser_app    ,password: '...' ,roles: [dbrole_readwrite] }  # Regular application account
  - { name: dbuser_report ,password: '...' ,roles: [dbrole_readonly]  }  # Read-only reporting account
  - { name: dbuser_etl    ,password: '...' ,roles: [dbrole_offline]   }  # Offline ETL account

Customize the declarative role system through pg_default_roles. This parameter replaces the complete list. Retain all required system users and default roles. Also check references in HBA rules, default privileges, and scripts.


Default Privileges

Roles define who receives privileges. Default privileges give newly created objects the required grants automatically.

PostgreSQL provides ALTER DEFAULT PRIVILEGES. Pigsty declares these rules through pg_default_privileges:

pg_default_privileges:            # Apply these privileges to new objects created by managed identities
  - GRANT USAGE      ON SCHEMAS   TO dbrole_readonly
  - GRANT SELECT     ON TABLES    TO dbrole_readonly
  - GRANT SELECT     ON SEQUENCES TO dbrole_readonly
  - GRANT EXECUTE    ON FUNCTIONS TO dbrole_readonly
  - GRANT USAGE      ON SCHEMAS   TO dbrole_offline
  - GRANT SELECT     ON TABLES    TO dbrole_offline
  - GRANT SELECT     ON SEQUENCES TO dbrole_offline
  - GRANT EXECUTE    ON FUNCTIONS TO dbrole_offline
  - GRANT INSERT     ON TABLES    TO dbrole_readwrite
  - GRANT UPDATE     ON TABLES    TO dbrole_readwrite
  - GRANT DELETE     ON TABLES    TO dbrole_readwrite
  - GRANT USAGE      ON SEQUENCES TO dbrole_readwrite
  - GRANT UPDATE     ON SEQUENCES TO dbrole_readwrite
  - GRANT TRUNCATE   ON TABLES    TO dbrole_admin
  - GRANT REFERENCES ON TABLES    TO dbrole_admin
  - GRANT TRIGGER    ON TABLES    TO dbrole_admin
  - GRANT CREATE     ON SCHEMAS   TO dbrole_admin

The read-only role can query data and execute functions. The read-write role also has DML privileges. The administrator role adds privileges needed for object management.

Ownership Convention

Default privileges apply only to objects created by identities with those defaults configured. Pigsty configures them for:

  • the database OS user pg_dbsu, which defaults to postgres;
  • the administration user pg_admin_username, which defaults to dbuser_dba;
  • dbrole_admin;
  • each database owner declared in pg_databases.

Application DDL should normally run as the declared database owner. Administration and release workflows can use dbuser_dba or first run SET ROLE dbrole_admin. Other users need their own ALTER DEFAULT PRIVILEGES configuration. Otherwise, objects they create do not receive this default privilege model.

PostgreSQL default privileges follow the object creator. They do not propagate automatically from the database or session login name. This is PostgreSQL behavior, not a Pigsty limitation.


Database Isolation

By default, PostgreSQL grants database CONNECT to PUBLIC. If HBA permits it, a login role can enter a database it does not own. Restrict this default when multiple applications share a cluster.

Set revokeconn in a database definition to revoke public connection access:

pg_databases:
  - { name: app_a ,owner: dbuser_a ,revokeconn: true }
  - { name: app_b ,owner: dbuser_b ,revokeconn: true }

This revokes CONNECT from PUBLIC. It grants access explicitly to the replication, monitoring, and administration users and the database owner. The owner receives GRANT OPTION and can grant access to others. Without additional grants or inherited roles, app_a cannot connect to app_b.

Cluster initialization also revokes CREATE from PUBLIC on the database and the public schema:

REVOKE CREATE ON DATABASE app FROM PUBLIC;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

Ordinary users cannot freely create objects in public databases or schemas. This reduces risks from unsafe search_path settings and object shadowing. PostgreSQL 15 restricted the default CREATE privilege on the public schema. Pigsty applies this boundary to all supported major versions.


Offline Role and Instance Isolation

dbrole_offline provides independent read-only privileges for ETL, reporting, and ad hoc queries. It controls object access, not which instance accepts connections.

The default intranet HBA rule for +dbrole_offline has no role filter, so it applies to every instance. To restrict it to pg_role: offline or a replica with pg_offline_query: true, change the complete pg_default_hba_rules list:

pg_default_hba_rules:
  # Copy and retain all other default rules; change only the offline-role rule
  - { user: '+dbrole_offline', db: all, addr: intra, auth: pwd, role: offline, order: 650,
      title: 'allow offline users on offline instances' }

pg_default_hba_rules replaces the entire list; do not use the example rule alone. HBA limits expensive queries to offline instances only if it filters by instance role. Users must also lack other inherited roles permitted by broader rules. Add a dedicated service endpoint, connection limits, and query resource controls for resource isolation.


Beyond the Database

Least privilege also applies at the host level:

  • The postgres superuser has no password and permits only local ident login. Its sudo access defaults to required database service and log commands (pg_dbsu_sudo: limit).
  • dbuser_monitor has pg_monitor, the read-only role, and privileges on the monitor schema. By default, it cannot write business tables.
  • replicator receives only directory function privileges needed for backup and recovery, without general superuser access.

Next

7.4 - Encrypted Communication

Pigsty provides a self-signed CA that issues certificates and distributes trust for managed components, creating a unified TLS foundation.

TLS can provide transport encryption, server authentication, and client authentication. Configure each protection separately. Enabling server TLS does not make clients verify the server or make the server require client certificates.

Most TLS operational work involves certificate issuance, distribution, trust, and rotation. Without central management, services often skip verification or keep plaintext connections.

Pigsty manages PKI declaratively. Deployment creates a local self-signed CA, issues component certificates, and distributes trust. TLS is then ready for use.


Local CA

During initial deployment, Pigsty checks for a CA on the administration node. It creates one when required:

FileDescriptionPermissions
files/pki/ca/ca.keyCA private key and root of trust for the deployment; protect it carefully0600, with directory mode 0700
files/pki/ca/ca.crtCA root certificate; safe to distribute0644
  • ca_create controls CA behavior. Pigsty reuses an existing key and certificate unchanged. If only the key exists, it issues a replacement certificate with that key. ca_create: false prevents creation of a missing private key. If ca.key is absent, deployment stops instead of creating an unexpected trust root. Always back up and restore ca.key and ca.crt together.
  • ca_cn sets the certificate CN, default pigsty-ca. The key is RSA 4096.
  • The root CA is valid for 100 years. Component certificates default to 20 years (cert_validity: 7300d). The browser-facing Nginx certificate is an exception, with a current default of 397 days.

Long default lifetimes reduce initial maintenance for private infrastructure. Production still needs certificate rotation. Organizations with a certificate policy should shorten lifetimes and monitor expiration.


Trust Distribution

Every node must trust the CA. When managing a node, Pigsty copies the CA certificate to /etc/pki/ca.crt and links it into the OS trust store:

  • EL family (RHEL, Rocky, Alma): link under /etc/pki/ca-trust/source/anchors/ and run update-ca-trust
  • Debian and Ubuntu: link under /usr/local/share/ca-certificates/ and run update-ca-certificates

Clients that use the OS trust store, such as curl, can then verify Pigsty CA certificates. The Nginx portal also publishes ca.crt at its site root for browsers and external clients.

By default, PostgreSQL libpq clients look for ~/.postgresql/root.crt and use sslmode=prefer. They do not directly use the OS trust store to verify server identity.


Server Identity Verification

Security-sensitive PostgreSQL clients should use sslmode=verify-full and specify the Pigsty CA:

psql "host=pg-meta dbname=postgres user=dbuser_dba sslmode=verify-full sslrootcert=/etc/pki/ca.crt"

verify-full checks the certificate chain and connection hostname. The server certificate SAN must contain the client’s DNS name or IP address. External clients must install ca.crt or specify it with sslrootcert.


Certificate Matrix

The local CA issues certificates for these components under one trust chain:

ComponentCertificate Identity (CN)Deployment PathEncryption State
PostgreSQL<cluster>-<sequence>/pg/cert/server.{crt,key}Server-side SSL enabled by default; HBA determines whether it is mandatory
PgBouncerReuses the PostgreSQL certificate/pg/cert/TLS disabled by default (pgbouncer_sslmode)
PatroniReuses the PostgreSQL certificate/pg/cert/API HTTPS disabled by default (patroni_ssl_enabled)
etcd<instance-name>/etc/etcd/server.{crt,key}TLS for client and peer traffic
Silo<node-name>~minio/.minio/certs/Silo HTTPS is enabled by default (minio_https)
Kafka<cluster>-<sequence>/etc/kafka/pki/kafka.pemSASL_SSL/SSL with kafka_security: scram; defaults to plaintext
MySQL<instance-name>/etc/mysql/pki/server.{crt,key}Secure transport enforced; clients and group replication verify the certificate chain
Nginxpigsty, with portal domains in SAN/etc/nginx/conf.d/cert/HTTPS enabled by default (nginx_sslmode)
INFRA node<node-name>/etc/pki/infra.{crt,key}Available to infrastructure components

These defaults distinguish available TLS from enabled TLS:

  • Enabled at deployment: PostgreSQL accepts SSL connections. etcd uses TLS for clients and peers.
  • Encrypted by default: Object storage backup traffic through MINIO and Nginx web traffic use HTTPS.
  • Available but disabled: Patroni API and PgBouncer TLS is disabled, with certificates already installed. Enable the corresponding parameters. ha/safe enables both.

SSL support, mandatory encryption, and client verification of server identity are separate states. HBA enforces encryption with auth: ssl or cert. Client sslmode and trust settings control server verification. Default rules require TLS only for administrator connections from arbitrary sources. The safe template uses ssl or cert for main TCP rules. It retains local ident and selected localhost password rules.


Client Certificates

cert.yml issues client certificates. For HBA cert authentication, the certificate CN represents the database user name:

./cert.yml -e cn=dbuser_dba                  # Issue a 20-year client certificate by default
./cert.yml -e cn=dbuser_dba -e expire=365d  # Or specify a shorter lifetime

The files are files/pki/misc/<cn>.key and files/pki/misc/<cn>.crt. Deliver private keys through a controlled channel and allow only the corresponding user to read them. Client certificates let the server verify clients. The client must still use verify-full to verify the database server.


Using an Enterprise CA

Pigsty can issue certificates from an existing enterprise CA or an intermediate signed by its root. Put the certificate and private key at these paths. Playbooks reuse an existing CA:

files/pki/ca/ca.key    # CA or intermediate CA private key
files/pki/ca/ca.crt    # Corresponding CA certificate

Also set ca_create: false. Deployment then stops if the private key is missing, rather than creating a new trust root. However, an existing key with a missing certificate still causes certificate reissuance. Verify and restore both files together.


Key Protection and Rotation

  • The CA private key exists only on the administration node. It and pigsty.yml are among the deployment’s highest-trust assets. Keep an offline backup. See Trust Boundaries.
  • If the CA key is compromised, establish a new trust root and reissue every component and client certificate. Plan a period of trust in both CAs to avoid interrupting all connections together.
  • Component certificate sources reside at files/pki/<component>/ on the administration node. Node files are deployment copies. Deleting only a node copy restores the same certificate. To rotate, update or remove the source, rerun the relevant playbook, and reload or roll the component as required.

Next

  • 🔑 Authentication: use HBA to decide who must use SSL or client certificates
  • 🔒 Data Security: encryption for stored data and backups
  • ✅ Compliance: evidence for certificate management

7.5 - Data Security

Protect PostgreSQL data integrity, recoverability, confidentiality, and traceability with checksums, backup and PITR, encryption, and audit logs.

Network boundaries, authentication, and access control reduce incident risk. Data controls must limit damage and support recovery after hardware failure, leaked credentials, or operator errors.

Data security must provide integrity, recoverability, confidentiality if copied, and records of what happened.


Integrity

Bad disk sectors, memory bit flips, and firmware defects can cause silent data corruption, with no immediate error. Pigsty enables pg_checksum: true by default. Initialization uses data-checksums. PostgreSQL calculates each page checksum on write and checks it on read.

Page checksums mainly detect corruption in storage, the I/O path, or pages after writing. They cannot detect every memory error, logical error, or incorrect application write. They do not replace backups.

CRIT requires checksums regardless of the parameter. Its strict synchronous replication (synchronous_mode_strict) blocks writes needing synchronous acknowledgment when no synchronous replica is available. This protects acknowledged transactions under specific conditions. Clients must not reduce synchronous_commit; a synchronous replica must participate in commits. Failover must select a node containing the required WAL. Verify RPO with failure exercises on the target topology.


Recoverability

Replicas mainly handle node failures. Backups cover accidental deletion, logical errors, cluster corruption, and wider disasters. HA can shorten interruptions after primary failure. However, replication copies accidental deletions to every replica, so backups remain essential.

Pigsty enables pgBackRest by default through pgbackrest_enabled. Base backups and continuous WAL archives provide Point-in-Time Recovery. Restore to a target within the retained backup and WAL window.

Select the backup repository with pgbackrest_method:

RepositoryLocationDefault RetentionEncryption
local (default)Local /pg/backup directoryLatest 2 full backupsNone
minioSilo or external S3-compatible object storage14 daysAES-256-CBC

Two additional controls reduce damage from accidental deletion:

  • Delayed replica: For a critical cluster, declare pg_delay: 1h. Before the error replays, pause replication and extract the required data. A delayed replica eventually catches up and does not replace backup.
  • Removal safeguards: Enabling pg_safeguard or etcd_safeguard blocks the corresponding removal playbook. This reduces accidental cluster removal.

Test that backups can restore data through routine recovery exercises. See Backup and Recovery for mechanisms and procedures.


Confidentiality

Protect data at rest at three layers:

Backup encryption. pgbackrest_method: minio selects S3-compatible storage. Providers include MINIO-managed Silo, independently managed MinIO or RustFS, and external S3 services. The preset enables AES-256-CBC encryption. Change its public pgBackRest passphrase in production. ha/safe derives an example passphrase from the cluster name:

pgbackrest_repo:
  minio:
    cipher_type: aes-256-cbc
    cipher_pass: 'pgBR.${pg_cluster}'   # Example only; replace before deployment

pgBR.${pg_cluster} is predictable, and configure -g does not replace it. In production, use a unique random passphrase. Store it separately from the backup. Without it, recovery is impossible.

Local backups have no encryption by default. Encryption reduces disclosure if someone copies backup files or media separately. Protection is limited when keys and backups share a host.

Transport encryption. Uploads to Silo or external S3 use HTTPS. HBA can require SSL for PostgreSQL clients and replication. Clients should also verify server certificates. See Encrypted Communication.

Encryption at rest. Upstream PostgreSQL currently lacks general built-in transparent data encryption (TDE). Pigsty offers two options. Use pg_tde with Percona Distribution for PostgreSQL for table-level encryption; see pgtde. Use security extensions such as pgsodium, pgcrypto, and anonymizer for column encryption and masking. The safe template installs this category. Full-disk encryption, such as LUKS or dm-crypt, adds OS-level protection against stolen media.


Audit and Traceability

Incident records must identify who did what and when. Pigsty provides these logging layers:

Default baseline: Log all DDL with log_statement: ddl. Log statements longer than 100 ms with log_min_duration_statement: 100. PostgreSQL 18 and later also record connection authorization.

CRIT template: log_connections and log_disconnections record connection events. PostgreSQL 18 can distinguish receipt, authentication, and authorization.

pgaudit: Install pgaudit and preload it through pg_libs for detailed statement auditing. This includes object reads, writes, and role-based audit classes. The safe template installs the extension. Declare loading and audit policy explicitly.

With INFRA logging enabled and Vector configured, PostgreSQL logs go to VictoriaLogs for centralized storage. Retention defaults to 15 days and can meet other compliance periods. Logs and metrics support search, alerts, and incident reconstruction. Classification, response, and evidence preservation still require an operational process.


Next

7.6 - Compliance

Compliance combines configuration, process, and evidence. This page covers launch hardening, MLPS and SOC 2 control mappings, supply-chain integrity, and vulnerability response.

Compliance requires continuous evidence of three elements:

  • Configuration: Security controls must be enabled. Pigsty directly provides configuration.
  • Process: The organization must establish access approval, change management, recovery exercises, and related procedures.
  • Evidence: Records must show that configuration and processes remain effective. The inventory, runtime logs, and monitoring system provide part of this evidence.

This page provides a pre-launch checklist and maps Pigsty capabilities to common compliance frameworks. These mappings support architecture and gap analysis. They are not an MLPS assessment, SOC 2 audit opinion, or legal advice.


Default Credentials Checklist

Default credentials are public in documentation and source code. Use them only for demonstrations and local development. Change every applicable default before production or network-exposed deployment:

ScopeExample Defaultconfigure -g
Grafana administrator and viewerpigsty, DBUser.ViewerYes
HAProxy administration interfacepigstyYes
PostgreSQL administration, monitoring, and replication usersDBUser.DBA, DBUser.Monitor, DBUser.ReplicatorYes
Patroni REST APIPatroni.APIYes
etcd rootEtcd.RootYes
MINIO module object-storage rootS3User.MinIOYes
Object-storage backup and example application usersS3User.Backup, S3User.Meta, S3User.DataYes
Example database usersDBUser.Meta, DBUser.Supa, Vibe.CodingYes
pgBackRest encryption passphrasecipher_pass: pgBackRestNo
Silo users and pgBR.${pg_cluster} in ha/safeTemplate example valuesNo
User-defined credentialsCustom valuesNo

Use -g to randomize built-in parameters and example strings recognized by the configuration wizard:

./configure -g     # Generate the inventory and randomize recognized default credentials

The wizard prints generated passwords to the terminal. Protect terminal history and automation logs as sensitive data. Then inspect the configuration. Replace pgBackRest cipher_pass, uncovered MINIO values in ha/safe, and all custom credentials.


Launch Hardening Checklist

Before deployment:

After deployment:

  • Confirm that all default credentials have changed. Include values covered by configure -g and uncovered backup, object storage, and custom credentials.
  • Compare /pg/data/pg_hba.conf with the declared HBA policy and intended access boundary.
  • Query effective users, roles, default privileges, and database CONNECT grants. Compare them with the inventory.
  • Run a full backup and recovery exercise to validate the backup path.
  • Confirm log collection, monitoring alerts, and notification channels.

Periodically:

  • Audit privileges: compare pg_users declarations with effective grants, and remove expired or departed-user accounts
  • Rotate credentials and certificates
  • Exercise recovery and failover
  • Track security updates for Pigsty and upstream components

Compliance Evidence

Use declarative configuration as a starting point for audit evidence. Retain runtime state to prove that the configuration was applied and remains effective.

EvidenceSource
Security baseline and change historyThe pigsty.yml inventory and Git history
Access-control matrixpg_default_roles, pg_users, and pg_hba_rules declarations
Effective authentication policyRendered pg_hba.conf on each instance, compared with declarations to detect drift
Effective users and privilegesPostgreSQL catalogs, database ACLs, \du+, and \ddp+
Operation and connection logsPostgreSQL DDL, slow-query, and connection logs retained in VictoriaLogs
Backup recordspgBackRest information and monitoring dashboards
Security incidents and alertsMonitoring alert history
Certificate inventoryfiles/pki/ and deployed component certificates

MLPS Level 3 Mapping

This table maps database-related capabilities to the secure computing environment controls in GB/T 22239-2019 Level 3:

ControlPigsty CapabilityAdditional Requirement
Unique identityIndependent accounts and SCRAM-SHA-256 password storageReal-name account management process
Password complexity and rotationpasswordcheck, credcheck, and expire_inEnable extensions and establish a rotation process
Login failure handlingCan be implemented with credcheck and related extensionsEnable and configure as required
Access control and least privilegeFour-tier roles, default privileges, and database isolationPrivilege approval workflow
Security auditDDL, connection, and slow-query logs; pgaudit; centralized retentionCRIT or manual connection logging; required retention period
Communication confidentialityLocal CA and TLS; HBA-enforced ssl or certEnforce TLS, client verify-full, and certificate rotation
Data integrityPage checksums by default and strict synchronous replication with CRITStorage protection, defined failure model, and exercises
Data confidentialityAES-encrypted backup plus TDE and column-encryption optionsEnable as required
Backup and recoverypgBackRest, PITR, and a remote S3-compatible repositoryRecovery exercise process
Residual information protection—Media destruction and erasure process

MLPS also covers physical security, networks, and management systems beyond a database distribution. Pigsty supports database-related technical controls. The overall system must address facilities, network devices, and governance.


SOC 2 Mapping

Database-related controls in the SOC 2 Trust Services Criteria (TSC) include:

CriterionPigsty CapabilityAdditional Requirement
CC6.1 Logical access securityHBA, RBAC, default privileges, and database isolationPrivilege design, approval, and periodic review
CC6.2 User registration and authorizationDeclarative users, roles, and expirationJoiner, mover, leaver, and identity-verification process
CC6.3 Access changes and revocationpg_users, role changes, REVOKE, and expirationTickets, approval evidence, and timely revocation
CC6.6 External boundary threatsFirewalls, listen addresses, HBA, and restricted management ingressNetwork architecture, boundary devices, and continuous validation
CC6.7 Information transmission and movementTLS, client verification, and backup encryptionPolicies for exports, media, and third-party transfer
CC7.2 System monitoringVictoria observability stack with extensive metrics and alertsAlert-response process
CC7.3 Incident traceabilityCentralized logs and audit extensionsLog-review process
A1.2 Availability and recoveryHigh Availability and PITRExercise records and RTO/RPO objectives

Supply Chain and Vulnerability Response

Compliance reviews also cover the software supply chain. Pigsty provides these distribution and response controls:

Package integrity: Pigsty repository RPM and DEB packages (repo.pigsty.io, repo.pigsty.cc) are GPG-signed. Verify fingerprint 9592 A7BC 7A68 2E73 3337 6E09 E793 5D8D B9BD 8B20 (B9BD8B20) before trusting the key. Deployment-generated repository definitions and the INFRA node local repository do not enforce verification for every package by default. In production, review package-manager trust and signature settings.

Vulnerability response: Report issues privately through GitHub vulnerability reporting or email as specified in SECURITY.md. The project targets acknowledgment within three business days and initial assessment within seven days.

Version support: Security fixes ship in the latest stable release. Update to receive them. Subscription services provide extended support for users who must retain a version longer.


Next