Skip to content

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

Return to the regular view of this page.

Documentation

Learn how to use pgcli to manage PostgreSQL databases.

Complete guide to managing PostgreSQL databases with pgcli.

  • Quick Start — Install and basic usage
  • Backup — Snapshots and pgBackRest management
  • Restore — Point-in-time recovery (PITR)
  • Replication — Read-only replicas and failover
  • Extensions — Install and manage PostgreSQL extensions

1 - Quick Start

Install pgcli and create your first PostgreSQL instance.

Get pgcli up and running in minutes.

Installation

Install pgcli with a single command:

curl -fsSL https://raw.githubusercontent.com/mars-base/pgcli/main/scripts/install.sh | bash

This script will:

  • Download the latest pgcli binary for your platform
  • Install it to /usr/local/bin (or ~/.local/bin if no sudo)
  • Add pgcli to your PATH

Initialize Configuration

# Initialize config with a default instance
pg config init --add default --base-dir /data/pg

This creates ~/.pgcli/pg.yaml with sensible defaults including:

  • A default instance named default
  • Data directory at /data/pg/default
  • Auto-assigned ports starting from 35432

Start Instance

# Start the default instance
pg start

# Check status and connection info
pg status

The output shows the connection URL, admin password, and backup status.

Connect to Your Database

# Using pgcli's built-in psql wrapper
pg psql

# Or connect directly with the connection string shown in pg status
psql postgres://admin:<password>@localhost:35432/admin_db

# Execute SQL directly
pg exec "SELECT version()"

Basic Operations

# List all instances
pg list

# Stop an instance
pg stop

# Start an instance
pg start

# View instance status
pg status

# Execute SQL
pg exec "SELECT version();"

Multi-Instance

# Create additional instances
pg create -i proj01 --base-dir /data/pg
pg create -i proj02 --base-dir /data/pg

# List all instances
pg list

# Start all instances
pg start --all

Multiple Config Files (Isolation)

For isolated testing environments or one config per project on the same host, generate a separate config file per environment with a distinct --namespace and disjoint port ranges.

# Environment "t1": containers prefixed pgcli-pg-t1-*, PG ports from 38000
pg config init -o ~/.pgcli-t1/pg.yaml --namespace t1 --pg-start-port 38000 --pg-ssh-port 43000 --add proj1

# Environment "t2": different namespace and disjoint ports
pg config init -o ~/.pgcli-t2/pg.yaml --namespace t2 --pg-start-port 38100 --pg-ssh-port 43100 --add proj2

# Manage each environment with -c
pg -c ~/.pgcli-t1/pg.yaml start -i proj1
pg -c ~/.pgcli-t2/pg.yaml list
Parameter Default Meaning
--namespace default Container name prefix: pgcli-pg-<namespace>-<instance>
--pg-start-port 35432 First PG host port; instances get sequential ports
--pg-ssh-port 42201 First SSH host port; sequential from here

Planning tips:

  • Always pass an explicit --namespace to avoid container name clashes.
  • Port ranges must not overlap between configs on one host.
  • The namespace is baked into container names at creation time. Changing it later requires pg destroy and re-init.

Interactive psql Session

# Open interactive psql shell (default instance)
pg psql

# Open psql for specific instance
pg psql -i proj01

# Run SQL from stdin (non-interactive, for scripts)
echo "SELECT version();" | pg psql

# Execute a single SQL command
pg psql -- -c "SHOW work_mem"

# Connect to a different database
pg psql -- -d postgres

# Use psql meta-commands
pg psql -- -c "\dt"     # list tables
pg psql -- -c "\du"     # list users
pg psql -- -c "\l"      # list databases

# Connect to a remote database via connection string
pg psql --dsn postgres://user:pass@host:5432/db

Inside the interactive psql shell you get:

  • SQL queries with history and tab completion
  • psql meta-commands (\dt, \du, \l, etc.)
  • \q to quit

Container Shell

# Open bash shell in container (default instance)
pg shell

# Open shell for specific instance
pg shell -i proj01

# Run a command directly
pg shell -- -c "ls -la /var/lib/postgresql/data"

# View logs
pg shell -- -c "tail -f /var/log/postgresql/postgresql-*.log"

The shell runs as root inside the container, giving full access to:

  • PostgreSQL data directory (/var/lib/postgresql/data)
  • Configuration files
  • Log files (/var/log/postgresql/)
  • All system tools and utilities

Next Steps

2 - Namespace Isolation

Create isolated environments on a single host using namespaces

Namespaces allow you to create completely isolated environments on a single host, perfect for separating production, development, and testing environments without container name conflicts or port collisions.

What is a Namespace?

A namespace is a prefix applied to all container names within a configuration file. This isolation mechanism ensures that:

  • Container names don’t clash: Each namespace gets its own container prefix
  • Port ranges are separate: Each config file allocates ports from its own range
  • Backup containers are isolated: Each namespace has its own pgBackRest container
  • Configuration files are independent: Each environment uses a separate config file

Use Case: Production and Development Environments

A common scenario is running production and development environments on the same server:

# Create production environment
pg config init \
  --namespace prod \
  --pg-start-port 35432 \
  --pg-ssh-port 42201 \
  --add app-db \
  -o ~/.pgcli-prod/pg.yaml

# Create development environment
pg config init \
  --namespace dev \
  --pg-start-port 38000 \
  --pg-ssh-port 43000 \
  --add app-db \
  -o ~/.pgcli-dev/pg.yaml

This creates two completely isolated environments:

Environment Config File Container Prefix PG Port Range SSH Port Range
Production ~/.pgcli-prod/pg.yaml pgcli-pg-prod-* 35432+ 42201+
Development ~/.pgcli-dev/pg.yaml pgcli-pg-dev-* 38000+ 43000+

Managing Multiple Environments

Use the -c flag to specify which configuration file to use:

# Start production database
pg -c ~/.pgcli-prod/pg.yaml start -i app-db

# Start development database
pg -c ~/.pgcli-dev/pg.yaml start -i app-db

# List instances in production
pg -c ~/.pgcli-prod/pg.yaml list

# List instances in development
pg -c ~/.pgcli-dev/pg.yaml list

How It Works

Container Naming

With namespace prod and instance app-db:

  • Instance container: pgcli-pg-prod-app-db
  • Backup container: pgcli-backup-prod
  • Network: pgcli-net-prod (if using separate networks)

Without namespace (or --namespace ""):

  • Instance container: pgcli-pg-default-app-db
  • Backup container: pgcli-backup-default

Port Allocation

Each instance in a config file gets sequential ports:

  • First instance: pg_start_port (e.g., 35432)
  • Second instance: pg_start_port + 1 (e.g., 35433)
  • And so on…

Same for SSH ports used by pgBackRest.

Configuration Persistence

The namespace and port ranges are saved in the config file:

namespace: prod
pg_start_port: 35432
pg_ssh_port: 42201

Best Practices

1. Always Use Explicit Namespaces

Never rely on the default namespace when running multiple configs on one host:

# Bad: both configs would use "default" namespace and clash
pg config init --add app -o ~/.pgcli-prod/pg.yaml
pg config init --add app -o ~/.pgcli-dev/pg.yaml  # Conflict!

# Good: explicit namespaces
pg config init --namespace prod --add app -o ~/.pgcli-prod/pg.yaml
pg config init --namespace dev --add app -o ~/.pgcli-dev/pg.yaml

2. Use Disjoint Port Ranges

Ensure port ranges don’t overlap between configs:

# Production: 35432-35999, 42201-42999
pg config init --namespace prod \
  --pg-start-port 35432 \
  --pg-ssh-port 42201 \
  --add app -o ~/.pgcli-prod/pg.yaml

# Development: 38000-38999, 43000-43999
pg config init --namespace dev \
  --pg-start-port 38000 \
  --pg-ssh-port 43000 \
  --add app -o ~/.pgcli-dev/pg.yaml

# Testing: 40000-40999, 44000-44999
pg config init --namespace test \
  --pg-start-port 40000 \
  --pg-ssh-port 44000 \
  --add app -o ~/.pgcli-test/pg.yaml

Leave headroom for multiple instances within each environment.

3. Namespace is Baked into Container Names

The namespace is embedded in container names at creation time. Changing it later breaks the link:

# Create with namespace "prod"
pg -c ~/.pgcli-prod/pg.yaml start -i app-db
# Container: pgcli-pg-prod-app-db

# Edit config to change namespace to "production"
# This WON'T work - container name mismatch!

# Instead: destroy and recreate
pg -c ~/.pgcli-prod/pg.yaml destroy -i app-db --clean-data
pg -c ~/.pgcli-prod/pg.yaml create -i app-db
pg -c ~/.pgcli-prod/pg.yaml start -i app-db

4. Use Shell Aliases for Convenience

Create aliases to avoid typing -c repeatedly:

# Add to ~/.bashrc or ~/.zshrc
alias pg-prod='pg -c ~/.pgcli-prod/pg.yaml'
alias pg-dev='pg -c ~/.pgcli-dev/pg.yaml'
alias pg-test='pg -c ~/.pgcli-test/pg.yaml'

# Now use:
pg-prod start -i app-db
pg-dev list
pg-test destroy -i test-db --force

Advanced: Multiple Environments with Replicas

You can even set up isolated replication environments:

# Production: primary + replica
pg -c ~/.pgcli-prod/pg.yaml create -i primary --base-dir /data/prod
pg -c ~/.pgcli-prod/pg.yaml replica create replica -i primary

# Development: separate primary + replica
pg -c ~/.pgcli-dev/pg.yaml create -i primary --base-dir /data/dev
pg -c ~/.pgcli-dev/pg.yaml replica create replica -i primary

Each environment maintains its own replication slots, backup stanzas, and data directories.

Troubleshooting

Container Name Conflicts

Error: container "pgcli-pg-default-app-db" already exists

Cause: Two configs using the same namespace.

Solution: Use different namespaces or destroy the conflicting instance first.

Port Already in Use

Error: listen tcp 0.0.0.0:35432: bind: address already in use

Cause: Port ranges overlap between configs.

Solution: Use disjoint port ranges with sufficient spacing.

Instance Not Found After Namespace Change

Error: instance "app-db" exists in config but container not found

Cause: Changed namespace in config file after creating instances.

Solution: Either destroy and recreate instances, or revert the namespace change.

Summary

Namespaces provide complete isolation for multiple environments on a single host:

  • Separate configs: Each environment gets its own pg.yaml
  • Distinct namespaces: Prevents container name collisions
  • Disjoint ports: Avoids port conflicts
  • Independent operation: Each environment managed separately with -c

Perfect for running production, development, testing, and staging environments on the same server without interference.

3 - Backup

Backup guide for pgcli

Snapshots

# Create snapshot (full backup)
pg snapshot create -i proj01

# Create differential backup (recommended)
pg snapshot create --type diff -i proj01

# Stream backup container logs during snapshot
pg snapshot create --tail-logs -i proj01

# List snapshots
pg snapshot list -i proj01

# Limit the number of snapshots displayed
pg snapshot list --limit 5 -i proj01

# Delete snapshot
pg snapshot delete 20260826-073712F -i proj01

Snapshot types:

  • full — Complete backup (default, self-contained)
  • diff — Changes since last full backup
  • incr — Changes since last backup

Shared Backup Container

All instances share a single pgbackrest container; each instance gets its own stanza in the repository.

# Initialize the shared pgbackrest container (build image, create dirs, generate config)
pg backup setup

# Use a custom base directory for backup data and logs
pg backup setup --base-dir /mnt/backup

# Start / stop the backup container
pg backup start
pg backup stop

# Show backup container status
pg backup status

Backup infrastructure (network, image, directories, config, container) is prepared automatically on pg start; run pg backup setup manually to reinitialize, e.g. after changing the base directory.

4 - Restore

Point-in-Time Recovery (PITR) guide for pgcli

Point-in-Time Recovery (PITR)

Restore to any point in time after the first backup.

# Restore (read-only, inspect before committing)
pg restore --time "2026-08-26 15:30:00+00"

# Preview what would be restored without executing (dry run)
pg restore --time "2026-08-26 15:30:00+00" --dry-run

# Stream restore container logs during recovery
pg restore --time "2026-08-26 15:30:00+00" --tail-logs

# Try different time if needed
pg restore --time "2026-08-26 15:25:00+00"

# Promote to read-write (switches timeline)
pg restore --time "2026-08-26 15:30:00+00" --promote

# Skip confirmation
pg restore --time "2026-08-26 15:30:00+00" --promote --force

Time formats:

  • 2026-08-26 15:30:00+08:00 — with timezone offset
  • 2026-08-26 15:30:00+08 — timezone hour only
  • 2026-08-26 15:30:00Z — UTC
  • 2026-08-26 15:30:00 — assumed UTC

Recovery workflow: Stop → Restore → Start → WAL replay to target time

Note: After --promote, create a new full snapshot before further PITR.

5 - Replica (read-only standby)

Replica (read-only standby) guide for pgcli

Create a read-only physical replica of an existing instance. The replica continuously streams WAL from its primary via PostgreSQL physical replication and serves read-only queries — useful for read/write splitting, reporting, or as a warm standby.

# Create a replica of the default instance
pg replica create ro1

# Create a replica of a specific instance
pg replica create ro1 -i proj01

# List replicas and replication lag
pg replica list

What happens

  1. Pre-flight check — the primary must be running (verified before any config is written; a stopped primary fails with no side effects)
  2. Register — a new instance entry is added with the primary’s database name and password (see Notes), PITR disabled, and replica_of set to the primary
  3. Replication setup — on the primary:
    • pg_hba.conf gains host replication entries for loopback and RFC1918 ranges (idempotent)
    • a physical replication slot pgcli_r_<name> is created, reserving WAL so the replica can never fall behind WAL recycling
  4. Base backuppg_basebackup -R copies the primary’s data directory into the replica’s data dir, writing primary_conninfo (with password) and standby.signal so the replica boots in standby mode
  5. Start — the replica container starts and streams WAL continuously

Verify

# Read-only standby?
pg exec -i ro1 "SELECT pg_is_in_recovery()"      # t

# Writes are rejected
pg exec -i ro1 "INSERT INTO t VALUES (1)"         # read-only transaction error

# Streaming is live
pg exec -i ro1 "SELECT pg_is_in_recovery(), now() - pg_last_xact_replay_timestamp()"

# Slot is active on the primary
pg exec -i primary "SELECT slot_name, active FROM pg_replication_slots"

# Overview
pg list                              # ROLE/PRIMARY columns
pg replica list                      # NAME/PRIMARY/STATUS/LAG
pg status -i ro1                     # Role: standby (replica of ...)

Destroy

Destroying a replica is a 2-step process:

# Step 1: Destroy the replica instance (removes container + data + config entry)
pg destroy -i ro1 --clean-data --force

# Step 2: Drop the replication slot on the primary (no-op if already gone)
pg replica drop ro1 -i <primary>

Step 1 must run before step 2: PostgreSQL refuses to drop a slot that is still being streamed (replication slot is active), so the replica must be destroyed first to close its streaming connection.

Note: If the primary is a local pgcli-managed instance (same host), Step 2 can be skipped — destroy automatically cleans up the slot. Step 2 is required when the primary is not accessible from the replica host.

Cross-network replicas

The same-host flow above assumes primary and replica share one server (one podman daemon, one network). For a replica on another host, pgcli runs one command on each side — no SSH, the only cross-machine information is what you pass as parameters:

# ---- on the PRIMARY host: prepare the primary (run first) ----
pg replica create ro1 -i pg01 --replica-host 10.241.20.100

# ---- on the REPLICA host: copy data and start the replica (run second) ----
pg replica create ro1 --primary-dsn "postgres://admin:<password>@10.241.20.50:35432/pg01_db" --primary-name pg01

Getting the primary DSN

If the primary is a pgcli-managed instance, get its connection info with pg status:

pg status -i pg01
# ...
#   Connection: postgres://admin:fbcQx9uIzvTO6dVJ@127.0.0.1:35432/pg01_db

Then replace 127.0.0.1 with the primary host’s IP as seen from the replica host (e.g. 10.241.20.50) — the user, password and database are used as-is. Note the primary host must accept TCP connections on that port from the replica host (firewall/security group).

What each side does:

  • Primary side (--replica-host <ip|hostname>): only prepares the primary — nothing is created locally. It appends a host replication all <addr> entry to pg_hba.conf and creates the physical slot pgcli_r_<name>, then prints the exact --primary-dsn command to run on the replica host. IPs get a /32 (/128 for IPv6) mask; hostnames are written as-is. An IP already inside a managed RFC1918 range (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) is skipped as redundant. Idempotent — re-running adds no duplicate lines.
  • Replica side (--primary-dsn): first verifies the slot exists on the primary (validates connectivity and ordering — running before the primary side fails with an actionable message and no side effects), then registers the instance, runs pg_basebackup from the DSN over the network (host networking), and starts the standby.

The replica side runs only on the replica host: user, database and password of the replica instance come from the DSN (physical replication copies pg_authid, so the local password must equal the primary’s). The primary name is given with --primary-name and recorded as replica_of; it does not have to exist in the replica host’s config, and -i is not used for the remote primary — if given, it keeps its strict meaning and must reference a real local instance.

Destroy is symmetric, one command per host — in this order:

# 1. on the REPLICA host: removes container + config, also drops the slot
#    on the remote primary via DSN (automatic if primary is reachable)
pg destroy -i ro1

# 2. on the PRIMARY host (only if step 1's DSN connection failed):
#    drop the slot manually
pg replica drop ro1 -i pg01

Step 1 must run before step 2: PostgreSQL refuses to drop a slot that is still being streamed (replication slot is active), so the replica must be destroyed first to close its streaming connection.

Automatic slot cleanup: When a cross-host replica has PrimaryDSN set, destroy automatically attempts to drop the replication slot on the remote primary via DSN. If the primary is reachable, no manual step 2 is needed. replica drop is idempotent — re-running when the slot is already gone succeeds as a no-op.

Non-pgcli primary

The primary does not have to be managed by pgcli — the replica side works against any PostgreSQL server, as long as the primary side has been prepared manually (the slot check only verifies the slot exists, not who created it):

  1. Allow replication from the replica host in pg_hba.conf, then reload (SELECT pg_reload_conf()):
    host replication <replica user> <replica ip>/32 scram-sha-256
  2. Create the physical slot with the exact name pgcli_r_<replica-name> (the replica side checks this name):
    SELECT pg_create_physical_replication_slot('pgcli_r_ro1');
    Requires wal_level = replica (or logical) and a user with REPLICATION privilege — the DSN user.

Then the replica-side command is unchanged:

pg replica create ro1 --primary-dsn "postgres://<user>:<pass>@<primary ip>:5432/<db>" --primary-name pg01

On destroy, there is no pgcli on the primary side — after pg destroy -i ro1, drop the slot manually:

SELECT pg_drop_replication_slot('pgcli_r_ro1');

If a base backup fails (e.g. network hiccup), destroy the replica and re-run the replica-side command — the slot and hba entry on the primary side remain valid.

Notes

  • Read-only — the replica rejects all writes (cannot execute INSERT in a read-only transaction). To make it writable you would promote it (pg_ctl promote), which is not exposed as a pgcli command yet
  • Same data, same password — physical replication is a byte-for-byte copy of the primary, including pg_authid. The replica’s admin password and database name are therefore identical to the primary’s; only container name, port and data directory differ. With --dsn-style connections use the replica’s port
  • PITR disabled on replicas — a standby archives nothing and is not registered with the pgBackRest backup container; backups run on the primary
  • Primary must be running — both for initial creation (pg_basebackup) and for continuous streaming; if the primary restarts, the replica reconnects automatically (slot shows active)
  • Lag displayreplica list lag (now() - pg_last_xact_replay_timestamp()) grows while the primary is idle; it drops back to zero on the next replicated transaction. This is expected idle behavior, not drift
  • Idempotent start — repeated pg start -i ro1 skips the base backup when the data directory is already initialized

6 - Failover: Replica Promotion

Failover: Replica Promotion guide for pgcli

Promote a replica to become the new primary when the current primary fails. pgcli provides a 3-step manual failover workflow — each step runs on its respective host, no auto-detection of same-host vs cross-host topology.

Overview

                    ┌─────────────┐
                    │   primary   │  ← crashes / becomes unavailable
                    │  (pg01)     │
                    └──────┬──────┘
                           │ WAL streaming
               ┌───────────┼───────────┐
               ▼           ▼           ▼
          ┌─────────┐ ┌─────────┐ ┌─────────┐
          │  ro1    │ │  ro2    │ │  ro3    │
          │ replica │ │ replica │ │ replica │
          └─────────┘ └─────────┘ └─────────┘

After failover (promote ro1 → new primary):

                    ┌─────────────┐
                    │   ro1       │  ← new primary (promoted)
                    │  (primary)  │
                    └──────┬──────┘
                           │ WAL streaming
               ┌───────────┼───────────┐
               ▼           ▼           ▼
          ┌─────────┐ ┌─────────┐ ┌─────────┐
          │  ro2    │ │  ro3    │ │  pg01   │
          │ replica │ │ replica │ │ replica │  ← demoted old primary
          └─────────┘ └─────────┘ └─────────┘

3-Step Failover

Each step is an independent command. Run them in order, on their respective hosts.

# Step 1: On the replica being promoted
pg replica promote ro1

# Step 2: On the old primary host (when it recovers)
pg replica drop ro1 -i pg01

# Step 3: On each remaining replica host
pg replica repoint ro2 --primary-dsn "postgres://admin:<pw>@<new-primary-ip>:<port>/<db>" --primary-name ro1
pg replica repoint ro3 --primary-dsn "postgres://admin:<pw>@<new-primary-ip>:<port>/<db>" --primary-name ro1

Step 1: pg replica promote <name>

Run on the host of the replica being promoted to primary.

pg replica promote ro1

What happens:

  1. Validates the instance is a replica (ReplicaOf is set) and the container is running
  2. Calls pg_promote() (PostgreSQL 12+ native promotion — no container restart)
  3. Waits for recovery to end (usually sub-second)
  4. Cleans up primary_conninfo from postgresql.auto.conf via ALTER SYSTEM RESET
  5. Updates config: clears ReplicaOf and PrimaryDSN, enables PITR
  6. Automatically initializes PITR:
    • pgBackRest stanza creation
    • archive_mode / archive_command configuration
    • PostgreSQL restart to apply postmaster-level parameters
  7. Prints next-step instructions

Idempotent: If the replica is already promoted (e.g. from a manual pg_ctl promote), the command skips to config update.

Step 2: pg replica drop <name> -i <old-primary>

Run on the old primary host to clean up the replication slot. This step is only needed in specific scenarios.

pg replica drop ro1 -i pg01

This drops the physical replication slot pgcli_r_ro1 on the old primary. Without cleanup, the slot would hold WAL indefinitely until the primary runs out of disk space.

When to run:

Scenario Action Why
Old primary is permanently lost Skip Slot is gone with the server
Plan to demote old primary to replica Skip repoint destroys the data directory (including pg_replslot/), all slots are implicitly removed
Old primary recovered, keep running as independent primary Must run Slot holds WAL indefinitely; without cleanup the disk will eventually fill up
Old primary recovered but will be shut down Optional No harm in skipping if the instance will not run again

Keep the old primary as-is? If you want to preserve the old primary with its original data (e.g. for forensic analysis or as a read-only archive), you can simply leave it alone — do not run drop or repoint on it. The old primary keeps running as an independent instance with stale data. Just be aware that the replication slot for the promoted replica still exists and will accumulate WAL; you may want to drop just that specific slot (pg replica drop ro1 -i pg01) while leaving everything else untouched.

Step 3: pg replica repoint <name> --primary-dsn <dsn> --primary-name <name>

Run on each remaining replica host to re-point it to the new primary.

pg replica repoint ro2 \
  --primary-dsn "postgres://admin:fbcQx9uIzvTO6dVJ@10.241.21.97:35439/pg01_db" \
  --primary-name ro1

What happens:

  1. Queries the new primary’s extensions via DSN (pg_extension catalog)
  2. If non-builtin extensions exist (e.g. pg_cron, timescaledb), builds a local -ext image with matching packages
  3. Stops the old replica container and destroys its data directory
  4. Creates a replication slot on the new primary via DSN
  5. Updates config: ReplicaOf, PrimaryDSN, ImageTag, Extensions, disables PITR
  6. Re-initializes via pg_basebackup -R from the new primary
  7. Starts the replica container in standby mode

Why destroy + rebuild instead of ALTER SYSTEM SET?

After promotion, the new primary advances to a new timeline. Other replicas on the old timeline cannot simply change primary_conninfo — PostgreSQL rejects the connection with:

FATAL: requested starting point on timeline 1 is not in this server's history

The only safe approach is a full pg_basebackup from the new primary.

Getting the Primary DSN

Get the new primary’s connection string from pg status on the promoted replica’s host:

pg status -i ro1
# Connection: postgres://admin:fbcQx9uIzvTO6dVJ@127.0.0.1:35439/pg01_db

Replace 127.0.0.1 with the new primary host’s IP reachable from the replica host (e.g. 10.241.21.97).

Demoting the Old Primary

When the old primary recovers, you can rejoin it as a replica of the new primary using the same repoint command:

# On the old primary host
pg replica repoint pg01 \
  --primary-dsn "postgres://admin:fbcQx9uIzvTO6dVJ@10.241.21.97:35439/pg01_db" \
  --primary-name ro1

This works even though pg01 was a primary (no ReplicaOf set). The command:

  1. Stops pg01 and destroys its data (including old PITR stanza)
  2. Creates a replication slot for pg01 on the new primary
  3. Sets ReplicaOf = "ro1", PITR.Enabled = false
  4. Re-initializes from the new primary via pg_basebackup

After repoint, pg01 streams WAL from the new primary as a read-only replica — no WAL archiving, no backups.

Extension Sync

When a replica is repointed to a new primary, pgcli automatically synchronizes extensions:

  1. Query — Connects to the new primary via DSN and queries pg_extension for installed extensions
  2. Filter — Identifies non-builtin extensions (those requiring external packages, e.g. pg_cron, pgmq, timescaledb)
  3. Build — If non-builtin extensions exist, builds a local -ext image:
    • If a local -ext image already exists, installs missing packages on top (reuses Pigsty repo — fast)
    • If no -ext image exists, builds from the base image with Pigsty repo setup
    • apt-get install is idempotent — installing an already-present package is a no-op
  4. Apply — On replica start, ApplyExtensions writes shared_preload_libraries to postgresql.conf
  5. Skip CREATE EXTENSION — Replicas are read-only; extensions are replicated from the primary via pg_basebackup + WAL streaming

This ensures the replica container has the required shared libraries (e.g. pg_cron) that are referenced in postgresql.auto.conf.

Same-Host vs Cross-Host

pgcli does not auto-detect topology. You choose where to run each command:

Scenario Step 1 Step 2 Step 3
All on one host pg replica promote ro1 pg replica drop ro1 -i pg01 pg replica repoint ro2 --primary-dsn "postgres://...@127.0.0.1:..." --primary-name ro1
Primary + replicas split across hosts On replica host On old primary host On each replica host with the new primary’s network IP
Mixed On the respective host On old primary host On each replica host

The --primary-dsn must use an IP/hostname reachable from the host where repoint runs.

Complete Example

# ── Initial setup: pg01 (primary) + ro1, ro2 (replicas) on same host ──

$ pg list
NAME    ROLE      PRIMARY   STATUS
pg01    primary   -         Up 2 hours
ro1     replica   pg01      Up 1 hour
ro2     replica   pg01      Up 1 hour

# ── pg01 crashes ──

# Step 1: Promote ro1
$ pg replica promote ro1
  [OK] pg_promote() signaled
  [OK] recovery ended, instance is now read-write
  [OK] primary_conninfo removed from postgresql.auto.conf
✓ Replica "ro1" promoted to primary

$ pg start -i ro1           # enable PITR + WAL archiving

# Step 2: Clean up on old primary (skip if pg01 is permanently lost)
$ pg replica drop ro1 -i pg01
  [OK] replication slot "pgcli_r_ro1" removed from primary "pg01"

# Step 3: Re-point ro2 to new primary
$ pg replica repoint ro2 \
    --primary-dsn "postgres://admin:fbcQx9uIzvTO6dVJ@127.0.0.1:35437/pg01_db" \
    --primary-name ro1
  [OK] extension image built with pg_cron, pgmq, timescaledb
  [OK] replication slot "pgcli_r_ro2" created on new primary
  [OK] config updated (ReplicaOf = "ro1")
✓ Replica "ro2" re-pointed to "ro1"

# ── pg01 recovers, demote to replica ──
$ pg replica repoint pg01 \
    --primary-dsn "postgres://admin:fbcQx9uIzvTO6dVJ@127.0.0.1:35437/pg01_db" \
    --primary-name ro1
  [OK] backup stanza removed: pgcli_pg01
  [OK] replication slot "pgcli_r_pg01" created on new primary
  [OK] config updated (ReplicaOf = "ro1", PITR disabled)
✓ Replica "pg01" re-pointed to "ro1"

# ── Final state ──
$ pg list
NAME    ROLE      PRIMARY   STATUS
pg01    replica   ro1       Up 30 seconds    # demoted
ro1     primary   -         Up 10 minutes    # new primary
ro2     replica   ro1       Up 5 minutes

Cross-Host Example

# ── Setup: ra3 (primary, host A) + ra2 (replica, host A) + ro2 (replica, host B) ──

# ra3 crashes. On host A, promote ra2:
$ pg replica promote ra2
  [OK] pg_promote() signaled
  [OK] recovery ended
  [OK] PITR initialized (stanza + archive_mode)
✓ Replica "ra2" promoted to primary

# On host B (10.241.20.147), re-point ro2 to new primary ra2 (host A = 10.241.21.97):
$ pg replica repoint ro2 \
    --primary-dsn "postgres://admin:fbcQx9uIzvTO6dVJ@10.241.21.97:35438/pg01_db" \
    --primary-name ra2
-> New primary has 3 non-builtin extension(s): pg_cron, pgmq, timescaledb
-> Extension image already has all required packages
  [OK] replication slot "pgcli_r_ro2" created on new primary
  [OK] config updated (ReplicaOf = "ra2", image = ...-ext)
✓ Replica "ro2" re-pointed to "ra2"

# Verify cross-host replication:
$ pg exec -i ra2 "INSERT INTO test(msg) VALUES ('after failover')"
$ pg exec -i ro2 "SELECT * FROM test ORDER BY id DESC LIMIT 1"
   msg: after failover

Cascading Replication

A replica can itself serve as a primary for downstream replicas, forming a cascading chain. This reduces load on the primary and enables hierarchical topologies.

primary (ra3)
  ├─→ replica (ra2) ← upstream for ra2_ro1
  │     └─→ replica (ra2_ro1)
  └─→ replica (pg01)

How It Works

  1. Create a replica of a replica: Use the replica as the -i target

    # ra2 is a replica of ra3, create ra2_ro1 as a replica of ra2
    pg replica create ra2_ro1 -i ra2
  2. WAL propagation:

    • ra2 streams WAL from ra3
    • ra2_ro1 streams WAL from ra2
    • Data flows: ra3 → ra2 → ra2_ro1
  3. Replication slots: Each link maintains its own slot

    • ra3 has slot pgcli_r_ra2
    • ra2 has slot pgcli_r_ra2_ro1

Benefits

  • Reduced primary load: Only direct replicas connect to primary
  • Geographic distribution: Primary → regional replica → local replicas
  • Network efficiency: Local replicas can share a regional upstream

Limitations

  • Increased latency: Each hop adds replication delay
  • Cascading failures: If ra2 fails, ra2_ro1 loses its upstream
  • Promotion complexity: Promoting ra2_ro1 requires repointing it to a new primary

Verify Cascading

# Check ra2's downstream replicas
pg exec -i ra2 "SELECT client_addr, state FROM pg_stat_replication"

# Check ra2_ro1's upstream
pg exec -i ra2_ro1 "SELECT conninfo FROM pg_stat_wal_receiver"

Failover with Cascading

If ra2 (middle node) fails:

# Option 1: Re-point ra2_ro1 to ra3 directly
pg replica repoint ra2_ro1 \
  --primary-dsn "postgres://admin:password@ra3-host:5432/pg01_db" \
  --primary-name ra3

# Option 2: Wait for ra2 to recover (automatic once ra2 reconnects to ra3)

If ra3 (primary) fails and ra2 is promoted:

# Step 1: Promote ra2
pg replica promote ra2

# Step 2: ra2_ro1 automatically follows (it's already replicating from ra2)
# No action needed for ra2_ro1

# Step 3: Re-point other replicas to the new primary
pg replica repoint pg01 \
  --primary-dsn "postgres://admin:password@ra2-host:5432/pg01_db" \
  --primary-name ra2

Notes

  • pg_promote() — PostgreSQL 12+ native function, no container restart required. The instance exits recovery in-place and becomes read-write immediately
  • Timeline divergence — After promotion, the new primary is on a new timeline. Other replicas cannot be re-pointed with ALTER SYSTEM SET primary_conninfo — they must be rebuilt via pg_basebackup
  • PITR on promoted replica — After promotion, run pg start to create the pgBackRest stanza and enable WAL archiving. The promoted replica has no prior backup history
  • Replication slots — The old primary’s slot for the promoted replica becomes stale after promotion. pg replica drop cleans it up. If the old primary is demoted to a replica, repoint destroys the old data and the stale slot is no longer referenced
  • Extensions — Replica containers inherit shared_preload_libraries from the primary via postgresql.auto.conf. The repoint command ensures the local image has the required extension packages before rebuilding the replica
  • CREATE EXTENSION skipped — Replicas are read-only; pg_basebackup copies the extension metadata from the primary, so CREATE EXTENSION is not needed (and would fail with “cannot execute CREATE EXTENSION in a read-only transaction”)

7 - PostgreSQL Extensions

PostgreSQL Extensions guide for pgcli

pgcli supports installing and managing PostgreSQL extensions from the Pigsty DEB repository.

How It Works

Extensions are baked into a derived container image:

  1. pg extension install builds a new image (based on the current image + Pigsty repo + extension packages)
  2. Stops and removes the old container
  3. Recreates the container from the new image (host data volumes are preserved)
  4. Updates image_tag in the config file

Benefits of this approach:

  • Extensions survive container rebuilds (baked into the image layer)
  • pg start does not need to run apt-get install on every boot
  • Extension files are persistent and decoupled from the container lifecycle

Commands

Install Extensions

# Install a single extension
pg extension install pg_stat_statements

# Install multiple extensions (single image build)
pg extension install pgmq uuid-ossp pg_stat_statements

# Target a specific instance
pg extension install pg_stat_statements -i pg01

Restart confirmation: Extensions requiring shared_preload_libraries (e.g., pg_stat_statements, pg_cron) need a PostgreSQL restart. By default, you will be prompted for confirmation:

# Interactive confirmation (default)
pg extension install pg_stat_statements
# Output:
# Installing extensions that require shared_preload_libraries will cause a PostgreSQL restart.
# Extensions to be installed: [pg_stat_statements]
# This will cause a brief interruption to database connections.
# Restart PostgreSQL now? [y/N]:

# Skip confirmation and restart automatically
pg extension install pg_stat_statements --auto-restart

If you decline the restart, you can apply the changes later:

pg stop -i pg01
pg start -i pg01

List Installed Extensions

pg extension list -i pg01

Example output:

Installed extensions in "pg01":
  pg_stat_statements (managed)
  uuid-ossp (managed)
  plpgsql (unmanaged)
  • managed: tracked by pgcli (recorded in config, included in image)
  • unmanaged: manually installed extensions (not tracked in config)

Remove Extensions

pg extension remove pgmq -i pg01

Workflow:

  1. DROP EXTENSION IF EXISTS pgmq
  2. Update config and shared_preload_libraries
  3. No image rebuild — the -ext image is shared across instances and packages are never uninstalled

Restart confirmation: If removing extensions that require shared_preload_libraries (e.g., pg_stat_statements, pg_cron), you will be prompted for confirmation before restarting:

# Interactive confirmation (default)
pg extension remove pg_stat_statements -i pg01
# Output:
# Removing extensions that require shared_preload_libraries will cause a PostgreSQL restart.
# Extensions to be removed: [pg_stat_statements]
# This will cause a brief interruption to database connections.
# Restart PostgreSQL now? [y/N]:

# Skip confirmation and restart automatically
pg extension remove pg_stat_statements -i pg01 --auto-restart

If you decline the restart, you can apply the changes later:

pg stop -i pg01
pg start -i pg01

View Available Extensions

pg extension available

Lists all 440 known extensions:

  • 45 builtin (contrib, already in the base image — no image build needed)
  • 395 Pigsty catalog (from Pigsty DEB repo, requires image build)

Built-in Extension Catalog

Requires shared_preload_libraries (restart on install)

Extension Description
pg_stat_statements SQL performance analysis
pg_cron Scheduled job execution
pg_hint_plan Query hints
pg_stat_monitor Advanced performance monitoring
pg_qualstats Query predicate statistics
pg_stat_kcache Kernel-level performance stats
pg_wait_sampling Wait event sampling
pg_track_settings Configuration change tracking
timescaledb Time-series database extension

No shared_preload_libraries (no restart needed)

Extension Description
uuid-ossp UUID generation functions
pgmq Lightweight message queue
hstore Key-value pair storage
pgcrypto Cryptographic functions
tablefunc Crosstab functions
btree_gist B-tree GiST index support
btree_gin B-tree GIN index support
pg_trgm Trigram similarity matching
unaccent Accent removal functions
fuzzystrmatch Fuzzy string matching
intarray Integer array operations
isn ISBN/ISSN/EAN standard number types
pg_repack Online table reorganization
pg_squeeze Table space reclamation
pg_partman Partition management
pgvector Vector similarity search
postgis Geospatial data support

Extensions Outside the Catalog

Only extensions in the catalog (builtin + Pigsty) can be installed via pg extension install. Unknown extension names are rejected before the build starts:

  [X] Unknown extension(s): [nonexistent_ext]

      These extensions are not in the Pigsty catalog or builtin contrib list.
      Check available extensions: pg extension available
      Full Pigsty catalog: https://pigsty.cc/ext/list/

Full catalog: https://pigsty.cc/ext/list/

Configuration

After installing extensions, the config is updated:

instances:
  pg01:
    extensions:
      - pg_stat_statements
      - uuid-ossp
      - pgmq
    podman:
      image_tag: ghcr.io/mars-base/pgcli/pgcli-pg:18-2.58.0-ext

The image_tag points to the derived image containing all installed extensions.

Shared Preload Libraries

Extensions requiring shared_preload_libraries are automatically configured in postgresql.conf:

# === pgcli extensions (managed — do not edit) ===
shared_preload_libraries = 'pg_stat_statements,pg_cron'
# === end pgcli extensions ===

This is a postmaster-level parameter; PostgreSQL must be restarted after changes.

Troubleshooting

Extension Install Failure

  [X] Unknown extension(s): [nonexistent_ext]

Cause: Extension name is not in the builtin contrib list or Pigsty catalog.

Resolution:

  • Verify the extension name: pg extension available
  • Check the Pigsty catalog: https://pigsty.cc/ext/list/
  • Note the exact SQL extension name (e.g., vector not pgvector)

CREATE EXTENSION Failure

ERROR: extension "pgmq" already exists

The extension is installed but not tracked in config. You can safely ignore this, or manually add it to the config:

extensions:
  - pgmq

Shared Preload Library Conflict

If shared_preload_libraries was manually edited in postgresql.conf, pgcli’s sentinel block will overwrite it.

Resolution: Remove the manual configuration and let pgcli manage it.

Notes

  • Extension count: 45 builtin (contrib) + 395 Pigsty catalog = 440 total known extensions
  • Image size: Each extension adds 10-50MB to the image, but Pigsty packages are optimized
  • Build time: First extension install takes 1-3 minutes (download + build); subsequent installs are faster (cache hits)
  • Replica behavior: Replicas can install extensions, but CREATE EXTENSION will be rejected (read-only). Install on the primary; replicas sync via physical replication
  • Extension upgrades: ALTER EXTENSION ... UPDATE TO ... is not yet supported; run manually via pg exec

8 - Clone

Clone guide for pgcli

Create a new instance whose data is copied from an existing one, streamed directly — no temp file on disk.

# Clone the default instance
pg clone test02

# Clone a specific instance
pg clone test02 -i proj01

# Clone a remote database via connection string
pg clone test02 --dsn postgres://user:pass@host:5432/db

# Custom data directory for the new instance
pg clone test02 -i proj01 --base-dir /data/pg

What happens

  1. Pre-flight check — the source is verified before anything is created:
    • Local instance: container must be running
    • --dsn: an authenticated SELECT 1 must succeed (catches wrong password, unreachable host)
  2. Create — a new instance entry is added to the config with a random password, its own container name, data directory and auto-assigned port
  3. Start — the new instance is started (same workflow as pg start)
  4. Stream — source data is piped to the target with live transfer progress shown once per second

Notes

  • The source instance must be running (or the --dsn target reachable); a bad source fails immediately with no side effects
  • The new instance name must not already exist in config
  • --dsn and --instance are mutually exclusive: with --dsn the connection string determines host, port and database
  • The new instance gets a fresh random password — find it in the clone output or pg status -i <name>
  • Logical copy only (schema + data); for large databases a physical approach may be faster

9 - Data Import/Export

Data Import/Export guide for pgcli

Export and import databases to dump files. Supports custom format (recommended) and plain SQL, with automatic gzip compression. Also supports piping between instances.

# Export to custom format (recommended, fastest restore)
pg export -i proj01 -o backup.dump

# Export to SQL format (human-readable)
pg export -i proj01 -o backup.sql

# Export with gzip compression (auto-detected from .gz extension)
pg export -i proj01 -o backup.dump.gz
pg export -i proj01 -o backup.sql.gz

# Export specific database
pg export -i proj01 -d mydb -o backup.dump

# Export with custom compression level (0-9)
pg export -i proj01 -o backup.sql.gz --compress=9

# Export with verbose output (show progress)
pg export -i proj01 -o backup.dump -v

# Import from custom format
pg import -i proj02 backup.dump

# Import from SQL format
pg import -i proj02 backup.sql

# Import compressed file (auto-detected from .gz extension)
pg import -i proj02 backup.dump.gz

# Import to specific database
pg import -i proj02 -d mydb backup.dump

# Import with cleanup (drop existing objects before restore)
pg import -i proj02 --clean backup.dump

# Import with verbose output
pg import -i proj02 backup.dump -v

# Pipe between instances (no temp file)
pg export -i proj01 | pg import -i proj02
pg export -i proj01 -d mydb | pg import -i proj02 -d mydb --clean

# Pipe across hosts via SSH
pg export -i proj01 | ssh user@remote "pg import -i proj02"
ssh user@remote "pg export -i proj01" | pg import -i proj02
ssh user@host1 "pg export -i proj01" | ssh user@host2 "pg import -i proj02"

# Work with remote databases via connection string (--dsn)
pg export --dsn postgres://user:pass@host:5432/mydb -o backup.dump
pg import --dsn postgres://user:pass@host:5432/mydb backup.dump --clean
pg export -i proj01 | pg import --dsn postgres://user:pass@host:5432/mydb
pg export --dsn postgres://user:pass@host1:5432/db1 | pg import --dsn postgres://user:pass@host2:5432/db2

# DSN can also be used for local instances (useful when ports differ from defaults)
pg export --dsn postgres://admin:pass@127.0.0.1:35432/mydb | pg import --dsn postgres://admin:pass@127.0.0.1:35433/mydb --clean

Format comparison:

Feature Custom (.dump) SQL (.sql)
Import speed Faster (binary format) Slower (text format)
File size Smaller (compressed) Larger (plain text)
Human-readable No Yes
Selective restore Yes (specific tables) No
Best for Migration, backup, large databases Version control, CI seed data, manual editing

Format detection: Uses magic bytes (content-based) with extension fallback.

  • Files starting with PGDMP → custom format
  • .sql or .sql.gz → plain SQL format
  • .gz extension or gzip magic bytes (0x1f 0x8b) → automatic decompression
  • Extension is used as fallback if content detection fails

Remote databases (–dsn): Connect to any PostgreSQL instance using a connection string.

  • No local PostgreSQL installation needed
  • Works with local-to-remote, remote-to-local, and remote-to-remote migrations
  • Supports all the same flags as local instances (-o, -d, --clean, -v, --compress)

Note on existing data: Importing into a database with existing tables will fail unless you use the --clean flag, which drops objects before restoring. Use --clean when importing into a database that already contains data.

Use cases:

  • Migrate data between instances: pg export -i proj01 | pg import -i proj02
  • Cross-host migration: pg export -i proj01 | pg import --dsn postgres://user:pass@remote:5432/db
  • Share database with team: pg export -i proj01 -o dump.dump.gz (compressed, smaller file)
  • Backup before major changes: pg export -i proj01 -o pre-migration.sql.gz
  • CI/CD pipelines: export test data, import into fresh test databases

10 - exec and psql

exec and psql guide for pgcli

Two ways to run SQL against an instance: pg exec for one-shot SQL or container commands, pg psql for interactive sessions.

pg exec

SQL mode (default)

Arguments without -- are executed as SQL via psql, using the instance’s configured user and database.

pg exec "SELECT version()"
pg exec -i proj01 "SELECT count(*) FROM users"
pg exec "CREATE TABLE test (id serial PRIMARY KEY, msg text)"

Container command mode (after –)

Arguments after -- are run directly inside the container (as root).

pg exec -- pg_isready
pg exec -- ls -la /var/lib/postgresql/data
pg exec -- bash -c "cat /var/lib/postgresql/data/postgresql.conf"
pg exec -- tail -f /var/log/postgresql/postgresql-*.log

Remote database (–dsn)

Execute SQL against any database reachable via a connection string, using a temporary container. --dsn only supports SQL mode; container commands require a local instance.

pg exec --dsn postgres://user:pass@host:5432/db "SELECT count(*) FROM users"

pg psql

Interactive session

pg psql                          # default instance
pg psql -i proj01                # specific instance

Inside the shell you get full psql features: SQL with history and tab completion, meta-commands (\dt, \du, \l), and \q to quit.

Non-interactive (scripts)

echo "SELECT version();" | pg psql        # SQL from stdin
pg psql -- -c "SHOW work_mem"             # single command
pg psql -- -d other_db                    # connect to different database
pg psql -- -U other_user                  # connect as different user

Switch to postgres superuser

Some administrative tasks (e.g., creating certain extensions, modifying system-level settings) require postgres superuser privileges. Use -- to pass psql arguments and switch user:

pg psql -i pg01 -- -U postgres -d postgres

Recommended approach: Use the instance default user (admin) for daily operations, and switch to postgres only when superuser privileges are needed. This is safer and more convenient than modifying config files or restarting containers.

Example scenarios:

# Create an extension that requires superuser privileges
pg psql -i pg01 -- -U postgres -d postgres -c "CREATE EXTENSION pg_cron"

# Configure cron.database_name (pg_cron specific parameter)
pg psql -i pg01 -- -U postgres -d postgres -c "ALTER SYSTEM SET cron.database_name = 'pg01_db'"

# View system-level configuration
pg psql -i pg01 -- -U postgres -d postgres -c "SHOW shared_preload_libraries"

Remote database (–dsn)

pg psql --dsn postgres://user:pass@host:5432/db

Rules

  • --dsn and --instance are mutually exclusive: the connection string determines host, port and database, so -i is rejected to avoid silent misuse.
  • With --dsn, the database is the path part of the URL: postgres://user:pass@host:5432/mydb connects to mydb. To use another database, change the path.
  • With a local instance, -- passes raw psql arguments (including -d/-U), overriding the instance defaults.

11 - Destroy

Destroy an instance and remove its configuration

Destroy stops and removes the container, then removes the instance from the configuration file. By default, host data directories are preserved. Use --clean-data to also remove data, WAL archives, and the pgBackRest repository stanza.

Basic Usage

# Destroy instance (keeps data directory)
pg destroy -i proj01

# Destroy without confirmation prompt
pg destroy -i proj01 --force

# Destroy with data cleanup (fresh start)
pg destroy -i proj01 --clean-data

# Skip confirmation and clean all data
pg destroy -i proj01 --clean-data --force

What Happens

When you run pg destroy:

  1. Stop container — PostgreSQL is gracefully shut down
  2. Remove container — The Podman container is deleted
  3. Remove configuration — The instance entry is removed from ~/.pgcli/pg.yaml
  4. Preserve data (by default) — Host data directory at --base-dir/<instance> is kept

With --clean-data:

  1. All of the above, plus:
  2. Remove host data — The data directory is deleted
  3. Remove WAL archives — Any WAL files on the host are removed
  4. Remove backup stanza — The pgBackRest repository stanza for this instance is removed

Recreating an Instance

After destroying, you can recreate the instance with a fresh start:

# Destroy and clean all data
pg destroy -i proj01 --clean-data

# Recreate with the same name
pg create -i proj01 --base-dir /data/pg

# Start the new instance
pg start -i proj01

Important: Without --clean-data, the old data directory is preserved. When you recreate the instance, PostgreSQL will use the existing data, and init.sh (which creates users and sets the admin password) does not run again. This can cause issues if:

  • The data was created with a different user or password
  • You changed the default user in configuration
  • You want a completely fresh start

Use --clean-data when you need a clean slate.

Confirmation Prompt

By default, pg destroy asks for confirmation before proceeding:

$ pg destroy -i proj01
!  This will destroy instance "proj01":
   - Container: pgcli-pg-default-proj01
   - Data dir: /data/pg/proj01 (preserved)

Continue? [y/N]:

Use --force to skip the prompt:

pg destroy -i proj01 --force

Use Cases

1. Clean Restart After Configuration Change

If you changed the default user, password, or PostgreSQL version in the config, destroy and recreate:

# Update configuration
# Edit ~/.pgcli/pg.yaml to change postgres.user or image_tag

# Destroy with data cleanup
pg destroy -i proj01 --clean-data --force

# Recreate with new settings
pg create -i proj01 --base-dir /data/pg
pg start -i proj01

2. Remove Test Instance

After testing, remove an instance you no longer need:

pg destroy -i test-instance --force

3. Troubleshooting Corrupted State

If an instance is in a bad state (e.g., failed to start, corrupted data), destroy and recreate:

pg destroy -i broken-instance --clean-data --force
pg create -i broken-instance --base-dir /data/pg
pg start -i broken-instance

4. Free Up Resources

Destroy instances you’re not actively using to free up:

  • Podman containers (CPU and memory)
  • Host disk space (with --clean-data)
  • Configuration file entries

Relationship with Replicas

When destroying a replica, the replication slot on the primary is not automatically removed. You need to clean it up separately:

# Step 1: Destroy the replica
pg destroy -i ro1 --force

# Step 2: On the primary, drop the replication slot
pg replica drop ro1 -i primary-instance

This two-step process ensures you don’t accidentally lose the slot if you plan to recreate the replica later.

Safety Considerations

  • Data Loss: --clean-data permanently deletes all data, WAL, and backups for the instance. Use with caution.
  • No Undo: Once destroyed, the instance cannot be recovered unless you have external backups.
  • Configuration Loss: The instance entry is removed from the config file. If you need the configuration later, back it up first.

Flags

Flag Default Description
--force false Skip confirmation prompt
--clean-data false Also remove host data, WAL archives, and backup stanza
-i, --instance default Instance name to destroy
  • pg create — Create a new instance
  • pg start — Start an instance
  • pg stop — Stop an instance (container remains)
  • pg replica drop — Remove replication slot from primary

12 - Administration

Administration guide for pgcli

Shell Completion

Enable tab completion for commands, flags, and instance names.

Bash

# Linux
pg completion bash > /etc/bash_completion.d/pg

# macOS (with Homebrew bash-completion)
pg completion bash > $(brew --prefix)/etc/bash_completion.d/pg

# Or load in current session
source <(pg completion bash)

Zsh

# Enable completion system (once)
echo "autoload -U compinit; compinit" >> ~/.zshrc

# Install completion
pg completion zsh > "${fpath[1]}/_pg"

Fish

pg completion fish > ~/.config/fish/completions/pg.fish

PowerShell

pg completion powershell > pg.ps1
# Source from your PowerShell profile

PostgreSQL Configuration

Modify PostgreSQL runtime parameters via pg exec with ALTER SYSTEM, then reload:

# Change a parameter
pg exec "ALTER SYSTEM SET work_mem = '256MB'"
pg exec "SELECT pg_reload_conf()"

# For a specific instance
pg exec -i proj01 "ALTER SYSTEM SET effective_cache_size = '4GB'"
pg exec -i proj01 "SELECT pg_reload_conf()"

Note: Some parameters (e.g. shared_buffers, max_connections) require a restart rather than reload. Use pg stop && pg start to apply those changes.

Configuration File Management

Inspect or validate the config file (~/.pgcli/pg.yaml by default, override with -c).

# Show current configuration (YAML)
pg config show

# Show configuration as JSON
pg config show --json

# Validate config file structure
pg config validate

Init generates a default config; --add creates a named instance in the same file, -o writes to a custom path:

pg config init --add default --base-dir /data/pg
pg config init --add proj01 --base-dir /data/pg -o ./my-pg.yaml

Isolation parameters for running multiple configs on one host (see Quick Start for planning):

pg config init --namespace t1 --pg-start-port 38000 --pg-ssh-port 43000 --add proj01 -o ~/.pgcli-t1/pg.yaml
Parameter Default Meaning
--namespace default Prefix for container names: pgcli-pg-<namespace>-<instance>, backup container pgcli-backup-<namespace>. Pass --namespace "" to keep legacy names without a prefix
--pg-start-port 35432 First PG host port in the allocation range
--pg-ssh-port 42201 First SSH host port in the allocation range

All three are saved into the config file (namespace, pg_start_port, pg_ssh_port); use disjoint port ranges across configs so allocations never collide.