Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ concurrency:
permissions: read-all

jobs:
docker:
docker-build:
Comment thread
atodorov marked this conversation as resolved.
runs-on: ubuntu-26.04
strategy:
matrix:
Expand All @@ -34,7 +34,7 @@ jobs:

docker images

test:
end-to-end:
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v7
Expand Down
9 changes: 9 additions & 0 deletions 17/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Copyright (c) 2026 Alexander Todorov <atodorov@otb.bg>
#
# Licensed under GNU Affero General Public License v3 or later (AGPLv3+)
# https://www.gnu.org/licenses/agpl-3.0.html

FROM postgres:17@sha256:e38411452a464af89e5adadb8d223bf53b898d47d6ef918b2d58c08707350449

COPY ./docker-entrypoint-initdb.d/00_enable_ssl.sh /docker-entrypoint-initdb.d/

COPY entrypoint.sh /usr/local/bin/
ENTRYPOINT ["entrypoint.sh"]
CMD ["postgres"]
34 changes: 34 additions & 0 deletions 17/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

# Copyright (c) 2026 Alexander Todorov <atodorov@otb.bg>
#
# Licensed under GNU Affero General Public License v3 or later (AGPLv3+)
# https://www.gnu.org/licenses/agpl-3.0.html

echo "INFO: check if replication is configured"
if [ ! -s "$PGDATA/PG_VERSION" ] && [ -n "$POSTGRES_REPLICATION_USER" ]; then
mkdir -p "$PGDATA"
chmod 00700 "$PGDATA" || :

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this block runs before docker-entrypoint.sh, pg_basebackup populates $PGDATA directly and the standard entrypoint's initdb logic will see an existing PG_VERSION and skip docker-entrypoint-initdb.d entirely. That's probably what you want for a replica (the data comes from the primary), but it also means postgresql.conf/pg_hba.conf tuning done in initdb scripts (like 00_enable_ssl.sh) never runs against the replica's freshly-synced config. Since --write-recovery-conf only appends replication settings, the replica ends up with whatever config was baked into the primary backup. Worth a comment here making that explicit, or confirming that the base image's config is sufficient for the replica to actually serve SSL connections.


if [ -z "$POSTGRES_REPLICATION_PASSWORD" ]; then
echo "ERROR: POSTGRES_REPLICATION_PASSWORD is undefined"
exit 1
fi

if [ -z "$POSTGRES_PRIMARY_HOST" ]; then
echo "ERROR: POSTGRES_PRIMARY_HOST is undefined"
exit 2
fi

echo "INFO: starting initial wal sync"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sslmode=require here hard-fails if the primary doesn't have SSL configured. For a replication connection that's fine in this repo (SSL is forced), but the connection string is built by directly interpolating $POSTGRES_REPLICATION_PASSWORD into a URI — if the password ever contains @, : or / this will silently produce a malformed DSN. Consider passing credentials via PGPASSWORD and --host/--username flags instead of embedding them in the URL.

pg_basebackup \
--dbname "postgres://$POSTGRES_REPLICATION_USER:$POSTGRES_REPLICATION_PASSWORD@$POSTGRES_PRIMARY_HOST/postgres?sslmode=require" \
--pgdata "$PGDATA" \
--progress --verbose --write-recovery-conf --wal-method stream \

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using $POSTGRES_REPLICATION_USER as the replication slot name means the slot name is the same for every replica that connects with this user. If you ever spin up a second replica of postgres_17 (or tear one down and bring up a new one while the old slot still exists on the primary), pg_basebackup --create-slot will fail with "replication slot already exists", and worse, the old slot will keep retaining WAL on the primary even after the replica is gone. Consider using a dedicated slot-name variable (e.g. POSTGRES_REPLICATION_SLOT) defaulting to something unique per replica (hostname or container name), and document that operators must drop stale slots on the primary.

--create-slot --slot "$POSTGRES_REPLICATION_USER"

echo "INFO: completed initial wal sync"
fi

echo "INFO: starting postgres"
exec docker-entrypoint.sh "$@"
4 changes: 4 additions & 0 deletions 18/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
FROM postgres:18@sha256:06cad38a5d9f5d24b4d83d86def30795d5e4b757fedbf5281172b576dedcd941

COPY ./docker-entrypoint-initdb.d/00_enable_ssl.sh /docker-entrypoint-initdb.d/

COPY entrypoint.sh /usr/local/bin/
ENTRYPOINT ["entrypoint.sh"]
CMD ["postgres"]
34 changes: 34 additions & 0 deletions 18/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

# Copyright (c) 2026 Alexander Todorov <atodorov@otb.bg>
#
# Licensed under GNU Affero General Public License v3 or later (AGPLv3+)
# https://www.gnu.org/licenses/agpl-3.0.html

echo "INFO: check if replication is configured"
if [ ! -s "$PGDATA/PG_VERSION" ] && [ -n "$POSTGRES_REPLICATION_USER" ]; then
mkdir -p "$PGDATA"
chmod 00700 "$PGDATA" || :

if [ -z "$POSTGRES_REPLICATION_PASSWORD" ]; then
echo "ERROR: POSTGRES_REPLICATION_PASSWORD is undefined"
exit 1
fi

if [ -z "$POSTGRES_PRIMARY_HOST" ]; then
echo "ERROR: POSTGRES_PRIMARY_HOST is undefined"
exit 2
fi

echo "INFO: starting initial wal sync"
pg_basebackup \
--dbname "postgres://$POSTGRES_REPLICATION_USER:$POSTGRES_REPLICATION_PASSWORD@$POSTGRES_PRIMARY_HOST/postgres?sslmode=require" \
--pgdata "$PGDATA" \
--progress --verbose --write-recovery-conf --wal-method stream \

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same slot-name collision concern as in 17/entrypoint.sh — slot is named after the replication user, so multiple replicas sharing the user will conflict.

--create-slot --slot "$POSTGRES_REPLICATION_USER"

echo "INFO: completed initial wal sync"
fi

echo "INFO: starting postgres"
exec docker-entrypoint.sh "$@"
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ services:
restart: always
volumes:
- db17_data:/var/lib/postgresql
- ./initdb.d/01_replication_users.sh:/docker-entrypoint-initdb.d/01_replication_users.sh:Z
environment:
POSTGRES_DB: kiwitcms
POSTGRES_USER: kiwitcms
Expand Down Expand Up @@ -73,6 +74,7 @@ services:
dockerfile: Dockerfile
volumes:
- db18_data:/var/lib/postgresql
- ./initdb.d/01_replication_users.sh:/docker-entrypoint-initdb.d/01_replication_users.sh:Z

web:
container_name: web
Expand All @@ -99,3 +101,8 @@ services:
volumes:
db17_data:
db18_data:


Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinning the network name to postgres_default makes the test's --network=postgres_default work, but it also means this compose project can no longer be run twice side-by-side on the same host (both will try to claim the same network) and it overrides compose's project-name isolation. Since the test already knows the compose project, it could discover the network dynamically (docker network ls --filter name=_default) or the replicas could be added to the compose file under a profile instead — either avoids hard-coding the name.

networks:
default:
name: postgres_default
23 changes: 23 additions & 0 deletions initdb.d/01_replication_users.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# Copyright (c) 2026 Alexander Todorov <atodorov@otb.bg>
#
# Licensed under GNU Affero General Public License v3 or later (AGPLv3+)
# https://www.gnu.org/licenses/agpl-3.0.html

set -eu

replication_user() {
local username=$1
local password=$2

psql --set ON_ERROR_STOP=1 \
--set=username="$username" \
--set=password="$password" \
--username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER :"username" WITH ENCRYPTED PASSWORD :'password' REPLICATION;
EOSQL
}

replication_user "rpl_usr_17" "replicate-me"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both replication users are created with the same hard-coded password replicate-me, and the file is mounted into the primary unconditionally. Anyone who can reach the primary's port 5432 now has replication credentials baked into the image's compose setup. At minimum, source these from environment variables (e.g. POSTGRES_REPLICATION_PASSWORD_RPL_USR_17) so production deployments aren't stuck with a password that's committed to git; and consider scoping the users' pg_hba.conf entries to the replica subnet rather than relying on the default.

replication_user "rpl_usr_18" "replicate-me"
24 changes: 24 additions & 0 deletions testing/test_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ rlJournalStart
rlAssertGrep "pg_hba.conf rejects connection .* no encryption" "$rlRun_LOG"
rlPhaseEnd

rlPhaseStartTest "Start replication containers"
rlRun -t -c "docker run -d --name=replica_17 --network=postgres_default -e POSTGRES_REPLICATION_USER=rpl_usr_17 -e POSTGRES_REPLICATION_PASSWORD=replicate-me -e POSTGRES_PRIMARY_HOST=postgres_17 postgres-postgres_17:latest"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two fixed sleep 120 calls add ~4 minutes to every CI run and will still be flaky on a slow runner (pg_basebackup of a freshly-initialised Kiwi TCMS DB is usually fast, but not guaranteed). Poll instead: loop on docker logs replica_17 | grep -q 'completed initial wal sync' (or check pg_isready / SELECT pg_is_in_recovery() inside the container) with a timeout, so the test proceeds as soon as the replica is actually ready.

sleep 120
rlRun -t -c "docker logs replica_17"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replicas are started with docker run against the compose-created network, but they never get cleaned up if an earlier phase fails — rlPhaseStartCleanup only runs on the happy path in some beakerlib configs. Consider registering the replica containers for cleanup the same way the compose stack is, or at least docker rm -f replica_17 replica_18 defensively at the start of the phase in case a previous run left them behind.

rlRun -t -c "docker run -d --name=replica_18 --network=postgres_default -e POSTGRES_REPLICATION_USER=rpl_usr_18 -e POSTGRES_REPLICATION_PASSWORD=replicate-me -e POSTGRES_PRIMARY_HOST=postgres_18 postgres-postgres_18:latest"
sleep 120
rlRun -t -c "docker logs replica_18"
rlPhaseEnd

rlPhaseStartTest "Container restart"
rlRun -t -c "docker compose restart"
assert_up_and_running
Expand All @@ -56,7 +66,21 @@ rlJournalStart
assert_up_and_running
rlPhaseEnd

rlPhaseStartTest "Check content in replicated databases"
REPLICA_17_ADDRESS=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' replica_17)
rlRun -t -c "psql --dbname 'postgres://kiwitcms:kiwitcms@$REPLICA_17_ADDRESS/kiwitcms?sslmode=require' -c 'SELECT * FROM management_priority;' | grep '5 rows'"

REPLICA_18_ADDRESS=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' replica_18)
rlRun -t -c "psql --dbname 'postgres://kiwitcms:kiwitcms@$REPLICA_18_ADDRESS/kiwitcms?sslmode=require' -c 'SELECT * FROM management_priority;' | grep '5 rows'"
rlPhaseEnd

rlPhaseStartCleanup
rlRun -t -c "docker kill replica_17"
rlRun -t -c "docker rm replica_17"

rlRun -t -c "docker kill replica_18"
rlRun -t -c "docker rm replica_18"

rlRun -t -c "docker compose down"
if [ -n "$ImageOS" ]; then
rlRun -t -c "docker volume rm postgres_db17_data"
Expand Down