PgDog
PgDog is a high-performance Postgres proxy written in Rust (the successor to PgCat). It provides connection pooling, read/write load balancing across replicas, and horizontal sharding — in front of one or more PostgreSQL backends. pgcli can run PgDog as a standalone, top-level addon: shared infrastructure rather than a per-instance sidecar.
Security caveat: PgDog authenticates clients against plaintext passwords stored in
users.toml. pgcli writes that file mode0600and mounts it read-only into the container, but the passwords still sit in clear text on disk and inpg.yaml. Keep the listen port on loopback (the default) or inside a trusted network, and treat the config file as a secret.
PgDog is configured with two TOML files — pgdog.toml (the proxy, its
backends, and sharding rules) and users.toml (the client credentials).
pgcli generates both entirely from the install flags: there is no config
file to hand-edit, so pg addon install pgdog is the source of truth and is
idempotent — re-running it re-renders the files from the flags given.
Platform support
PgDog runs on Linux (host networking) and on macOS for single-host
dev/test, joining the same pgcli-net bridge a PG instance uses under podman
machine and publishing its client + openmetrics ports. On macOS:
- The listen bind is widened to
0.0.0.0inside the container so the published port is reachable (a loopback-only bind can’t be forwarded to); the client address you connect to is still127.0.0.1:<port>. - Backends must be reachable from the bridge. PgDog has no automatic
name-resolution for
--backend, so a127.0.0.1backend would point at the Mac, not the podman machine VM. Give each--backendeither the target instance’s container name (runpg status -i <instance>and read theContainer:line, e.g.app=pgcli-pg-mypg:5432:...) or an address the Mac can route to — never127.0.0.1for a managed instance.
How It Works
- Shared infrastructure: PgDog lives in the top-level
addons.pgdogmap inpg.yaml, keyed by proxy name — not under any single instance. - Host network (Linux) / bridge (macOS): the proxy listens on its client
port (auto-assigned from
pgdog_start_port, default 7432) plus a Prometheus-styleopenmetricsport on the next free number. On Linux it runs with--network host; on macOS it joinspgcli-netand publishes both ports. - Generated config:
pgdog.toml+users.tomlare written under<base-dir>/addon/pgdog/<name>/and bind-mounted into the container at/pgdog. - Image pinned:
ghcr.io/pgdogdev/pgdog:v0.1.57by default; override with--image.
Install
A minimal single-backend proxy (pooling only — see the note below):
Declare a
--sharded-tableso the example is a complete, self-consistent config (it mirrors the sharding example below). Be aware that with a single--backendthere is nothing to shard across — every row lands on that one backend regardless of the declaration, so this minimal example only demonstrates connection pooling. For real sharding you need two or more--backends with distinct shard numbers (see Sharding and replicas).
--backend NAME=HOST:PORT:DBNAME[:SHARD[:ROLE]]— one[[databases]]entry.NAMEis the logical database clients connect to;DBNAMEis the real database on the backend.SHARDdefaults to 0;ROLEto PgDog’s default (primary, or setreplicafor read routing). Repeatable.--user NAME:PASSWORD[:DBNAME]— one[[users]]entry. TheDBNAMEis the logical database (a--backendname) the user may reach; it defaults to the first backend’s name. Repeatable.- At least one
--backendand one--userare required.
Other flags:
| Flag | Meaning | Default |
|---|---|---|
--name |
proxy / addon key | pgdog |
--port |
client host port (openmetrics takes the next free) | auto-assign |
--host |
listen address | 127.0.0.1 |
--pool-mode |
transaction or session |
transaction |
--workers |
worker threads | 2 |
--default-pool-size |
server connections per user/db pair | 10 |
--image |
PgDog image tag | ghcr.io/pgdogdev/pgdog:v0.1.57 |
Sharding and replicas
Repeat --backend across shards and roles, and declare the shard key with
--sharded-table DBNAME:TABLE:COLUMN:DATA_TYPE:
This puts two shards (each with a primary + replica) behind the logical
database app and routes the users table by its id column.
Provisioning the backends
PgDog routes to databases, tables, and roles that must already exist on the
backends — installing the proxy does not create them. For a local pgcli
instance, use pg exec to create each shard database, its sharded table, and
the login role. With the default instance on a single host (shards as separate
databases on one port), the example below provisions the backends (steps 1–4),
installs the proxy (step 5), and writes through it to show the routing (steps
6–7):
pg exec -i default -- psql -d <db> runs a command against a specific database
in the named instance’s container. The id bigint column matches the
--sharded-table "app:users:id:bigint" declaration above, so PgDog can route
users rows by id. Steps 6–7 make that routing visible: the two shard queries
return different, non-overlapping subsets of the ids you inserted (the exact
split depends on PgDog’s hash), and together they account for every row — no row
lands in both shards. For remote backends, run the equivalent SQL against each
host directly.
The backend role is not optional. By default PgDog authenticates to the
backends using the client’s --user name and its users.toml password. So
every backend must have a login role named appuser (etc.), or connections fail
with password for user "..." is wrong, or the database does not exist, even on
a loopback trust setup where the password is ignored but the role must still
exist. Under password auth (the default for non-loopback scram-sha-256), the
users.toml password must match the role’s.
Decoupling client and backend users. PgDog itself supports connecting to the backends with a different user than the client uses: a
[[users]]entry can carryserver_user/server_password, and a[[databases]]entry can carryuser/password(which take priority). pgcli’s install flags do not currently emit these fields, so throughpg addon install pgdogthe backend role name is always the--username. If you need a shared backend role (a commonpostgressuperuser, or a name that differs from every client user), hand-edit the generatedusers.toml/pgdog.tomland restart the container — but note a re-install regenerates both files from the flags and will discard those edits.
Connecting
The install summary prints the endpoint. Clients connect through the proxy’s
client port, using the logical database name and a --user credential — with
pgcli, that’s a --dsn pointing at the proxy:
Prometheus metrics are served on the openmetrics port:
Writing to shards
How a write lands on the shards depends on the form of the statement. Observed with PgDog v0.1.57:
Single-row INSERT — routed by the shard key. One VALUES tuple per
statement; PgDog hashes the id and sends that row to exactly one shard:
Inserting ids 1–6 as six single-row statements split them across the two
shards (in one run: shard0 got 1,2, shard1 got 3,4,5,6 — the exact split
is PgDog’s hash, not something to rely on). No row lands in both shards.
Multi-row INSERT — broadcast, not split. A single statement carrying
several VALUES tuples is sent verbatim to every shard, so each shard
ends up with every row:
This reported INSERT 0 8 — 4 rows in each shard. The per-shard PRIMARY KEY still holds (the duplicate ids live in different shards, never colliding),
and there is no error or warning: every shard silently grows a full copy of
the batch. Reads through the proxy will then see each id multiple times.
Bulk-loading correctly: loop one statement per row through the proxy (each
routed individually), or skip the proxy and run the load against each shard
directly (COPY via pg exec -i <inst> -- psql -d <shard>).
Configuration
After install, pg.yaml records each proxy under the top-level addons.pgdog:
Port base is configurable via the top-level pgdog_start_port (default 7432);
the openmetrics port is always allocated just above it.
List
Every PgDog proxy appears under the Infra add-ons (pgdog) section with its runtime status, ports, backend/user counts, image, and container name:
Backends / Users are the counts from pgdog.toml / users.toml; Status
reflects the live container (a stopped proxy shows stopped).
Start and stop
After a reboot or a manual stop, bring the proxy up without re-running install (config and users are untouched):
start is idempotent — a running proxy is left alone; a proxy whose container
was removed is recreated from its config directory.
Auto-start on Boot
Containers carry a --restart unless-stopped policy (crashes, not reboots). To
bring a proxy up after a host reboot:
This sets autostart: true and installs/refreshes the boot service (see
Auto-start on Boot). Boot is start-only: it starts the
existing container reading the pgdog.toml/users.toml already on disk, and
never re-generates config or auth. Install the proxy first. pg autostart status lists every target’s state.
Logs
PgDog logs connection-pool events (new server connections, auth, client
connect/disconnect) as structured INFO lines. A repeatedly failing pool with
password for user "..." is wrong, or the database does not exist means step 3
or 4 above was skipped — the backend has no such login role, or its password
does not match users.toml.
Notes
- Reinstall is idempotent: re-running
pg addon install pgdog --name <p>re-renders the config from the given flags and recreates the container. The backend/user/shard lists are fully replaced by this command’s flags — an omitted--backendis removed, not kept. - Plaintext credentials: passwords live in
users.tomlandpg.yaml. Prefer a dedicated low-privilege role for proxy users and restrict network access to the client port. - Write forms differ: single-row
INSERTis routed by the shard key; a multi-rowINSERT VALUESis broadcast to every shard. See Writing to shards. - Not a per-instance sidecar: unlike PgBouncer, PgDog frontends backends by
address in
--backend— it does not have to point at a pgcli-managed instance, and one proxy can span many databases/shards.