PostgreSQL with modular extensions and runtime parameters. https://git.openso.de/openso/postgres
  • Shell 57.4%
  • Python 29.6%
  • Dockerfile 13%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Marcel Henneke 91b85a0729
All checks were successful
ci / compose (push) Successful in 12s
ci / shellcheck (push) Successful in 2s
ci / conventions (push) Successful in 2s
ci / image (push) Successful in 51s
Add VectorChord extension, server defaults, and healthcheck (#3)
2026-11-01 14:38:23 +01:00
.forgejo Rebuild gosu with Go 1.26.10 and add CI actionlint config (#1) 2026-11-01 13:13:29 +01:00
.githooks Build the PostgreSQL image with pinned extensions. 2026-11-01 12:36:26 +01:00
.openso/scripts Rebuild gosu with Go 1.26.10 and add CI actionlint config (#1) 2026-11-01 13:13:29 +01:00
.dockerignore Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00
AGENTS.md Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00
Dockerfile Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00
entrypoint.postgresql.sh Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00
healthcheck.postgresql.sh Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00
init_extensions.postgresql.sh Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00
LICENSE Build the PostgreSQL image with pinned extensions. 2026-11-01 12:36:26 +01:00
README.md Add VectorChord extension, server defaults, and healthcheck (#3) 2026-11-01 14:38:23 +01:00

postgres

PostgreSQL 18.6 with pgvector and ParadeDB pg_search baked into one image. Runtime switches enable those extensions in a database. The switches do not install packages and do not select another image.

This repository is the image. Every app uses it. A new major changes the FROM pin here. Do not open a second repository for one app or one extension.

Image

git.openso.de/openso/postgres:main

docker-templates uses git.openso.de/openso/postgres with no tag. Docker resolves that reference to :latest. :latest tracks the same build as :main.

The official docker-entrypoint.sh stays the image entrypoint and still drops to the postgres user (uid/gid 999) before the server starts. Deployments set the container command to /usr/local/bin/entrypoint.postgresql.sh. Leave the container user unset so that wrapper can prepare the data directory and then exec the official entrypoint.

Use

Mount a volume at /var/lib/postgresql. In this major, PGDATA is /var/lib/postgresql/18/docker. The official image still reads POSTGRES_USER (default postgres), POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE, POSTGRES_DB (default matches POSTGRES_USER), PGDATA, and TZ. On an empty data directory the password is required. A later start reuses the existing cluster and does not read the password again.

POSTGRES_PASSWORD_FILE points at a Docker secret. Do not put the password in the Compose file.

services:
  postgresql:
    image: git.openso.de/openso/postgres
    command: ["/usr/local/bin/entrypoint.postgresql.sh"]
    shm_size: 128mb
    environment:
      TZ: Europe/Berlin
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD_FILE: /run/secrets/POSTGRES_PASSWORD
      POSTGRES_VECTOR: "true"
      POSTGRES_PG_SEARCH: "false"
      POSTGRES_PG_STAT_STATEMENTS: "true"
      POSTGRES_VECTORCHORD: "false"
      # POSTGRES_EXTRA_OPTS: max_wal_size=5GB shared_buffers=512MB work_mem=16MB
      # DB_STORAGE_TYPE: HDD
    secrets:
      - POSTGRES_PASSWORD
    volumes:
      - postgresql-data:/var/lib/postgresql:rw
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready --username \"$$POSTGRES_USER\" --dbname \"$$POSTGRES_DB\""]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 40s

secrets:
  POSTGRES_PASSWORD:
    file: ./secrets/POSTGRES_PASSWORD

volumes:
  postgresql-data:

Create ./secrets/POSTGRES_PASSWORD before the first start. The file is one line, the password, and stays out of git. POSTGRES_VECTOR, POSTGRES_PG_SEARCH, POSTGRES_PG_STAT_STATEMENTS, and POSTGRES_VECTORCHORD are optional. Leave a switch unset or false to keep that extension out of the database. Publishing 5432 is only for a client on the Docker host. An internal stack omits ports and reaches the service as postgresql:5432.

Connect from the host after the healthcheck passes:

docker compose exec postgresql \
  psql --username app --dbname app \
  --command '\dx'

\dx lists the extensions created for the switches that were true on first initialization.

Environment

Boolean switches accept true and false, in any letter case, with surrounding spaces ignored. Empty or unset means false. Any other value stops startup.

Variable Empty or unset true
POSTGRES_VECTOR vector stays unused CREATE EXTENSION vector
POSTGRES_PG_SEARCH pg_search stays unused creates pg_search and also enables vector; adds pg_search to shared_preload_libraries
POSTGRES_PG_STAT_STATEMENTS pg_stat_statements stays unused creates it and adds it to shared_preload_libraries
POSTGRES_VECTORCHORD vchord stays unused creates vector and vchord, preloads vchord.so, and applies the Immich server profile
POSTGRES_SHARED_PRELOAD_LIBRARIES preload list is derived from the switches that value is the preload list
POSTGRES_AUTO_UPDATE_EXTENSIONS defaults to true exact value true updates enabled extensions on an existing data directory to the packages in the image

pg_stat_statements is already in the official image. pg_cron is absent. A new extension needs a package pin in the Dockerfile and a new boolean switch in both startup scripts.

POSTGRES_VECTORCHORD=true is the Immich database profile. It enables vector because VectorChord requires it. On top of the SSD or HDD profile it sets search_path="$user", public, max_wal_size=5GB, shared_buffers=512MB, work_mem=16MB, autovacuum_vacuum_scale_factor=0.1, autovacuum_analyze_scale_factor=0.05, and autovacuum_vacuum_cost_limit=1000. POSTGRES_EXTRA_OPTS still overrides any of those. DB_USERNAME, DB_DATABASE_NAME, DB_PASSWORD, and DB_PASSWORD_FILE fill the official POSTGRES_* names when those are empty. The data directory must be local Linux storage or a named volume. IGNORE_DATABASE_FSTYPE=true bypasses that check. Give this service a mem_limit above 512MB. Use /usr/local/bin/healthcheck.postgresql.sh as the healthcheck so a checksum failure marks the container unhealthy. An override of POSTGRES_SHARED_PRELOAD_LIBRARIES has to keep vchord.so in the list.

Server settings below are separate from the extension switches. POSTGRES_INITDB_ARGS is read by the official entrypoint on an empty data directory. An existing cluster ignores it. Leave the variable unset for data checksums. Set it, including to an empty value, to replace that default. A replacement does not keep --data-checksums unless the new value still contains it.

DB_STORAGE_TYPE accepts SSD or HDD, in any letter case, with surrounding spaces ignored. Empty or unset means SSD.

POSTGRES_EXTRA_OPTS is a list of name=value settings. Separate entries with spaces or newlines. A value cannot contain whitespace. The same name later in the list replaces the earlier one, including a built-in default such as wal_compression. hba_file is rejected. shared_preload_libraries in this list replaces the list derived from the switches.

Variable Empty or unset Set
POSTGRES_INITDB_ARGS initdb --data-checksums on a new data directory that value is passed to initdb
DB_STORAGE_TYPE SSD profile HDD leaves effective_io_concurrency and random_page_cost at the PostgreSQL defaults
POSTGRES_EXTRA_OPTS no extra settings each name=value is a -c flag

Every start also sets summarize_wal=on and wal_compression=on. The SSD profile adds effective_io_concurrency=200 and random_page_cost=1.2. Memory and WAL sizing stay with the app, except the VectorChord profile above. The official image already initializes a new cluster as UTF-8 because LANG is en_US.utf8.

Startup

The wrapper then:

  1. Prepares /var/lib/postgresql and PGDATA for the PostgreSQL user. Root in a hardened service has no DAC_OVERRIDE, so the parent directory has to be writable by UID 999 before maintenance can create a sibling of the data directory.
  2. Writes /tmp/pg_hba.conf. Local sockets use trust. TCP and replication use scram-sha-256.
  3. Reads the boolean switches. POSTGRES_PG_SEARCH=true also enables vector.
  4. Starts PostgreSQL with summarize_wal=on, wal_compression=on, the SSD profile unless DB_STORAGE_TYPE=HDD, shared_preload_libraries when pg_search, pg_stat_statements, or vchord is enabled, the Immich profile when POSTGRES_VECTORCHORD=true, then POSTGRES_EXTRA_OPTS. hba_file=/tmp/pg_hba.conf is last. POSTGRES_SHARED_PRELOAD_LIBRARIES replaces the derived preload list when it is set.
  5. On an existing data directory, when POSTGRES_AUTO_UPDATE_EXTENSIONS=true and at least one switch is true, starts PostgreSQL once, runs CREATE EXTENSION IF NOT EXISTS and ALTER EXTENSION UPDATE in one transaction, then stops that temporary server.
  6. Execs docker-entrypoint.sh postgres with those flags.

/docker-entrypoint-initdb.d/init_extensions.sh runs once, on the first initialization of an empty data directory. It creates the same extension set. Later starts use the wrapper in step 5.

summarize_wal=on is there for physical incremental backup with pg_basebackup --incremental.

Changing a switch from false to true on a database that already exists creates that extension on the next start while POSTGRES_AUTO_UPDATE_EXTENSIONS stays true. pg_search and pg_stat_statements still need their preload entry, which the wrapper adds before that temporary start.

Contents

Piece Pin
PostgreSQL postgres:18.6
pgvector postgresql-18-pgvector=0.8.7-1.pgdg13+1
pg_search ParadeDB 0.26.4, package postgresql-18-pg-search_0.26.4-1PARADEDB-<dist>_<arch>.deb, checksum taken from that GitHub release
VectorChord 1.1.1, package postgresql-18-vchord_1.1.1-1_<arch>.deb from the VectorChord GitHub release, checksum taken from that release
gosu tag 1.19, rebuilt with Go 1.26.10 and golang.org/x/sys v0.49.0
Build tools removed after install ca-certificates=20250419+deb13u1, curl=8.14.1-2+deb13u7, jq=1.7.1-6+deb13u4

The gosu binary shipped in postgres:18.6 is Go 1.24.6. The image scan fails on fixed HIGH and CRITICAL findings in that standard library. This image replaces /usr/local/bin/gosu with a build of the same 1.19 tag. The build checks that gosu --version reports go1.26.10.

Build

A push to main builds and pushes :main and :latest. A tag v* builds and pushes :<tag> and :latest. The first commit on main is that push, so the first image is produced without a manual build.

A version bump is a change of the FROM tag, an apt pin, POSTGRES_PG_SEARCH_VERSION, GOSU_VERSION, or GOLANG_X_SYS_VERSION. Do not resolve a floating latest release during the build.