Skip to content

Commit 92a7c20

Browse files
committed
Updated for pg_stat extension and 18-trixie
1 parent 2a31cae commit 92a7c20

File tree

7 files changed

+153
-81
lines changed

7 files changed

+153
-81
lines changed

.github/workflows/docker.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
name: Build Containers
22
on:
33
release:
4-
types: [ created, edited ]
4+
types: [created, edited]
55
workflow_dispatch:
66
inputs:
77
tag:
8-
description: 'tag'
8+
description: "tag"
99
required: true
10-
default: 'latest'
10+
default: "latest"
1111
type: string
1212
jobs:
1313
build:
1414
name: Build
1515
strategy:
1616
matrix:
17-
arch: [ amd64, arm64 ]
18-
version: [ 17-bookworm ]
17+
arch: [amd64, arm64]
18+
version: [17-bookworm, 18-trixie]
1919
runs-on:
2020
- ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || matrix.arch }}
2121
env:
@@ -28,7 +28,7 @@ jobs:
2828
run: |
2929
sudo apt -y update
3030
sudo apt -y install build-essential git
31-
git config --global advice.detachedHead false
31+
git config --global advice.detachedHead false
3232
- name: Checkout
3333
uses: actions/checkout@v4
3434
with:
@@ -40,15 +40,15 @@ jobs:
4040
username: ${{ github.repository_owner }}
4141
password: ${{ secrets.GITHUB_TOKEN }}
4242
- name: Build and Push
43-
id: build
43+
id: build
4444
run: |
4545
make docker && make docker-push
4646
manifest:
4747
name: Manifest
4848
needs: build
4949
strategy:
5050
matrix:
51-
version: [ 17-bookworm ]
51+
version: [17-bookworm, 18-trixie]
5252
runs-on: ubuntu-latest
5353
steps:
5454
- name: Login

Dockerfile

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@ ARG VERSION=17-bookworm
44
FROM postgres:${VERSION}
55
ARG VERSION
66
LABEL org.opencontainers.image.description="PostgreSQL image with primary/replica support" \
7-
org.opencontainers.image.version="$VERSION"
7+
org.opencontainers.image.version="$VERSION"
88

99
# Install packages postgis and pgvector
1010
ENV POSTGIS_MAJOR=3
1111
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
12-
ca-certificates \
13-
postgresql-$PG_MAJOR-postgis-$POSTGIS_MAJOR \
14-
postgresql-$PG_MAJOR-postgis-$POSTGIS_MAJOR-scripts \
15-
postgresql-$PG_MAJOR-pgvector \
12+
ca-certificates \
13+
postgresql-$PG_MAJOR-postgis-$POSTGIS_MAJOR \
14+
postgresql-$PG_MAJOR-postgis-$POSTGIS_MAJOR-scripts \
15+
postgresql-$PG_MAJOR-pgvector \
1616
&& rm -rf /var/lib/apt/lists/*
1717

1818
# Copy scripts
1919
RUN mkdir -p /docker-entrypoint-initdb.d
2020
COPY --chmod=755 ./scripts/10_primary.sh /docker-entrypoint-initdb.d
2121
COPY --chmod=755 ./scripts/20_replica.sh /docker-entrypoint-initdb.d
22+
COPY --chmod=755 ./scripts/30_ssl.sh /docker-entrypoint-initdb.d
23+
COPY --chmod=755 ./scripts/40_databases.sh /docker-entrypoint-initdb.d
2224

2325
# Set the environment
24-
ENV POSTGRES_REPLICATION_USER=replication \
25-
POSTGRES_REPLICATION_SLOT=replica1
26+
ENV POSTGRES_REPLICATION_USER=replicator \
27+
POSTGRES_REPLICATION_SLOT=replica1

README.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,21 @@ docker command line:
1818
to connect to the primary, in the form `host=<hostname> port=5432`. When not set,
1919
the instance role is a primary.
2020
* `POSTGRES_REPLICATION_PASSWORD`: **Required**: The password for the `POSTGRES_REPLICATION_USER`.
21-
* `POSTGRES_REPLICATION_USER`: **Default is `replicator`**: The user that the primary will use to connect
22-
to the replica.
23-
* `POSTGRES_REPLICATION_SLOT`: **Default is `replica1`** The replication slot for each replica.
21+
* `POSTGRES_REPLICATION_USER`: **Default is `replicator`**: The user that the replica will use to connect to the primary.
22+
* `POSTGRES_REPLICATION_SLOT`: **Default is `replica1`**: The replication slot for each replica.
2423
On the primary, this is a comma-separated list of replication slots. On a replica, this is the name
25-
of the replication slot used for syncronization.
24+
of the replication slot used for synchronization.
2625
* `POSTGRES_DATABASES`: **Optional**: A comma-separated list of databases (and associated owner role,
2726
which has the same name as the database), in addition to the main database.
2827
* `POSTGRES_PASSWORD_<role>`: **Optional**: For any database which is created, you can enable
2928
login and set the password for the database owner role by setting this environment variable. Without
3029
this environment variable, the role will not be able to login.
3130
* `POSTGRES_SSL_CERT`: **Optional**: The SSL certificate file location for the server, within the container.
31+
Requires `POSTGRES_SSL_KEY` to also be set.
3232
* `POSTGRES_SSL_KEY`: **Optional**: The SSL private key file location for the server, within the container.
33-
* `POSTGRES_SSL_CA`: **Optional**: The SSL authority certificate file location for the server, within the
34-
container.
33+
Requires `POSTGRES_SSL_CERT` to also be set.
34+
* `POSTGRES_SSL_CA`: **Optional**: The SSL CA certificate file location for client certificate verification.
35+
Only used when `POSTGRES_SSL_CERT` and `POSTGRES_SSL_KEY` are set.
3536

3637
## Running a Primary server
3738

@@ -79,12 +80,17 @@ the replica.
7980

8081
## Extensions
8182

82-
The docker images also contain [PostGIS](https://postgis.net/) and
83-
[pgvector](https://github.com/pgvector/pgvector) extensions.
83+
The docker images include the following extensions:
84+
85+
* [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) - Pre-loaded for query
86+
performance monitoring. Create the extension with `CREATE EXTENSION pg_stat_statements;` to start collecting
87+
statistics.
88+
* [PostGIS](https://postgis.net/) - Spatial database extender.
89+
* [pgvector](https://github.com/pgvector/pgvector) - Vector similarity search.
8490

8591
## Bugs, feature requests and contributions
8692

87-
You can raise issues and feature requests using
93+
You can raise issues and feature requests using
8894
the [GitHub issue tracker](https://github.com/mutablelogic/docker-postgres/issues)
8995
or send pull requests. The image is built from
9096
the [Official Docker image](https://hub.docker.com/_/postgres).

scripts/10_primary.sh

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,54 @@ if [ ! -z "${POSTGRES_REPLICATION_PRIMARY}" ]; then
99
exit 0
1010
fi
1111

12-
# If there is no replication password, then quit
12+
# Validate required environment variables
1313
if [ -z "${POSTGRES_REPLICATION_PASSWORD}" ]; then
1414
echo "POSTGRES_REPLICATION_PASSWORD needs to be set."
1515
exit 1
1616
fi
1717

18+
if [ -z "${POSTGRES_REPLICATION_USER}" ]; then
19+
echo "POSTGRES_REPLICATION_USER needs to be set."
20+
exit 1
21+
fi
22+
23+
if [ -z "${POSTGRES_REPLICATION_SLOT}" ]; then
24+
echo "POSTGRES_REPLICATION_SLOT needs to be set."
25+
exit 1
26+
fi
27+
1828
# Make the data directory
1929
install -d "${PGDATA}" -o postgres -g postgres -m 700
2030

21-
# Create the replication user
31+
# Create the replication user (idempotent)
2232
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
23-
CREATE USER ${POSTGRES_REPLICATION_USER} WITH REPLICATION ENCRYPTED PASSWORD '${POSTGRES_REPLICATION_PASSWORD}';
33+
DO \$\$
34+
BEGIN
35+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${POSTGRES_REPLICATION_USER}') THEN
36+
CREATE USER ${POSTGRES_REPLICATION_USER} WITH REPLICATION ENCRYPTED PASSWORD '${POSTGRES_REPLICATION_PASSWORD}';
37+
END IF;
38+
END
39+
\$\$;
2440
EOSQL
2541

26-
# Create the replication slots
42+
# Create the replication slots (idempotent)
2743
IFS=',' read -r -a array <<< "${POSTGRES_REPLICATION_SLOT}"
2844
for SLOT in "${array[@]}"; do
2945
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
30-
SELECT pg_create_physical_replication_slot('${SLOT}');
46+
SELECT pg_create_physical_replication_slot('${SLOT}')
47+
WHERE NOT EXISTS (SELECT 1 FROM pg_replication_slots WHERE slot_name = '${SLOT}');
3148
EOSQL
3249
done
3350

3451
# Set configuration for replication
3552
CONF="${PGDATA}/postgresql.conf"
36-
sed -i -e"s/^#wal_level.*$/wal_level=replica/" ${CONF}
37-
sed -i -e"s/^#max_wal_senders.*$/max_wal_senders=10/" ${CONF}
38-
sed -i -e"s/^#max_replication_slots.*$/max_replication_slots=10/" ${CONF}
53+
sed -i -e"s/^#wal_level.*$/wal_level = replica/" ${CONF}
54+
sed -i -e"s/^#max_wal_senders.*$/max_wal_senders = 10/" ${CONF}
55+
sed -i -e"s/^#max_replication_slots.*$/max_replication_slots = 10/" ${CONF}
56+
57+
# Enable pg_stat_statements extension
58+
sed -i -e"s/^#shared_preload_libraries.*$/shared_preload_libraries = 'pg_stat_statements'/" ${CONF}
59+
echo "pg_stat_statements.max = 1000" >> ${CONF}
3960

4061
# Add a replication user to pg_hba.conf
4162
echo "host replication ${POSTGRES_REPLICATION_USER} all scram-sha-256" >> "${PGDATA}/pg_hba.conf"

scripts/20_replica.sh

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,41 @@
33
# The primary instance should be running before starting the replica
44
set -e
55

6-
# Script should only run on the primary node
6+
# Script should only run on a replica node
77
if [ -z "${POSTGRES_REPLICATION_PRIMARY}" ]; then
88
echo "Skipping replica initialisation on a primary."
99
exit 0
1010
fi
1111

12-
# If there is no replication password, then quit
12+
# Validate required environment variables
1313
if [ -z "${POSTGRES_REPLICATION_PASSWORD}" ]; then
1414
echo "POSTGRES_REPLICATION_PASSWORD needs to be set."
1515
exit 1
1616
fi
1717

18+
if [ -z "${POSTGRES_REPLICATION_USER}" ]; then
19+
echo "POSTGRES_REPLICATION_USER needs to be set."
20+
exit 1
21+
fi
22+
23+
if [ -z "${POSTGRES_REPLICATION_SLOT}" ]; then
24+
echo "POSTGRES_REPLICATION_SLOT needs to be set."
25+
exit 1
26+
fi
27+
1828
# Make the data directory
1929
install -d "${PGDATA}" -o postgres -g postgres -m 700
2030

21-
# Set password for replication user
22-
echo "*:*:*:${POSTGRES_REPLICATION_USER}:${POSTGRES_REPLICATION_PASSWORD}" >> /var/lib/postgresql/.pgpass
23-
chmod 600 /var/lib/postgresql/.pgpass
31+
# Set password for replication user (idempotent)
32+
PGPASS_FILE="/var/lib/postgresql/.pgpass"
33+
PGPASS_ENTRY="*:*:*:${POSTGRES_REPLICATION_USER}:${POSTGRES_REPLICATION_PASSWORD}"
34+
if [ ! -f "${PGPASS_FILE}" ] || ! grep -qF "${PGPASS_ENTRY}" "${PGPASS_FILE}"; then
35+
echo "${PGPASS_ENTRY}" >> "${PGPASS_FILE}"
36+
chmod 600 "${PGPASS_FILE}"
37+
fi
2438

25-
# Stop the server
26-
pg_ctl -D "${PGDATA}" stop -m fast
39+
# Stop the server if running
40+
pg_ctl -D "${PGDATA}" stop -m fast 2>/dev/null || true
2741

2842
# Perform the backup
2943
rm -fr ${PGDATA}/*
@@ -33,13 +47,12 @@ pg_basebackup -v --pgdata="${PGDATA}" \
3347
--dbname="${POSTGRES_REPLICATION_PRIMARY}" --username="${POSTGRES_REPLICATION_USER}" --no-password
3448

3549
# Set configuration for replication
50+
# Note: pg_basebackup --write-recovery-conf already sets primary_conninfo and primary_slot_name
3651
CONF="${PGDATA}/postgresql.conf"
37-
sed -i -e"s/^#max_wal_senders.*$/max_wal_senders=10/" ${CONF}
38-
sed -i -e"s/^#max_replication_slots.*$/max_replication_slots=10/" ${CONF}
39-
sed -i -e"s/^#primary_conninfo.*$/primary_conninfo='user=${POSTGRES_REPLICATION_USER} ${POSTGRES_REPLICATION_PRIMARY}'/" ${CONF}
40-
sed -i -e"s/^#primary_slot_name.*$/primary_slot_name='${POSTGRES_REPLICATION_SLOT}'/" ${CONF}
41-
sed -i -e"s/^#hot_standby.*$/hot_standby=on/" ${CONF}
42-
sed -i -e"s/^#hot_standby_feedback.*$/hot_standby_feedback=on/" ${CONF}
52+
sed -i -e"s/^#*max_wal_senders.*$/max_wal_senders = 10/" ${CONF}
53+
sed -i -e"s/^#*max_replication_slots.*$/max_replication_slots = 10/" ${CONF}
54+
sed -i -e"s/^#*hot_standby.*$/hot_standby = on/" ${CONF}
55+
sed -i -e"s/^#*hot_standby_feedback.*$/hot_standby_feedback = on/" ${CONF}
4356

4457
# Start the server
4558
pg_ctl -D "${PGDATA}" start

scripts/30_ssl.sh

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,42 @@
33
# https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-OPENSSL-CONFIG
44
set -e
55

6-
# Set configuration for replication
7-
CONF="${PGDATA}/postgresql.conf"
6+
# Skip if no SSL cert configured
7+
if [ -z "${POSTGRES_SSL_CERT}" ]; then
8+
echo "No SSL certificate configured, skipping SSL setup."
9+
exit 0
10+
fi
811

9-
if [ ! -z "${POSTGRES_SSL_CERT}" ]; then
10-
if [ ! -f "${POSTGRES_SSL_CERT}" ]; then
11-
echo "POSTGRES_SSL_CERT file not found."
12-
exit 1
13-
fi
14-
sed -i -e"s/^#ssl_cert_file.*$/ssl_cert_file=${POSTGRES_SSL_CERT}/" ${CONF}
12+
# Both cert and key are required
13+
if [ -z "${POSTGRES_SSL_KEY}" ]; then
14+
echo "POSTGRES_SSL_KEY must be set when POSTGRES_SSL_CERT is set."
15+
exit 1
1516
fi
1617

17-
if [ ! -z "${POSTGRES_SSL_KEY}" ]; then
18-
if [ ! -f "${POSTGRES_SSL_KEY}" ]; then
19-
echo "POSTGRES_SSL_KEY file not found."
20-
exit 1
21-
fi
22-
sed -i -e"s/^#ssl_key_file.*$/ssl_key_file=${POSTGRES_SSL_KEY}/" ${CONF}
18+
if [ ! -f "${POSTGRES_SSL_CERT}" ]; then
19+
echo "POSTGRES_SSL_CERT file not found: ${POSTGRES_SSL_CERT}"
20+
exit 1
2321
fi
2422

23+
if [ ! -f "${POSTGRES_SSL_KEY}" ]; then
24+
echo "POSTGRES_SSL_KEY file not found: ${POSTGRES_SSL_KEY}"
25+
exit 1
26+
fi
27+
28+
# Set configuration for SSL
29+
CONF="${PGDATA}/postgresql.conf"
30+
31+
sed -i -e"s|^#ssl = .*$|ssl = on|" ${CONF}
32+
sed -i -e"s|^#ssl_cert_file.*$|ssl_cert_file = '${POSTGRES_SSL_CERT}'|" ${CONF}
33+
sed -i -e"s|^#ssl_key_file.*$|ssl_key_file = '${POSTGRES_SSL_KEY}'|" ${CONF}
34+
35+
# Optional CA file for client certificate verification
2536
if [ ! -z "${POSTGRES_SSL_CA}" ]; then
2637
if [ ! -f "${POSTGRES_SSL_CA}" ]; then
27-
echo "POSTGRES_SSL_CA file not found."
38+
echo "POSTGRES_SSL_CA file not found: ${POSTGRES_SSL_CA}"
2839
exit 1
2940
fi
30-
sed -i -e"s/^#ssl_ca_file.*$/ssl_ca_file=${POSTGRES_SSL_CA}/" ${CONF}
41+
sed -i -e"s|^#ssl_ca_file.*$|ssl_ca_file = '${POSTGRES_SSL_CA}'|" ${CONF}
3142
fi
43+
44+
echo "SSL configured successfully."

scripts/40_databases.sh

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,48 @@ if [ ! -z "${POSTGRES_REPLICATION_PRIMARY}" ]; then
88
exit 0
99
fi
1010

11+
# Skip if no databases configured
12+
if [ -z "${POSTGRES_DATABASES}" ]; then
13+
echo "No additional databases configured, skipping."
14+
exit 0
15+
fi
16+
1117
# Create the databases
1218
IFS=',' read -r -a array <<< "${POSTGRES_DATABASES}"
1319
for NAME in "${array[@]}"; do
14-
# if there is an environment variable called POSTGRES_PASSWORD_<NAME>, then use that password
15-
if [ ! -z "${!POSTGRES_PASSWORD_${NAME}}" ]; then
16-
ALTER_ROLE="ALTER ROLE ${NAME} WITH PASSWORD ${!POSTGRES_PASSWORD_${NAME}}"
17-
else
18-
ALTER_ROLE="ALTER ROLE ${NAME} WITH NOLOGIN"
19-
fi
20-
# Execute SQL to create the database and role
20+
# Build the password variable name and get its value
21+
PASSWORD_VAR="POSTGRES_PASSWORD_${NAME}"
22+
PASSWORD="${!PASSWORD_VAR}"
23+
24+
echo "Creating database and role: ${NAME}"
25+
26+
# Create role if it doesn't exist
2127
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
22-
DO $$ BEGIN
23-
-- Create database
24-
CREATE EXTENSION IF NOT EXISTS dblink;
25-
IF NOT EXISTS (
26-
SELECT 1 FROM pg_database WHERE datname = '${NAME}'
27-
) THEN
28-
PERFORM dblink_exec('dbname=${POSTGRES_DB}', 'CREATE DATABASE ${NAME}');
28+
DO \$\$
29+
BEGIN
30+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${NAME}') THEN
31+
CREATE ROLE ${NAME};
2932
END IF;
33+
END
34+
\$\$;
35+
EOSQL
3036

31-
-- Create role
32-
CREATE ROLE ${NAME};
33-
${ALTER_ROLE};
34-
ALTER DATABASE ${NAME} OWNER TO ${NAME};
35-
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
36-
END $$;
37+
# Set password or NOLOGIN
38+
if [ ! -z "${PASSWORD}" ]; then
39+
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
40+
ALTER ROLE ${NAME} WITH LOGIN PASSWORD '${PASSWORD}';
3741
EOSQL
42+
else
43+
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
44+
ALTER ROLE ${NAME} WITH NOLOGIN;
45+
EOSQL
46+
fi
47+
48+
# Create database if it doesn't exist
49+
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL
50+
SELECT 'CREATE DATABASE ${NAME} OWNER ${NAME}'
51+
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${NAME}')\gexec
52+
EOSQL
53+
54+
echo "Created database and role: ${NAME}"
3855
done

0 commit comments

Comments
 (0)