PostgreSQL Docker Image
  • Shell 89.2%
  • Dockerfile 10.8%
Find a file
Torsten Raudssus 5b4bc9fad3 Fix DB provisioning psql var interpolation; add smoke test
psql does not interpolate :'db'/:"db" variables in -c command strings (only via
stdin/file), so docker_provision_databases sent the literal :'db' to the server
and aborted init with a syntax error as soon as a database was provisioned.
Interpolate the names (constrained to [a-z0-9_] by their env var origin) directly
in bash instead, matching how docker_provision_roles already builds its SQL.

Add test/smoke-test.sh: builds the image and asserts provisioning, tenant
isolation, config pass-through, the pg_cron-in-postgres fix, and seed-vs-reconcile
behaviour end-to-end. All 14 checks pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:51:28 +02:00
docs/adr Add declarative DB/role provisioning, tenant isolation and ENV config 2026-06-02 20:28:23 +02:00
test Fix DB provisioning psql var interpolation; add smoke test 2026-06-02 20:51:28 +02:00
.dockerignore docker should ignore bash history 2024-03-31 00:21:09 +01:00
.gitignore Updated to PostgreSQL 16 and added proper locale settings 2024-03-01 02:54:24 +01:00
CONTEXT.md Add declarative DB/role provisioning, tenant isolation and ENV config 2026-06-02 20:28:23 +02:00
docker-entrypoint.sh Fix DB provisioning psql var interpolation; add smoke test 2026-06-02 20:51:28 +02:00
Dockerfile Add declarative DB/role provisioning, tenant isolation and ENV config 2026-06-02 20:28:23 +02:00
README.md Add declarative DB/role provisioning, tenant isolation and ENV config 2026-06-02 20:28:23 +02:00

PostgreSQL 18 Image

Production-ready PostgreSQL 18 Docker image with dynamic UID/GID, 12 pre-installed extensions (+1 optional), non-root execution, and automatic version mismatch detection.

src.ci/srv/postgres:latest

Quick Start

services:
  db:
    image: "src.ci/srv/postgres:latest"
    volumes:
      - "db-volume:/data"
    environment:
      POSTGRES_PASSWORD: "changeme"
docker run -d -e POSTGRES_PASSWORD=changeme -v db-data:/data src.ci/srv/postgres:latest

Key Features

  • Non-root execution — runs as postgres user, never as root
  • Dynamic UID/GID — configurable at build time to match your host permissions
  • 12 extensions — all pre-installed, activate on demand with CREATE EXTENSION (+ optional pg_trickle via build arg)
  • Auto-configured pg_cron — shared_preload_libraries set and CREATE EXTENSION pg_cron run (in the postgres database) on first init
  • Declarative databases & roles — create extra databases and roles from POSTGRES_DB_* / POSTGRES_ROLE_* env vars; owner-provisioned databases are isolated by default
  • ENV config pass-through — set any postgresql.conf parameter via POSTGRES_CONF_* without touching a config file
  • Version guard — detects old PG16/PG17 data and shows migration instructions
  • Healthcheck — built-in pg_isready check (30s interval, 3s timeout, 4 retries)
  • Docker secrets — supports POSTGRES_PASSWORD_FILE and friends
  • Init scripts — drop .sh, .sql, .sql.gz, .sql.xz, .sql.zst into /docker-entrypoint-initdb.d/

Extensions

All extensions are installed from the official PostgreSQL Debian repository, except pg_trickle which is built from source via pgrx and is opt-in at build time (see ENABLE_PG_TRICKLE below).

Extension Version Description
age 1.7.0 Graph database extension (openCypher queries)
extra_window_functions 1.0 Additional window functions
numeral 1 Numeral datatypes
pg_cron 1.6 Job scheduler
pg_repack 1.5.3 Table reorganization with minimal locks
pg_similarity 1.0 String similarity functions
pg_trickle ¹ 0.81.0 Streaming differential view maintenance (opt-in via ENABLE_PG_TRICKLE=true)
pgmemcache 2.3.0 Memcached interface
pgpcre 1 Perl Compatible Regular Expressions
postgis 3.6.2 Geospatial types and functions
set_user 4.2.0 Dynamic user switching with audit logging
table_log 0.6.1 Table change logging
vector 0.8.2 Vector data type for AI/ML (ivfflat, hnsw)

¹ pg_trickle is only built into the image when the build arg ENABLE_PG_TRICKLE=true is passed (Rust toolchain + pgrx add several minutes and ~1 GB to the build). It is pinned to tag v0.81.0 of trickle-labs/pg-trickle (the project moved from the former grove/pg-trickle). When enabled, it is added to shared_preload_libraries and CREATE EXTENSION pg_trickle is executed automatically in POSTGRES_DB on first init.

pg_cron is always preloaded and activated on first init — no manual postgresql.conf editing required. Because pg_cron is single-database (its scheduler and metadata live in the database named by cron.database_name, default postgres), the extension is created in the postgres database, not in POSTGRES_DB. Schedule jobs against any other database with cron.schedule_in_database('jobname', '* * * * *', 'SQL…', 'target_db').

Databases, roles & configuration

Beyond the official POSTGRES_USER / POSTGRES_DB / POSTGRES_PASSWORD (which keep working unchanged), this image can provision multiple databases and roles declaratively, and pass PostgreSQL config through from the environment. See the design rationale in docs/adr/ and the glossary in CONTEXT.md.

Databases and roles

Objects are declared by embedding their name in the variable name, using a closed set of suffixes parsed from the right (_OWNER, _OPTIONS, _PASSWORD_FILE, _PASSWORD). The name is lowercased; underscores in names are fine because only the known suffixes are stripped.

environment:
  # role "appuser": can log in and create databases, password from a Docker secret
  POSTGRES_ROLE_APPUSER_OPTIONS: "LOGIN,CREATEDB,CONNECTION LIMIT 20"
  POSTGRES_ROLE_APPUSER_PASSWORD_FILE: "/run/secrets/appuser_pw"

  # role "bi": login only
  POSTGRES_ROLE_BI_OPTIONS: "LOGIN"
  POSTGRES_ROLE_BI_PASSWORD: "insecure-inline-password"   # _FILE is preferred

  # database "app" owned by appuser, "my_app" owned by appuser, "scratch" with no owner
  POSTGRES_DB_APP_OWNER: "appuser"
  POSTGRES_DB_MY_APP_OWNER: "appuser"
  POSTGRES_DB_SCRATCH: "1"        # bare declaration: no known suffix -> database "scratch"
  • _OPTIONS is a comma-separated list of standard role attributes (LOGIN, CREATEDB, CONNECTION LIMIT n, …). They map straight onto CREATE ROLE … WITH ….
  • A role with LOGIN but no password aborts startup — unless POSTGRES_HOST_AUTH_METHOD=trust — exactly mirroring how the official image treats an empty POSTGRES_PASSWORD.
  • A database literally named foo_owner collides with the _OWNER suffix; create that one via a SQL init script instead.

Tenant isolation

A database declared with an owner is isolated on creation: CONNECT is revoked from PUBLIC and granted only to its owner, so other roles cannot connect to it. A database without an owner stays vanilla (any role may connect). The primary POSTGRES_DB is unaffected.

This is connection-level isolation only — catalog names (pg_database, pg_roles) remain visible server-wide. For full isolation, run separate instances.

Config pass-through

Any POSTGRES_CONF_<PARAM> variable becomes <param> = <value> in an include file (conf.d/00-env.conf) that is regenerated on every start. Unset a variable and the parameter falls back to its PostgreSQL default — the image never shifts PostgreSQL's own defaults on its own.

environment:
  POSTGRES_CONF_MAX_CONNECTIONS: "200"
  POSTGRES_CONF_SHARED_BUFFERS: "512MB"
  POSTGRES_CONF_IDLE_IN_TRANSACTION_SESSION_TIMEOUT: "5min"

Seed vs. reconcile

By default, databases and roles are provisioned only on first init of an empty /data (the official one-shot behaviour). Set RECONCILE_ON_START=true to additionally re-apply the declared databases and roles on every start:

  • additive — missing databases/roles are created, existing ones are ALTERed (including re-applying a _PASSWORD_FILE, which enables secret rotation). Objects are never dropped; removing a variable does not delete a database or role.
  • Config pass-through always applies on every start, regardless of this flag.

Runtime environment variables (this image's additions)

Variable Description
POSTGRES_ROLE_<NAME>_OPTIONS Comma-separated role attributes (e.g. LOGIN,CREATEDB)
POSTGRES_ROLE_<NAME>_PASSWORD_FILE File containing the role's password (preferred)
POSTGRES_ROLE_<NAME>_PASSWORD Inline role password (discouraged; use _FILE)
POSTGRES_DB_<NAME>_OWNER Owner role for a provisioned (and isolated) database
POSTGRES_DB_<NAME> Bare database declaration (no owner, no isolation)
POSTGRES_CONF_<PARAM> Sets postgresql.conf parameter <param>
RECONCILE_ON_START true re-applies declared databases/roles on every start

Upgrading from PG16 or PG17

The entrypoint automatically detects if the mounted data directory (or any subdirectory) contains a database from an older PostgreSQL version and will abort with clear migration instructions:

ERROR: Found PostgreSQL 17 data in /data,
       but this container runs PostgreSQL 18.

You have two options:

pg_dump / pg_restore (safe, works always):

# 1. Dump from old container
docker exec old-pg pg_dumpall -U postgres > backup.sql
# 2. Start fresh PG18 container
docker run -d -e POSTGRES_PASSWORD=... -v new-data:/data src.ci/srv/postgres:18
# 3. Restore
docker exec -i new-pg psql -U postgres < backup.sql

pg_upgrade (faster, in-place) — requires both old and new PG binaries. See the pg_upgrade documentation.

Build Arguments

Build your own image with custom UID/GID:

services:
  db:
    image: "src.ci/srv/postgres:1010"
    volumes:
      - "db-volume:/data"
    environment:
      POSTGRES_USER: "myuser"
      POSTGRES_DB: "mydb"
      POSTGRES_PASSWORD: "mypass"
    build:
      context: "docker/postgres"
      args:
        SRV_UID: "1010"
        SRV_GID: "1010"
Argument Default Description
SRV_UID 1000 User ID for the postgres user and data ownership
SRV_GID 100 Group ID for the postgres user and data ownership
SRV_LOCALE en_GB Locale for initdb (extended with .UTF-8 for all LC_* categories)
SRV_VERSION 0 Version label (org.opencontainers.image.version)
SRV_APT_GET_INSTALL Additional Debian packages to install at build time
ENABLE_PG_TRICKLE Set to true to build and enable pg_trickle (adds Rust + pgrx to the build)

Important: The data directory /data must exist with matching UID/GID before the container starts. If Docker creates it, it will be root-owned and initdb will fail.

Image Details

Base image buildpack-deps:bookworm
PostgreSQL 18.3 (from apt.postgresql.org)
Data directory /data
User postgres (configurable UID/GID)
Home /var/lib/postgresql
Port 5432
Entrypoint Official docker-library/postgres entrypoint

Tags

Tag PostgreSQL Status
latest, 18 18.3 current
17 17.x archived

Support