Groundskeeper

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.5

The 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:16

For 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.5

Binding 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 groundskeeper

Connection URI

postgresql://groundskeeper:<URL-ENCODED_PASSWORD>@127.0.0.1:5432/groundskeeper

URL-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;
SQL

With 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;
SQL

Do 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-pgdata

Inspect 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.

On this page