Local PostgreSQL with Podman
PostgreSQL is included as the correlation/provenance authoring store, while the decision data plane remains stateless and evaluates immutable compiled snapshots. This reference supports that foundational store, later control-plane development, and the optional G-NAF address store described in Persistence. Keep credentials and local data outside the repository.
Protected external configuration
Create a user-only directory and environment file. The password remains plaintext
despite mode 600; it and the container environment remain readable by the same
OS user and root.
ENV_DIR="$HOME/.config/groundskeeper"
ENV_FILE="$ENV_DIR/postgres.env"
mkdir -p "$ENV_DIR"
chmod 700 "$ENV_DIR"
umask 077
IFS= read -r -s -p 'Postgres password: ' PG_PASSWORD
printf '\n'
printf 'POSTGRES_USER=groundskeeper\nPOSTGRES_PASSWORD=%s\nPOSTGRES_DB=groundskeeper\n' \
"$PG_PASSWORD" >"$ENV_FILE"
unset PG_PASSWORD
chmod 600 "$ENV_FILE"Do not copy this file into the checkout, commit it, or pass the password directly on a command line.
Image and persistent storage
podman volume create groundskeeper-pgdata
# Ordinary control-plane development:
IMAGE=docker.io/library/postgres:16
# Preferred instead when G-NAF/geospatial work requires PostGIS:
# IMAGE=docker.io/postgis/postgis:16-3.5The PostGIS image replaces, rather than supplements or runs alongside, the ordinary PostgreSQL container. Both commands use the same PostgreSQL 16 server line and container name; select one image for a given volume. Before changing image families or versions against existing data, take a backup and verify image and extension compatibility.
Optional first-initialization script
For a new empty volume, create an external initialization script:
cat >"$ENV_DIR/001-extensions.sql" <<'SQL'
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
SQL
chmod 600 "$ENV_DIR/001-extensions.sql"Mount it only with the PostGIS image:
--volume "$ENV_DIR/001-extensions.sql:/docker-entrypoint-initdb.d/001-extensions.sql:ro,Z"postgis is not available in the ordinary postgres:16 image. Initialization
scripts and POSTGRES_* initialization variables run only when the data directory
is empty; recreating a container over the existing named volume does not rerun
them. On systems where SELinux relabeling is unavailable or inappropriate, omit
,Z and use the platform's supported read-only mount labeling.
Start PostgreSQL 16
For ordinary PostgreSQL:
podman run -d \
--name groundskeeper-postgres \
--env-file "$ENV_FILE" \
--volume groundskeeper-pgdata:/var/lib/postgresql/data \
--publish 127.0.0.1:5432:5432 \
--health-cmd='pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
--health-interval=5s \
--health-timeout=5s \
--health-retries=12 \
docker.io/library/postgres:16For G-NAF/PostGIS, use this command instead:
podman run -d \
--name groundskeeper-postgres \
--env-file "$ENV_FILE" \
--volume groundskeeper-pgdata:/var/lib/postgresql/data \
--volume "$ENV_DIR/001-extensions.sql:/docker-entrypoint-initdb.d/001-extensions.sql:ro,Z" \
--publish 127.0.0.1:5432:5432 \
--health-cmd='pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' \
--health-interval=5s \
--health-timeout=5s \
--health-retries=12 \
docker.io/postgis/postgis:16-3.5Binding the host port to 127.0.0.1 prevents LAN exposure. Change only the
host-side port (for example, 127.0.0.1:55432:5432) if 5432 is occupied.
Health checks
timeout 60 podman wait --condition=healthy groundskeeper-postgres
podman inspect --format '{{.State.Health.Status}}' groundskeeper-postgres
podman exec groundskeeper-postgres \
pg_isready -U groundskeeper -d groundskeeperConnection URI
postgresql://groundskeeper:<URL-ENCODED_PASSWORD>@127.0.0.1:5432/groundskeeperURL-encode reserved password characters. Prefer a protected application env file
or individual PG* variables over putting the URI into shell history. Adjust the
port if the host binding changed. Local plaintext transport is acceptable only for
this localhost-only development setup; remote deployments require their own TLS,
identity, and secret-management design.
Install or verify extensions manually
With the ordinary postgres:16 image, install and verify the bundled lexical
extensions only:
podman exec -i groundskeeper-postgres \
psql -v ON_ERROR_STOP=1 -U groundskeeper -d groundskeeper <<'SQL'
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
SELECT extname, extversion
FROM pg_extension
WHERE extname IN ('pg_trgm', 'unaccent')
ORDER BY extname;
SQLWith the PostGIS image, use this when the database already exists or to verify the initialization script:
podman exec -i groundskeeper-postgres \
psql -v ON_ERROR_STOP=1 -U groundskeeper -d groundskeeper <<'SQL'
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
SELECT extname, extversion
FROM pg_extension
WHERE extname IN ('postgis', 'pg_trgm', 'unaccent')
ORDER BY extname;
SQLDo not run CREATE EXTENSION postgis in the ordinary image: it does not supply the
required binaries. pg_trgm supports fuzzy lexical/address candidate retrieval;
unaccent can support normalized search but must not silently change canonical
evidence text.
Lifecycle and destructive cleanup
# Normal lifecycle (data remains in the named volume):
podman stop groundskeeper-postgres
podman start groundskeeper-postgres
# Remove the container only; named-volume data remains:
podman rm -f groundskeeper-postgres
# DESTRUCTIVE: permanently delete the local database volume:
podman volume rm groundskeeper-pgdataInspect and back up required data before the final command. Removing the named volume is the step that causes initialization scripts to run on the next start.
Deferred extensions
pgvector is deferred until a benchmark demonstrates a concrete semantic-
retrieval use case that structured fields, full-text search, and pg_trgm do not
meet. Neither postgres:16 nor postgis/postgis:16-3.5 should be assumed to ship a
PostgreSQL-16-compatible vector extension. If adopted later, select or build an
image that contains compatible PostGIS and pgvector versions, pin it by digest,
verify upgrade support, and only then run CREATE EXTENSION vector. Embeddings are
candidate retrieval only, never identity, sensitivity, or policy evidence.
The following are intentionally not needed for the planned architecture and stay deferred until a measured workload and ownership model justify them:
- TimescaleDB;
- Citus;
pg_cron;pg_partman;- PL/Python;
- PostgresML.