Repo: github.com/hrodrig/pgwd · Releases: Releases
Go CLI that checks PostgreSQL connection counts (active/idle) and notifies via Slack and/or Loki when configured thresholds are exceeded. It can also alert on stale connections (connections that stay open and never close).
Documentation: Sequence diagrams (Mermaid) for each use case, audited against the code, and terminal demo (recorded with VHS) — see docs/. Scanning before release (govulncheck, Grype): tools/README.md.
# See all options
pgwd -h
# Minimal: check once, alert to Slack (total/active default to 80% of server max_connections; change with -default-threshold-percent)
pgwd -db-url "postgres://user:pass@localhost:5432/mydb" \
-slack-webhook "https://hooks.slack.com/services/..."
# Same but alert at 70% of max_connections
pgwd -db-url "postgres://..." -slack-webhook "https://..." -default-threshold-percent 70
# Or set an explicit threshold
pgwd -db-url "postgres://user:pass@localhost:5432/mydb" \
-threshold-total 80 \
-slack-webhook "https://hooks.slack.com/services/..."Every option can be set by CLI flag or environment variable (prefix PGWD_). CLI overrides env. That lets you use env for secrets and defaults, and override with flags when needed.
export PGWD_DB_URL="postgres://user:pass@localhost:5432/mydb"
export PGWD_THRESHOLD_TOTAL=80
export PGWD_THRESHOLD_IDLE=50
export PGWD_SLACK_WEBHOOK="https://hooks.slack.com/services/..."
export PGWD_INTERVAL=60
pgwd
# Runs as daemon every 60s; no need to pass any flag.export PGWD_DB_URL="postgres://localhost:5432/mydb"
export PGWD_THRESHOLD_TOTAL=80
export PGWD_SLACK_WEBHOOK="https://hooks.slack.com/..."
# Override DB and run once (e.g. for a different host)
pgwd -db-url "postgres://prod-host:5432/mydb" -interval 0
# Override threshold for a quick test
pgwd -threshold-total 5 -dry-run| Threshold | Use when you care about… | Example |
|---|---|---|
| total | Overall connection usage (e.g. near max_connections) |
-threshold-total 80 |
| active | Queries running right now (load / long queries) | -threshold-active 50 |
| idle | Pool size / connections sitting idle | -threshold-idle 40 |
| stale | Connections open too long (leaks, never closed) | -stale-age 600 -threshold-stale 1 |
# Total connections ≥ 80 (one-shot, Slack)
pgwd -db-url "postgres://user:pass@localhost:5432/mydb" \
-threshold-total 80 \
-slack-webhook "https://hooks.slack.com/services/..."
# Active connections ≥ 50 (one-shot, Loki)
pgwd -db-url "postgres://..." -threshold-active 50 -loki-url "http://localhost:3100/loki/api/v1/push"
# Idle connections ≥ 40 (daemon every 60s, Slack)
pgwd -db-url "postgres://..." -threshold-idle 40 -interval 60 -slack-webhook "https://..."
# Stale: ≥ 1 connection open longer than 10 minutes
pgwd -db-url "postgres://..." -stale-age 600 -threshold-stale 1 -slack-webhook "https://..."You can combine several thresholds; each one that is exceeded generates an alert (same run can send multiple events).
# Alert on total OR idle OR stale in a single run
pgwd -db-url "postgres://..." \
-threshold-total 90 \
-threshold-idle 60 \
-stale-age 600 -threshold-stale 1 \
-interval 120 \
-slack-webhook "https://..." \
-loki-url "http://localhost:3100/loki/api/v1/push"# Slack only
pgwd -db-url "postgres://..." -threshold-total 80 -slack-webhook "https://hooks.slack.com/..."
# Loki only (optional labels)
pgwd -db-url "postgres://..." -threshold-total 80 \
-loki-url "http://localhost:3100/loki/api/v1/push" \
-loki-labels "job=pgwd,env=prod,db=myapp"
# Slack and Loki (same event sent to both)
pgwd -db-url "postgres://..." -threshold-total 80 \
-slack-webhook "https://hooks.slack.com/..." \
-loki-url "http://localhost:3100/loki/api/v1/push"# One-shot: run once, then exit (ideal for cron)
pgwd -db-url "postgres://..." -threshold-total 80 -slack-webhook "https://..."
# or: PGWD_INTERVAL=0 pgwd
# Daemon: run every N seconds until Ctrl+C or SIGTERM
pgwd -db-url "postgres://..." -threshold-total 80 -interval 60 -slack-webhook "https://..."
# Dry run: only print stats (total/active/idle), no notifications; no webhook/loki needed
pgwd -db-url "postgres://..." -threshold-total 100 -dry-run
# Output example: total=42 active=3 idle=39
# Force notification: send a test message to all configured notifiers (no threshold required)
# Use to validate delivery and format before relying on real alerts
pgwd -db-url "postgres://..." -slack-webhook "https://..." -force-notification
pgwd -db-url "postgres://..." -loki-url "http://localhost:3100/loki/api/v1/push" -force-notification| Scenario | Suggestion |
|---|---|
| Cron check every 5 min | One-shot (interval 0 or unset), one or more thresholds, Slack or Loki. Run from cron every 5 minutes. |
| Long-running watcher | Daemon with -interval 60 (or 120). Run under systemd/supervisor; stop with SIGTERM. |
| Detect connection leaks | Use stale-age + threshold-stale (e.g. 600 and 1). Alert when any connection stays open longer than 10 min. |
| Pre-production test | -dry-run and low thresholds to see current counts without sending alerts. |
| Validate notifications | -force-notification with Slack/Loki: sends one test message regardless of thresholds. Use one-shot to confirm delivery, format, and how messages look. (If the connection to Postgres fails, pgwd always sends a connect-failure alert when a notifier is configured.) |
| Test alerts without low max_connections | Use -test-max-connections N (e.g. 20) with -force-notification or low thresholds: thresholds and messages use N as “max_connections”, while stats stay real. Notifications show “(test override)” so total can exceed N. |
| Zero config (use defaults) | Only set -db-url and a notifier; total and active thresholds default to default-threshold-percent (default 80%) of server max_connections. Use -default-threshold-percent to change (e.g. 70 or 90). |
| Multiple environments | Set PGWD_* in env per environment; override -db-url or -loki-labels per deploy. |
| Postgres in Kubernetes | Use -kube-postgres namespace/svc/name (or namespace/pod/name). pgwd runs kubectl port-forward and connects to localhost. Optionally put DISCOVER_MY_PASSWORD in the URL to read the password from the pod's env (e.g. POSTGRES_PASSWORD). Requires kubectl in PATH. |
| Alert when Postgres is unreachable | If you configure a notifier (Slack/Loki), pgwd always sends an alert when the connection fails (e.g. refused, timeout, or "too many clients"). No extra flag needed. |
Cron runs with a minimal environment (e.g. PATH=/usr/bin:/bin). Two things to keep in mind:
-
-kube-postgresand PATH: If you use-kube-postgres, cron must seekubectlin PATH. SetPATHin the cron line or in a wrapper script so it includes the directory wherekubectllives (e.g./usr/local/bin):# In crontab: set PATH before the command PATH=/usr/local/bin:/usr/bin:/bin */5 * * * * /usr/local/bin/pgwd -kube-postgres default/svc/postgres -db-url "postgres://..." -slack-webhook "https://..."
Or use a wrapper script that exports PATH and runs pgwd:
#!/bin/sh export PATH="/usr/local/bin:$PATH" exec /usr/local/bin/pgwd "$@"
-
Seeing errors: If
kubectlis not found, pgwd exits immediately with a clear message to stderr. Cron often mails stderr to the user; otherwise redirect stdout and stderr to a log file so you can see why the job failed:*/5 * * * * /usr/local/bin/pgwd -db-url "postgres://..." -slack-webhook "https://..." >> /var/log/pgwd.log 2>&1
Here
>>appends stdout to the file and2>&1sends stderr to the same place.
You can run pgwd for several Postgres instances (e.g. one per Kubernetes service) from a single cron schedule: use a bash script that sets KUBECONFIG, PGWD_SLACK_WEBHOOK, and PATH, then invokes pgwd once per service with distinct -kube-local-port values so port-forwards do not clash. Add a second script that runs -force-notification on a schedule (e.g. every 2 hours) as a “still alive” heartbeat.
Check script (e.g. ~/bin/pgwd-cron.sh): runs every 5 minutes, checks all services, alerts only when thresholds are exceeded.
#!/bin/bash
mkdir -p ~/log
export KUBECONFIG=/path/to/your/kubeconfig
export PGWD_SLACK_WEBHOOK="https://hooks.slack.com/services/..."
export PATH="/usr/local/bin:$PATH"
PGWD=${PGWD:-/usr/local/bin/pgwd}
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) checking postgres-a"
$PGWD -kube-postgres mynamespace/svc/postgres-a \
-kube-local-port 15432 \
-db-url 'postgres://postgres:DISCOVER_MY_PASSWORD@postgres-a:15432/db_a'
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) checking postgres-b"
$PGWD -kube-postgres mynamespace/svc/postgres-b \
-kube-local-port 15433 \
-db-url 'postgres://postgres:DISCOVER_MY_PASSWORD@postgres-b:15433/db_b'
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) checking postgres-c"
$PGWD -kube-postgres mynamespace/svc/postgres-c \
-kube-local-port 15434 \
-db-url 'postgres://postgres:DISCOVER_MY_PASSWORD@postgres-c:15434/db_c'
exit 0Heartbeat script (e.g. ~/bin/pgwd-heartbeat.sh): runs every 2 hours, sends a test notification per service so you know the pipeline is up. Use different local ports (e.g. 25432…) so they do not conflict with the check script if both run close together.
#!/bin/bash
mkdir -p ~/log
export KUBECONFIG=/path/to/your/kubeconfig
export PGWD_SLACK_WEBHOOK="https://hooks.slack.com/services/..."
export PATH="/usr/local/bin:$PATH"
PGWD=${PGWD:-/usr/local/bin/pgwd}
$PGWD -kube-postgres mynamespace/svc/postgres-a \
-kube-local-port 25432 \
-db-url 'postgres://postgres:DISCOVER_MY_PASSWORD@postgres-a:25432/db_a' \
-force-notification
$PGWD -kube-postgres mynamespace/svc/postgres-b \
-kube-local-port 25433 \
-db-url 'postgres://postgres:DISCOVER_MY_PASSWORD@postgres-b:25433/db_b' \
-force-notification
exit 0Crontab (crontab -e): run checks every 5 minutes and heartbeat every 2 hours; append output to one log file.
PATH=/usr/bin:/bin
*/5 * * * * /bin/bash -l -c '~/bin/pgwd-cron.sh >> ~/log/pgwd.log 2>&1'
0 */2 * * * /bin/bash -l -c '~/bin/pgwd-heartbeat.sh >> ~/log/pgwd.log 2>&1'
Adjust KUBECONFIG, webhook URL, namespace, service names, database names, and PGWD path to your environment. If a pod uses a different env var for the password, add -kube-password-var VARNAME (and -kube-password-container if the var is in another container). The echo lines in the check script make it easy to see which service produced an error in the log.
When Postgres runs inside a Kubernetes cluster, use -kube-postgres so pgwd connects via kubectl port-forward (no separate script or manual port-forward).
Format: -kube-postgres <namespace>/<type>/<name> with type = svc or pod, e.g. default/svc/postgres or default/pod/postgres-0.
- Set
PGWD_DB_URLwith hostlocalhostand the same port as-kube-local-port(default 5432). Example:postgres://user:pass@localhost:5432/mydb. - Password from the pod: If the URL password is the literal
DISCOVER_MY_PASSWORD, pgwd reads the password from the Postgres pod's environment (POSTGRES_PASSWORDby default, orPGPASSWORD). Use-kube-password-varto choose the env var and-kube-password-containerif the Postgres container is not the default. - Requires:
kubectlin PATH and a valid kubeconfig. pgwd checks forkubectlbefore any kube step and exits with a clear error if it is missing. pgwd starts the port-forward, connects, and stops it on exit. When running from cron, set PATH sokubectlis findable (see Running from cron above).
# With password in URL
PGWD_DB_URL="postgres://postgres:secret@localhost:5432/mydb" \
pgwd -kube-postgres default/svc/postgres -slack-webhook "https://..." -dry-run
# Password from pod env (POSTGRES_PASSWORD)
PGWD_DB_URL="postgres://postgres:DISCOVER_MY_PASSWORD@localhost:5432/mydb" \
pgwd -kube-postgres default/svc/postgres -dry-runAll parameters can be set via CLI or environment variables with prefix PGWD_. CLI overrides env.
| CLI | Env | Description |
|---|---|---|
-db-url |
PGWD_DB_URL |
PostgreSQL connection URL (required). With -kube-postgres, use host localhost and port matching -kube-local-port. |
-kube-postgres |
PGWD_KUBE_POSTGRES |
Connect via kubectl port-forward: namespace/type/name (e.g. default/svc/postgres). Requires kubectl in PATH. |
-kube-local-port |
PGWD_KUBE_LOCAL_PORT |
Local port for port-forward (default 5432). Use different ports to run multiple pgwd against different Postgres in the cluster. |
-kube-password-var |
PGWD_KUBE_PASSWORD_VAR |
Pod env var name when URL password is DISCOVER_MY_PASSWORD (default POSTGRES_PASSWORD). |
-kube-password-container |
PGWD_KUBE_PASSWORD_CONTAINER |
Container name in pod for password discovery (default: primary container). |
-cluster |
PGWD_CLUSTER |
Cluster name shown in Slack/Loki (health-check style). When using -kube-postgres, detected from kubeconfig if unset. |
-client |
PGWD_CLIENT |
Client/service/pod name shown in Slack (e.g. VM or service name). When using -kube-postgres, derived from resource (e.g. svc/name) if unset; otherwise hostname. |
-threshold-total |
PGWD_THRESHOLD_TOTAL |
Alert when total connections ≥ N (default: default-threshold-percent of max_connections if 0) |
-threshold-active |
PGWD_THRESHOLD_ACTIVE |
Alert when active connections ≥ N (default: default-threshold-percent of max_connections if 0) |
-threshold-idle |
PGWD_THRESHOLD_IDLE |
Alert when idle connections ≥ N |
-stale-age |
PGWD_STALE_AGE |
Consider connection stale if open longer than N seconds (requires -threshold-stale) |
-threshold-stale |
PGWD_THRESHOLD_STALE |
Alert when stale connections (open > stale-age) ≥ N |
-slack-webhook |
PGWD_SLACK_WEBHOOK |
Slack Incoming Webhook URL |
-loki-url |
PGWD_LOKI_URL |
Loki push API URL (e.g. http://localhost:3100/loki/api/v1/push) |
-loki-labels |
PGWD_LOKI_LABELS |
Loki labels, e.g. job=pgwd,env=prod |
-interval |
PGWD_INTERVAL |
Run every N seconds; 0 = run once |
-dry-run |
PGWD_DRY_RUN |
Only print stats, do not send notifications |
-force-notification |
PGWD_FORCE_NOTIFICATION |
Always send at least one notification: test event when connected (to validate delivery, format, and channel). Requires at least one notifier. (Connection failure is always notified when a notifier is configured, with or without this flag.) |
-notify-on-connect-failure |
PGWD_NOTIFY_ON_CONNECT_FAILURE |
Legacy: connection failure is always notified when a notifier is configured; this flag is no longer required. Kept for backward compatibility; if set, still requires at least one notifier at startup. |
-default-threshold-percent |
PGWD_DEFAULT_THRESHOLD_PERCENT |
When total/active threshold are 0, set them to this % of max_connections (1–100). Default: 80 |
-test-max-connections |
PGWD_TEST_MAX_CONNECTIONS |
Override server max_connections for threshold defaults and display (testing only). When set, defaults and notifications use this value instead of the server’s; stats (total/active/idle) remain real. Notifications show “(test override)” so you can simulate e.g. a low limit and trigger alerts without a real low max_connections. |
Stale connections: A connection is "stale" if it has been open longer than stale-age seconds (based on backend_start in pg_stat_activity). Use this to detect leaks or connections that are never closed. When using threshold-stale, stale-age must be set and > 0.
Default thresholds: If you do not set threshold-total or threshold-active (leave them 0), pgwd sets them to a percentage of the server's max_connections after connecting. The percentage is controlled by -default-threshold-percent / PGWD_DEFAULT_THRESHOLD_PERCENT (default 80, range 1–100). Example: with max_connections=100 and default percent 80, total and active thresholds become 80; with -default-threshold-percent 70 they become 70. So you can run with only -db-url and a notifier and get alerts at your chosen percentage of the server limit. Idle and stale have no default (0 = disabled). Defaults are applied once at startup; the DB user must be able to read max_connections (any normal role can).
From source (recommended):
go install github.com/hrodrig/pgwd@latestThis installs the binary to $GOBIN (default $HOME/go/bin). Ensure $GOBIN is on your PATH.
Pre-built binaries: Releases provide binaries (tar.gz, zip), .deb, and .rpm packages for Linux, macOS, and Windows (amd64 and arm64).
Homebrew (macOS):
brew install hrodrig/pgwd/pgwdgo build -o pgwd ./cmd/pgwd
# or use the Makefile:
make build
make install
# Custom install path: GOBIN=~/bin make install (default is $HOME/go/bin)Release (GitHub): From branch main, after tagging (e.g. git tag v0.2.3), run make release. Requires goreleaser (brew install goreleaser). For a local snapshot build without publishing: make snapshot (outputs to dist/).
Unit tests for config (env, defaults, overrides) and notify (Loki label parsing):
go test ./internal/config/... ./internal/notify/... -vRun all tests (including any in other packages):
go test ./...Run a PostgreSQL container to test pgwd without a real server. Use port 5433 on the host so connections from your machine go to the container and not to a local Postgres on 5432 (common on macOS):
docker stop pgwd-pg 2>/dev/null; docker rm pgwd-pg 2>/dev/null
docker run -d --name pgwd-pg \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=postgres \
-p 5433:5432 \
postgres:16-alpineConnection URL (host port 5433, sslmode=disable):
postgres://postgres:secret@127.0.0.1:5433/postgres?sslmode=disable
Dry-run:
pgwd -db-url "postgres://postgres:secret@127.0.0.1:5433/postgres?sslmode=disable" -dry-runWith a notifier (e.g. Slack): add -slack-webhook "https://..." and optionally -threshold-total 5.
Stop and remove:
docker stop pgwd-pg && docker rm pgwd-pgUsing 127.0.0.1 and host port 5433 avoids hitting a local Postgres on 5432 and avoids IPv6 resolution quirks.
Who is on 5432? Run lsof -i :5432. If you see both postgres (local) and com.docke (Docker), connections to localhost:5432 go to the local postgres (it binds to localhost); the container is on *:5432. Use host port 5433 for the container so your client clearly reaches the container.
- At least one of: a threshold (
threshold-total,threshold-active,threshold-idle, orthreshold-stalewithstale-age),-dry-run, or-force-notification. If you set only-db-urland a notifier, pgwd defaults total and active todefault-threshold-percent(default 80) ofmax_connections. - If not using
-dry-run: at least one notifier (slack-webhookorloki-url). For-force-notification, a notifier is required. - For
threshold-stale,stale-agemust be set and greater than 0.
- One-shot (
interval0 or unset): runs one check, sends alerts if thresholds are exceeded, then exits. Exit code 0 on success; non-zero on fatal errors (e.g. DB connection failure). - Daemon (
intervalgreater than 0): runs everyintervalseconds until interrupted (Ctrl+C or SIGTERM). Exits with 0 after a clean shutdown. - Dry run: same as above but no HTTP calls to Slack/Loki; only logs stats to stdout.
pgwd -hShows all flags and their env equivalents.
Create an Incoming Webhook in your Slack workspace and set PGWD_SLACK_WEBHOOK or -slack-webhook.
Notification format: One message per alert. Body (plain text in the webhook payload):
:warning: *pgwd* – Threshold exceeded
*<Message>*
Connections: total=<Total>, active=<Active>, idle=<Idle> (limit <Threshold>=<ThresholdValue>)
<Message>is the event message (e.g.Total connections 85 >= 80orTest notification — delivery check (force-notification).).<Total>,<Active>,<Idle>are the current connection counts frompg_stat_activityfor the current database.<Threshold>is one oftotal,active,idle,stale, ortest(for force-notification).<ThresholdValue>is the configured limit that was exceeded (0 fortest).
Set the Loki push endpoint URL (e.g. http://loki:3100/loki/api/v1/push). Optionally set PGWD_LOKI_LABELS for stream labels (e.g. job=pgwd,env=prod); default includes job=pgwd.
Notification format: Each alert is one log line in a stream. The stream has labels from PGWD_LOKI_LABELS plus job=pgwd (if not set) and threshold=<total|active|idle|stale|test>. The log line is:
pgwd threshold exceeded: <Message> | total=<Total> active=<Active> idle=<Idle> (limit <Threshold>=<ThresholdValue>)
Same placeholders as Slack. Timestamp is the time of the push. You can query in Grafana or LogCLI by label (e.g. {job="pgwd", threshold="total"}).
| Symptom | What to check |
|---|---|
| "missing database URL" | Set PGWD_DB_URL or -db-url. The URL must be a valid PostgreSQL connection string. |
| "no thresholds set and could not default from server..." | pgwd could not read max_connections from the server (error or 0). Set -threshold-total and/or -threshold-active explicitly, or use -dry-run or -force-notification. With a normal Postgres, only -db-url and a notifier should be enough (defaults to 80% of max_connections). |
| "no notifier configured" | Set PGWD_SLACK_WEBHOOK or PGWD_LOKI_URL (or use -dry-run to skip notifications). |
| "force-notification requires at least one notifier" | Use -force-notification together with -slack-webhook and/or -loki-url. |
| "notify-on-connect-failure requires at least one notifier" | You set -notify-on-connect-failure but have no notifier. Add -slack-webhook and/or -loki-url. (Connect failure is always notified when a notifier is configured; the flag is optional.) |
| "kubectl not found in PATH" | When using -kube-postgres, ensure kubectl is installed and on your PATH (e.g. which kubectl). pgwd exits with this message before attempting port-forward or password discovery. |
| "when using threshold-stale, stale-age must be > 0" | Set -stale-age N (e.g. 600) when using -threshold-stale. |
| Slack/Loki not receiving alerts | Run once with -force-notification to send a test message. Check webhook URL, network/firewall, and that the app can reach Slack/Loki. |
| "postgres connect: ..." | DB unreachable: check host, port, TLS, credentials, and that the pgwd host can reach the Postgres server. |
| Stats or stale count errors in logs | Permissions: the DB user must be able to read pg_stat_activity (usually any role can). Check log.Printf output for the exact error. |
Published image (each release): Multi-arch images (linux/amd64, linux/arm64) are published to GitHub Container Registry as ghcr.io/hrodrig/pgwd. Use a version tag or latest:
docker pull ghcr.io/hrodrig/pgwd:v0.2.3
# or
docker pull ghcr.io/hrodrig/pgwd:latestBuild from source: The repo includes a multi-stage Dockerfile (Go 1.26, Alpine 3.23): build stage compiles the binary with version/commit/build date injected via build args; runtime stage is minimal and runs as non-root. Use make docker-build to build locally with version info.
Image details
- Runtime base: Alpine 3.23. Only
ca-certificatesfor HTTPS (Slack/Loki). Nowget,nc, orcurl(base image’swget/ncare BusyBox applets and are removed; they are not separate packages, so we remove the symlinks). - User: Runs as non-root user
pgwd(binary in/home/pgwd/pgwd). - Labels: OCI image labels (title, description, source, authors).
- Build context:
.dockerignoreuses a whitelist: onlygo.mod,go.sum,cmd/, andinternal/are sent;docs/,contrib/, README, etc. are excluded.
Build (from repo root)
Use make docker-build so the image gets version, commit, and build date from the VERSION file and git (same as make build):
make docker-buildThis runs docker build with --build-arg VERSION=..., --build-arg COMMIT=..., --build-arg BUILDDATE=.... If you build with plain docker build -t pgwd ., the binary will report dev / unknown for version and commit.
Validate the image
Use the published image ghcr.io/hrodrig/pgwd:latest (or :v0.2.3), or pgwd if you built locally with make docker-build:
# Help (no DB needed)
docker run --rm ghcr.io/hrodrig/pgwd:latest -h
# Version (should show e.g. pgwd v0.2.3 (commit ..., built ...))
docker run --rm ghcr.io/hrodrig/pgwd:latest --version
# Expect "missing database URL" (validates startup path)
docker run --rm ghcr.io/hrodrig/pgwd:latestRun (one-shot or daemon)
# One-shot: pass env and ensure network to Postgres (and Slack/Loki if used)
docker run --rm \
-e PGWD_DB_URL="postgres://user:pass@host.docker.internal:5432/mydb" \
-e PGWD_THRESHOLD_TOTAL=80 \
-e PGWD_SLACK_WEBHOOK="https://hooks.slack.com/..." \
ghcr.io/hrodrig/pgwd:latest
# Daemon (interval 60s)
docker run --rm -d --name pgwd \
-e PGWD_DB_URL="postgres://user:pass@host.docker.internal:5432/mydb" \
-e PGWD_THRESHOLD_TOTAL=80 \
-e PGWD_SLACK_WEBHOOK="https://hooks.slack.com/..." \
-e PGWD_INTERVAL=60 \
ghcr.io/hrodrig/pgwd:latestUse host.docker.internal (or your host IP) to reach Postgres on the host from the container. For secrets, prefer env files or a secrets manager instead of hardcoding in the image.
pgwd is configured only via environment variables (no config file yet). On systemd you use an env file that the unit loads.
Convention
| What | Path |
|---|---|
| Binary | /usr/local/bin/pgwd |
| Env file (option A) | /etc/pgwd.env |
| Env file (option B) | /etc/pgwd/pgwd.env (useful if you later add e.g. /etc/pgwd/pgwd.toml) |
The unit files in the repo try both env paths (EnvironmentFile=-/etc/pgwd/pgwd.env then -/etc/pgwd.env). Create one of them and restrict permissions: sudo chmod 600 /etc/pgwd.env.
Two ways to run
- Daemon — pgwd runs continuously and checks every
PGWD_INTERVALseconds. Usecontrib/systemd/pgwd.service. - One-shot on a schedule — pgwd runs once per tick (e.g. every 5 minutes). Use
contrib/systemd/pgwd.timer+contrib/systemd/pgwd-once.service.
Daemon (long-running)
# Install binary
sudo cp pgwd /usr/local/bin/pgwd
# Copy unit and create env file
sudo cp contrib/systemd/pgwd.service /etc/systemd/system/
sudo tee /etc/pgwd.env > /dev/null << 'EOF'
PGWD_DB_URL=postgres://user:pass@localhost:5432/mydb
PGWD_THRESHOLD_TOTAL=80
PGWD_THRESHOLD_IDLE=50
PGWD_SLACK_WEBHOOK=https://hooks.slack.com/services/...
PGWD_LOKI_URL=http://localhost:3100/loki/api/v1/push
PGWD_INTERVAL=60
EOF
sudo chmod 600 /etc/pgwd.env
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable --now pgwd
sudo systemctl status pgwd
# Logs
journalctl -u pgwd -fOne-shot from a timer (cron-like)
Runs pgwd once every 5 minutes (no PGWD_INTERVAL needed; the timer is the schedule).
sudo cp pgwd /usr/local/bin/pgwd
sudo cp contrib/systemd/pgwd-once.service contrib/systemd/pgwd.timer /etc/systemd/system/
# Create /etc/pgwd.env as above (omit PGWD_INTERVAL or set 0)
sudo systemctl daemon-reload
sudo systemctl enable --now pgwd.timer
systemctl list-timers --all | grep pgwdTo change the interval, edit the timer: OnUnitActiveSec=5min → e.g. OnUnitActiveSec=10min, then sudo systemctl daemon-reload.
Env file example (/etc/pgwd.env or /etc/pgwd/pgwd.env)
PGWD_DB_URL=postgres://user:pass@localhost:5432/mydb
PGWD_THRESHOLD_TOTAL=80
PGWD_THRESHOLD_IDLE=50
PGWD_SLACK_WEBHOOK=https://hooks.slack.com/services/...
PGWD_LOKI_URL=http://localhost:3100/loki/api/v1/push
PGWD_INTERVAL=60
# For timer (one-shot) omit PGWD_INTERVAL or set 0Optional: Run the service as a dedicated user: create useradd -r -s /bin/false pgwd, then in the unit add User=pgwd and Group=pgwd. Ensure that user can read the env file (e.g. same group or move secrets to a credential store).
